Compare commits

..

19 Commits

Author SHA1 Message Date
21b4b0ef24 Release 1.191.0 (#1244) 2022-09-10 16:13:22 +02:00
4f8fe83a16 Feature/clean up user database schema (#1242)
* Clean up user database schema

* Update changelog
2022-09-10 16:11:49 +02:00
980ad1028c Feature/allow date range change for demo user (#1243)
* Allow date range change

* Update changelog
2022-09-10 16:10:57 +02:00
0d5bc3f51b Release 1.190.0 (#1241) 2022-09-10 13:36:39 +02:00
aece76d98f Feature/add date range component to benchmark comparator (#1240)
* Add date range component

* Update changelog
2022-09-10 13:33:37 +02:00
fc4bb71184 Feature/migrate date range setting to user settings (#1239)
* Migrate date range to user settings

* Refactor currency and view mode in the user user settings

* Update changelog
2022-09-10 11:38:06 +02:00
20bc7ef99c Feature/improve benchmark comparator on mobile (#1238)
* Improve layout for mobile

* Update changelog
2022-09-09 19:44:36 +02:00
7a733ae49b Release 1.189.0 (#1237) 2022-09-08 21:06:37 +02:00
376ce88492 Bugfix/fix benchmark chart (#1236)
* Fix benchmark chart

* Distinguish between currency and unit in tooltip

* Update changelog
2022-09-08 21:05:19 +02:00
c4d83aabe7 Setup tests (#1234) 2022-09-08 17:32:55 +02:00
d4e2cec77e Release 1.188.0 (#1233) 2022-09-06 20:40:59 +02:00
75db7bf79a Bugfix/fix asset profile details dialog (#1232)
* Fix dialog for assets without (first) activity

* Update changelog
2022-09-06 20:39:45 +02:00
3ad99c9991 Add data management (#1230)
* Add data management for benchmarks
2022-09-06 20:39:27 +02:00
00e402d286 Add translations 2022-09-04 09:48:18 +02:00
4ac0484025 Update changelog 2022-09-04 09:45:29 +02:00
75d61bff6d Setup benchmark comparator 2022-09-04 09:45:22 +02:00
0de28d733e Release 1.187.0 (#1227) 2022-09-03 21:42:23 +02:00
3b2f13850c Feature/improve chart calculation (#1226)
* Improve chart calculation

* Update changelog
2022-09-03 21:41:06 +02:00
0cc42ffd7c Add end date parameter (#1224)
* Add end date parameter
2022-09-03 21:28:57 +02:00
73 changed files with 1265 additions and 414 deletions

View File

@ -5,6 +5,55 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.191.0 - 10.09.2022
### Changed
- Removed the `currency` and `viewMode` from the `User` database schema
### Fixed
- Allowed the date range change for the demo user
## 1.190.0 - 10.09.2022
### Added
- Added the date range component to the benchmark comparator
### Changed
- Improved the mobile layout of the benchmark comparator
- Migrated the date range setting from the locale storage to the user settings
- Refactored the `currency` and `view mode` in the user settings
## 1.189.0 - 08.09.2022
### Changed
- Distinguished between currency and unit in the chart tooltip
### Fixed
- Fixed the benchmark chart in the benchmark comparator (experimental)
## 1.188.0 - 06.09.2022
### Added
- Added a benchmark comparator (experimental)
### Fixed
- Improved the asset profile details dialog for assets without a (first) activity in the admin control panel
## 1.187.0 - 03.09.2022
### Added
- Supported units in the line chart component
- Added a new chart calculation engine (experimental)
## 1.186.2 - 03.09.2022 ## 1.186.2 - 03.09.2022
### Changed ### Changed

View File

@ -1,7 +1,18 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { BenchmarkResponse } from '@ghostfolio/common/interfaces'; import {
import { Controller, Get, UseInterceptors } from '@nestjs/common'; BenchmarkMarketDataDetails,
BenchmarkResponse
} from '@ghostfolio/common/interfaces';
import {
Controller,
Get,
Param,
UseGuards,
UseInterceptors
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { BenchmarkService } from './benchmark.service'; import { BenchmarkService } from './benchmark.service';
@ -17,4 +28,21 @@ export class BenchmarkController {
benchmarks: await this.benchmarkService.getBenchmarks() benchmarks: await this.benchmarkService.getBenchmarks()
}; };
} }
@Get(':dataSource/:symbol/:startDateString')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseGuards(AuthGuard('jwt'))
public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string
): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString);
return this.benchmarkService.getMarketDataBySymbol({
dataSource,
startDate,
symbol
});
}
} }

View File

@ -1,4 +1,5 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
@ -18,6 +19,7 @@ import { BenchmarkService } from './benchmark.service';
MarketDataModule, MarketDataModule,
PropertyModule, PropertyModule,
RedisCacheModule, RedisCacheModule,
SymbolModule,
SymbolProfileModule SymbolProfileModule
], ],
providers: [BenchmarkService] providers: [BenchmarkService]

View File

@ -0,0 +1,15 @@
import { BenchmarkService } from './benchmark.service';
describe('BenchmarkService', () => {
let benchmarkService: BenchmarkService;
beforeAll(async () => {
benchmarkService = new BenchmarkService(null, null, null, null, null, null);
});
it('calculateChangeInPercentage', async () => {
expect(benchmarkService.calculateChangeInPercentage(1, 2)).toEqual(1);
expect(benchmarkService.calculateChangeInPercentage(2, 2)).toEqual(0);
expect(benchmarkService.calculateChangeInPercentage(2, 1)).toEqual(-0.5);
});
});

View File

@ -1,13 +1,21 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config'; import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
BenchmarkMarketDataDetails,
BenchmarkResponse,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import Big from 'big.js'; import Big from 'big.js';
import { format } from 'date-fns';
import ms from 'ms'; import ms from 'ms';
import { v4 as uuidv4 } from 'uuid';
@Injectable() @Injectable()
export class BenchmarkService { export class BenchmarkService {
@ -18,9 +26,14 @@ export class BenchmarkService {
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService, private readonly redisCacheService: RedisCacheService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService,
private readonly symbolService: SymbolService
) {} ) {}
public calculateChangeInPercentage(baseValue: number, currentValue: number) {
return new Big(currentValue).div(baseValue).minus(1).toNumber();
}
public async getBenchmarks({ useCache = true } = {}): Promise< public async getBenchmarks({ useCache = true } = {}): Promise<
BenchmarkResponse['benchmarks'] BenchmarkResponse['benchmarks']
> { > {
@ -56,14 +69,15 @@ export class BenchmarkService {
const allTimeHighs = await Promise.all(promises); const allTimeHighs = await Promise.all(promises);
benchmarks = allTimeHighs.map((allTimeHigh, index) => { benchmarks = allTimeHighs.map((allTimeHigh, index) => {
const { marketPrice } = quotes[benchmarkAssets[index].symbol]; const { marketPrice } = quotes[benchmarkAssets[index].symbol] ?? {};
let performancePercentFromAllTimeHigh = new Big(0); let performancePercentFromAllTimeHigh = 0;
if (allTimeHigh) { if (allTimeHigh && marketPrice) {
performancePercentFromAllTimeHigh = new Big(marketPrice) performancePercentFromAllTimeHigh = this.calculateChangeInPercentage(
.div(allTimeHigh) allTimeHigh,
.minus(1); marketPrice
);
} }
return { return {
@ -78,7 +92,7 @@ export class BenchmarkService {
})?.name, })?.name,
performances: { performances: {
allTimeHigh: { allTimeHigh: {
performancePercent: performancePercentFromAllTimeHigh.toNumber() performancePercent: performancePercentFromAllTimeHigh
} }
} }
}; };
@ -93,7 +107,75 @@ export class BenchmarkService {
return benchmarks; return benchmarks;
} }
private getMarketCondition(aPerformanceInPercent: Big) { public async getBenchmarkAssetProfiles(): Promise<UniqueAsset[]> {
return aPerformanceInPercent.lte(-0.2) ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; const benchmarkAssets: UniqueAsset[] =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as UniqueAsset[]) ?? [];
const assetProfiles = await this.symbolProfileService.getSymbolProfiles(
benchmarkAssets
);
return assetProfiles.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
});
}
public async getMarketDataBySymbol({
dataSource,
startDate,
symbol
}: { startDate: Date } & UniqueAsset): Promise<BenchmarkMarketDataDetails> {
const [currentSymbolItem, marketDataItems] = await Promise.all([
this.symbolService.get({
dataGatheringItem: {
dataSource,
symbol
}
}),
this.marketDataService.marketDataItems({
orderBy: {
date: 'asc'
},
where: {
dataSource,
symbol,
date: {
gte: startDate
}
}
})
]);
marketDataItems.push({
...currentSymbolItem,
createdAt: new Date(),
date: new Date(),
id: uuidv4()
});
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
return {
marketData: marketDataItems.map((marketDataItem) => {
return {
date: format(marketDataItem.date, DATE_FORMAT),
value:
marketPriceAtStartDate === 0
? 0
: this.calculateChangeInPercentage(
marketPriceAtStartDate,
marketDataItem.marketPrice
) * 100
};
})
};
}
private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
} }
} }

View File

@ -1,5 +1,6 @@
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { Controller, Get } from '@nestjs/common'; import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { InfoService } from './info.service'; import { InfoService } from './info.service';
@ -8,6 +9,7 @@ export class InfoController {
public constructor(private readonly infoService: InfoService) {} public constructor(private readonly infoService: InfoService) {}
@Get() @Get()
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getInfo(): Promise<InfoItem> { public async getInfo(): Promise<InfoItem> {
return this.infoService.get(); return this.infoService.get();
} }

View File

@ -1,3 +1,4 @@
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
@ -16,6 +17,7 @@ import { InfoService } from './info.service';
@Module({ @Module({
controllers: [InfoController], controllers: [InfoController],
imports: [ imports: [
BenchmarkModule,
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,

View File

@ -1,3 +1,4 @@
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
@ -31,6 +32,7 @@ export class InfoService {
private static CACHE_KEY_STATISTICS = 'STATISTICS'; private static CACHE_KEY_STATISTICS = 'STATISTICS';
public constructor( public constructor(
private readonly benchmarkService: BenchmarkService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
@ -108,6 +110,7 @@ export class InfoService {
platforms, platforms,
systemMessage, systemMessage,
baseCurrency: this.configurationService.get('BASE_CURRENCY'), baseCurrency: this.configurationService.get('BASE_CURRENCY'),
benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(),
currencies: this.exchangeRateDataService.getCurrencies(), currencies: this.exchangeRateDataService.getCurrencies(),
demoAuthToken: this.getDemoAuthToken(), demoAuthToken: this.getDemoAuthToken(),
statistics: await this.getStatistics(), statistics: await this.getStatistics(),

View File

@ -103,7 +103,7 @@ export class OrderController {
impersonationId, impersonationId,
this.request.user.id this.request.user.id
); );
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
let activities = await this.orderService.getOrders({ let activities = await this.orderService.getOrders({
filters, filters,

View File

@ -16,6 +16,7 @@ import {
isBefore, isBefore,
isSameMonth, isSameMonth,
isSameYear, isSameYear,
isWithinInterval,
max, max,
min, min,
set set
@ -167,13 +168,21 @@ export class PortfolioCalculator {
this.transactionPoints = transactionPoints; this.transactionPoints = transactionPoints;
} }
public async getCurrentPositions(start: Date): Promise<CurrentPositions> { public async getCurrentPositions(
if (!this.transactionPoints?.length) { start: Date,
end = new Date(Date.now())
): Promise<CurrentPositions> {
const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
if (!transactionPointsBeforeEndDate.length) {
return { return {
currentValue: new Big(0), currentValue: new Big(0),
hasErrors: false,
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
hasErrors: false,
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
positions: [], positions: [],
@ -182,39 +191,38 @@ export class PortfolioCalculator {
} }
const lastTransactionPoint = const lastTransactionPoint =
this.transactionPoints[this.transactionPoints.length - 1]; transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
// use Date.now() to use the mock for today
const today = new Date(Date.now());
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
let firstIndex = this.transactionPoints.length; let firstIndex = transactionPointsBeforeEndDate.length;
const dates = []; const dates = [];
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
dates.push(resetHours(start)); dates.push(resetHours(start));
for (const item of this.transactionPoints[firstIndex - 1].items) { for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataGatheringItems.push({ dataGatheringItems.push({
dataSource: item.dataSource, dataSource: item.dataSource,
symbol: item.symbol symbol: item.symbol
}); });
currencies[item.symbol] = item.currency; currencies[item.symbol] = item.currency;
} }
for (let i = 0; i < this.transactionPoints.length; i++) { for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
if ( if (
!isBefore(parseDate(this.transactionPoints[i].date), start) && !isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
firstTransactionPoint === null firstTransactionPoint === null
) { ) {
firstTransactionPoint = this.transactionPoints[i]; firstTransactionPoint = transactionPointsBeforeEndDate[i];
firstIndex = i; firstIndex = i;
} }
if (firstTransactionPoint !== null) { if (firstTransactionPoint !== null) {
dates.push(resetHours(parseDate(this.transactionPoints[i].date))); dates.push(
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
);
} }
} }
dates.push(resetHours(today)); dates.push(resetHours(end));
const marketSymbols = await this.currentRateService.getValues({ const marketSymbols = await this.currentRateService.getValues({
currencies, currencies,
@ -241,7 +249,7 @@ export class PortfolioCalculator {
} }
} }
const todayString = format(today, DATE_FORMAT); const endDateString = format(end, DATE_FORMAT);
if (firstIndex > 0) { if (firstIndex > 0) {
firstIndex--; firstIndex--;
@ -254,7 +262,7 @@ export class PortfolioCalculator {
const errors: ResponseError['errors'] = []; const errors: ResponseError['errors'] = [];
for (const item of lastTransactionPoint.items) { for (const item of lastTransactionPoint.items) {
const marketValue = marketSymbolMap[todayString]?.[item.symbol]; const marketValue = marketSymbolMap[endDateString]?.[item.symbol];
const { const {
grossPerformance, grossPerformance,
@ -264,6 +272,7 @@ export class PortfolioCalculator {
netPerformance, netPerformance,
netPerformancePercentage netPerformancePercentage
} = this.getSymbolMetrics({ } = this.getSymbolMetrics({
end,
marketSymbolMap, marketSymbolMap,
start, start,
symbol: item.symbol symbol: item.symbol
@ -700,10 +709,12 @@ export class PortfolioCalculator {
} }
private getSymbolMetrics({ private getSymbolMetrics({
end,
marketSymbolMap, marketSymbolMap,
start, start,
symbol symbol
}: { }: {
end: Date;
marketSymbolMap: { marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [symbol: string]: Big };
}; };
@ -726,13 +737,12 @@ export class PortfolioCalculator {
} }
const dateOfFirstTransaction = new Date(first(orders).date); const dateOfFirstTransaction = new Date(first(orders).date);
const endDate = new Date(Date.now());
const unitPriceAtStartDate = const unitPriceAtStartDate =
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
const unitPriceAtEndDate = const unitPriceAtEndDate =
marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol]; marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol];
if ( if (
!unitPriceAtEndDate || !unitPriceAtEndDate ||
@ -785,7 +795,7 @@ export class PortfolioCalculator {
orders.push({ orders.push({
symbol, symbol,
currency: null, currency: null,
date: format(endDate, DATE_FORMAT), date: format(end, DATE_FORMAT),
dataSource: null, dataSource: null,
fee: new Big(0), fee: new Big(0),
itemType: 'end', itemType: 'end',

View File

@ -35,11 +35,11 @@ import {
Param, Param,
Query, Query,
UseGuards, UseGuards,
UseInterceptors UseInterceptors,
Version
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { ViewMode } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
@ -110,6 +110,26 @@ 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') @Get('details')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@ -175,7 +195,7 @@ export class PortfolioController {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice, portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency, portfolioPosition.currency,
this.request.user.Settings.currency this.request.user.Settings.settings.baseCurrency
); );
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
@ -278,7 +298,7 @@ export class PortfolioController {
if ( if (
impersonationId || impersonationId ||
this.request.user.Settings.viewMode === ViewMode.ZEN || this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
performanceInformation.performance = nullifyValuesInObject( performanceInformation.performance = nullifyValuesInObject(
@ -358,7 +378,8 @@ export class PortfolioController {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice, portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency, portfolioPosition.currency,
this.request.user?.Settings?.currency ?? this.baseCurrency this.request.user?.Settings?.settings.baseCurrency ??
this.baseCurrency
); );
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);

View File

@ -5,7 +5,6 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
@ -35,7 +34,8 @@ import {
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
Position, Position,
TimelinePosition TimelinePosition,
UserSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type { import type {
@ -57,6 +57,7 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
addDays,
differenceInDays, differenceInDays,
endOfToday, endOfToday,
format, format,
@ -71,7 +72,7 @@ import {
subDays, subDays,
subYears subYears
} from 'date-fns'; } from 'date-fns';
import { isEmpty, sortBy, uniq, uniqBy } from 'lodash'; import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash';
import { import {
HistoricalDataContainer, HistoricalDataContainer,
@ -85,6 +86,7 @@ const emergingMarkets = require('../../assets/countries/emerging-markets.json');
@Injectable() @Injectable()
export class PortfolioService { export class PortfolioService {
private static readonly MAX_CHART_ITEMS = 250;
private baseCurrency: string; private baseCurrency: string;
public constructor( public constructor(
@ -122,7 +124,7 @@ export class PortfolioService {
this.getDetails(aUserId, aUserId, undefined, aFilters) this.getDetails(aUserId, aUserId, undefined, aFilters)
]); ]);
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
return accounts.map((account) => { return accounts.map((account) => {
let transactionCount = 0; let transactionCount = 0;
@ -197,7 +199,7 @@ export class PortfolioService {
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency, currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
orders: portfolioOrders orders: portfolioOrders
}); });
@ -277,7 +279,7 @@ export class PortfolioService {
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency, currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
orders: portfolioOrders orders: portfolioOrders
}); });
@ -354,6 +356,78 @@ export class PortfolioService {
}; };
} }
public async getChartV2(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, 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
});
portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: []
};
}
const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
daysInMarket / Math.min(daysInMarket, PortfolioService.MAX_CHART_ITEMS)
);
const items: HistoricalDataItem[] = [];
let currentEndDate = startDate;
while (isBefore(currentEndDate, endDate)) {
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate,
currentEndDate
);
items.push({
date: format(currentEndDate, DATE_FORMAT),
value: currentPositions.netPerformancePercentage.toNumber() * 100
});
currentEndDate = addDays(currentEndDate, step);
}
const today = new Date();
if (last(items)?.date !== format(today, DATE_FORMAT)) {
// Add today
const { netPerformancePercentage } =
await portfolioCalculator.getCurrentPositions(startDate, today);
items.push({
date: format(today, DATE_FORMAT),
value: netPerformancePercentage.toNumber() * 100
});
}
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: items
};
}
public async getDetails( public async getDetails(
aImpersonationId: string, aImpersonationId: string,
aUserId: string, aUserId: string,
@ -367,8 +441,8 @@ export class PortfolioService {
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
); );
const userCurrency = const userCurrency =
user.Settings?.currency ?? user.Settings?.settings.baseCurrency ??
this.request.user?.Settings?.currency ?? this.request.user?.Settings?.settings.baseCurrency ??
this.baseCurrency; this.baseCurrency;
const { orders, portfolioOrders, transactionPoints } = const { orders, portfolioOrders, transactionPoints } =
@ -528,7 +602,7 @@ export class PortfolioService {
aImpersonationId: string, aImpersonationId: string,
aSymbol: string aSymbol: string
): Promise<PortfolioPositionDetail> { ): Promise<PortfolioPositionDetail> {
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const orders = ( const orders = (
@ -781,7 +855,7 @@ export class PortfolioService {
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency, currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
orders: portfolioOrders orders: portfolioOrders
}); });
@ -857,7 +931,7 @@ export class PortfolioService {
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency, currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
orders: portfolioOrders orders: portfolioOrders
}); });
@ -917,7 +991,7 @@ export class PortfolioService {
} }
public async getReport(impersonationId: string): Promise<PortfolioReport> { public async getReport(impersonationId: string): Promise<PortfolioReport> {
const currency = this.request.user.Settings.currency; const currency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
const { orders, portfolioOrders, transactionPoints } = const { orders, portfolioOrders, transactionPoints } =
@ -971,7 +1045,7 @@ export class PortfolioService {
accounts accounts
) )
], ],
{ baseCurrency: currency } <UserSettings>this.request.user.Settings.settings
), ),
currencyClusterRisk: await this.rulesService.evaluate( currencyClusterRisk: await this.rulesService.evaluate(
[ [
@ -992,7 +1066,7 @@ export class PortfolioService {
currentPositions currentPositions
) )
], ],
{ baseCurrency: currency } <UserSettings>this.request.user.Settings.settings
), ),
fees: await this.rulesService.evaluate( fees: await this.rulesService.evaluate(
[ [
@ -1002,14 +1076,14 @@ export class PortfolioService {
this.getFees(orders).toNumber() this.getFees(orders).toNumber()
) )
], ],
{ baseCurrency: currency } <UserSettings>this.request.user.Settings.settings
) )
} }
}; };
} }
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> { public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
@ -1183,7 +1257,7 @@ export class PortfolioService {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(), new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.SymbolProfile.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.settings.baseCurrency
); );
}) })
.reduce( .reduce(
@ -1202,7 +1276,7 @@ export class PortfolioService {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.SymbolProfile.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.settings.baseCurrency
); );
}) })
.reduce( .reduce(
@ -1224,7 +1298,7 @@ export class PortfolioService {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(), new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.SymbolProfile.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.settings.baseCurrency
); );
}) })
.reduce( .reduce(
@ -1265,7 +1339,7 @@ export class PortfolioService {
portfolioOrders: PortfolioOrder[]; portfolioOrders: PortfolioOrder[];
}> { }> {
const userCurrency = const userCurrency =
this.request.user?.Settings?.currency ?? this.baseCurrency; this.request.user?.Settings?.settings.baseCurrency ?? this.baseCurrency;
const orders = await this.orderService.getOrders({ const orders = await this.orderService.getOrders({
filters, filters,

View File

@ -1,5 +1,6 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
@ -8,7 +9,7 @@ export class RulesService {
public async evaluate<T extends RuleSettings>( public async evaluate<T extends RuleSettings>(
aRules: Rule<T>[], aRules: Rule<T>[],
aUserSettings: { baseCurrency: string } aUserSettings: UserSettings
) { ) {
return aRules return aRules
.filter((rule) => { .filter((rule) => {

View File

@ -1,7 +0,0 @@
import { ViewMode } from '@prisma/client';
export interface UserSettingsParams {
currency?: string;
userId: string;
viewMode?: ViewMode;
}

View File

@ -1,5 +0,0 @@
export interface UserSettings {
emergencyFund?: number;
locale?: string;
isRestrictedView?: boolean;
}

View File

@ -1,10 +1,35 @@
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import type { DateRange, ViewMode } from '@ghostfolio/common/types';
import {
IsBoolean,
IsIn,
IsNumber,
IsObject,
IsOptional,
IsString
} from 'class-validator';
export class UpdateUserSettingDto { export class UpdateUserSettingDto {
@IsOptional()
@IsString()
baseCurrency?: string;
@IsObject()
@IsOptional()
benchmark?: UniqueAsset;
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
@IsOptional()
dateRange?: DateRange;
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
emergencyFund?: number; emergencyFund?: number;
@IsBoolean()
@IsOptional()
isExperimentalFeatures?: boolean;
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
isRestrictedView?: boolean; isRestrictedView?: boolean;
@ -20,4 +45,8 @@ export class UpdateUserSettingDto {
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
savingsRate?: number; savingsRate?: number;
@IsIn(<ViewMode[]>['DEFAULT', 'ZEN'])
@IsOptional()
viewMode?: ViewMode;
} }

View File

@ -1,10 +0,0 @@
import { ViewMode } from '@prisma/client';
import { IsString } from 'class-validator';
export class UpdateUserSettingsDto {
@IsString()
baseCurrency: string;
@IsString()
viewMode: ViewMode;
}

View File

@ -1,7 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config'; import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config';
import { User } from '@ghostfolio/common/interfaces'; import { User, UserSettings } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -22,12 +22,10 @@ import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { User as UserModel } from '@prisma/client'; import { User as UserModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { size } from 'lodash';
import { UserItem } from './interfaces/user-item.interface'; import { UserItem } from './interfaces/user-item.interface';
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
import { UpdateUserSettingDto } from './update-user-setting.dto'; import { UpdateUserSettingDto } from './update-user-setting.dto';
import { UpdateUserSettingsDto } from './update-user-settings.dto';
import { UserService } from './user.service'; import { UserService } from './user.service';
@Controller('user') @Controller('user')
@ -103,6 +101,12 @@ export class UserController {
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async updateUserSetting(@Body() data: UpdateUserSettingDto) { public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
if ( if (
size(data) === 1 &&
data.dateRange &&
this.request.user.role === 'DEMO'
) {
// Allow date range change for demo user
} else if (
!hasPermission( !hasPermission(
this.request.user.permissions, this.request.user.permissions,
permissions.updateUserSettings permissions.updateUserSettings
@ -130,33 +134,4 @@ export class UserController {
userId: this.request.user.id userId: this.request.user.id
}); });
} }
@Put('settings')
@UseGuards(AuthGuard('jwt'))
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {
if (
!hasPermission(
this.request.user.permissions,
permissions.updateUserSettings
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const userSettings: UserSettingsParams = {
currency: data.baseCurrency,
userId: this.request.user.id
};
if (
hasPermission(this.request.user.permissions, permissions.updateViewMode)
) {
userSettings.viewMode = data.viewMode;
}
return await this.userService.updateUserSettings(userSettings);
}
} }

View File

@ -4,19 +4,20 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config'; import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces'; import {
User as IUser,
UserSettings,
UserWithSettings
} from '@ghostfolio/common/interfaces';
import { import {
getPermissions, getPermissions,
hasRole, hasRole,
permissions permissions
} from '@ghostfolio/common/permissions'; } from '@ghostfolio/common/permissions';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, Role, User, ViewMode } from '@prisma/client'; import { Prisma, Role, User } from '@prisma/client';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
const crypto = require('crypto'); const crypto = require('crypto');
@Injectable() @Injectable()
@ -69,9 +70,7 @@ export class UserService {
accounts: Account, accounts: Account,
settings: { settings: {
...(<UserSettings>Settings.settings), ...(<UserSettings>Settings.settings),
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY, locale: (<UserSettings>Settings.settings)?.locale ?? aLocale
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale,
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
} }
}; };
} }
@ -89,7 +88,7 @@ export class UserService {
} }
public isRestrictedView(aUser: UserWithSettings) { public isRestrictedView(aUser: UserWithSettings) {
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false; return aUser.Settings.settings.isRestrictedView ?? false;
} }
public async user( public async user(
@ -126,21 +125,35 @@ export class UserService {
}; };
if (user?.Settings) { if (user?.Settings) {
if (!user.Settings.currency) { if (!user.Settings.settings) {
// Set default currency if needed user.Settings.settings = {};
user.Settings.currency = UserService.DEFAULT_CURRENCY;
} }
} else if (user) { } else if (user) {
// Set default settings if needed // Set default settings if needed
user.Settings = { user.Settings = {
currency: UserService.DEFAULT_CURRENCY, settings: {},
settings: null,
updatedAt: new Date(), updatedAt: new Date(),
userId: user?.id, userId: user?.id
viewMode: ViewMode.DEFAULT
}; };
} }
// Set default value for base currency
if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
(user.Settings.settings as UserSettings).baseCurrency =
UserService.DEFAULT_CURRENCY;
}
// Set default value for date range
(user.Settings.settings as UserSettings).dateRange =
(user.Settings.settings as UserSettings).viewMode === 'ZEN'
? 'max'
: (user.Settings.settings as UserSettings)?.dateRange ?? 'max';
// Set default value for view mode
if (!(user.Settings.settings as UserSettings).viewMode) {
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
}
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
user.subscription = user.subscription =
this.subscriptionService.getSubscription(Subscription); this.subscriptionService.getSubscription(Subscription);
@ -221,7 +234,9 @@ export class UserService {
}, },
Settings: { Settings: {
create: { create: {
currency: this.baseCurrency settings: {
currency: this.baseCurrency
}
} }
} }
} }
@ -295,7 +310,7 @@ export class UserService {
userId: string; userId: string;
userSettings: UserSettings; userSettings: UserSettings;
}) { }) {
const settings = userSettings as Prisma.JsonObject; const settings = userSettings as unknown as Prisma.JsonObject;
await this.prismaService.settings.upsert({ await this.prismaService.settings.upsert({
create: { create: {
@ -317,33 +332,6 @@ export class UserService {
return; return;
} }
public async updateUserSettings({
currency,
userId,
viewMode
}: UserSettingsParams) {
await this.prismaService.settings.upsert({
create: {
currency,
User: {
connect: {
id: userId
}
},
viewMode
},
update: {
currency,
viewMode
},
where: {
userId: userId
}
});
return;
}
private getRandomString(length: number) { private getRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const result = []; const result = [];

View File

@ -5,6 +5,7 @@ import {
Injectable, Injectable,
NestInterceptor NestInterceptor
} from '@nestjs/common'; } from '@nestjs/common';
import { isArray } from 'lodash';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -36,6 +37,13 @@ export class TransformDataSourceInResponseInterceptor<T>
}); });
} }
if (isArray(data.benchmarks)) {
data.benchmarks.map((benchmark) => {
benchmark.dataSource = encodeDataSource(benchmark.dataSource);
return benchmark;
});
}
if (data.dataSource) { if (data.dataSource) {
data.dataSource = encodeDataSource(data.dataSource); data.dataSource = encodeDataSource(data.dataSource);
} }

View File

@ -1,5 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { EvaluationResult } from './evaluation-result.interface'; import { EvaluationResult } from './evaluation-result.interface';

View File

@ -1,3 +0,0 @@
export interface UserSettings {
baseCurrency: string;
}

View File

@ -1,8 +1,7 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { groupBy } from '@ghostfolio/common/helper'; import { groupBy } from '@ghostfolio/common/helper';
import { TimelinePosition } from '@ghostfolio/common/interfaces'; import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { EvaluationResult } from './interfaces/evaluation-result.interface';
import { RuleInterface } from './interfaces/rule.interface'; import { RuleInterface } from './interfaces/rule.interface';

View File

@ -1,9 +1,9 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioPosition PortfolioPosition,
UserSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,9 +1,9 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioPosition PortfolioPosition,
UserSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,7 +1,6 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PortfolioDetails } from '@ghostfolio/common/interfaces'; import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,7 +1,7 @@
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,7 +1,7 @@
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,7 +1,7 @@
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,7 +1,7 @@
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,6 +1,6 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -166,21 +166,6 @@ export class ExchangeRateDataService {
currencies.push(account.currency); currencies.push(account.currency);
}); });
(
await this.prismaService.settings.findMany({
distinct: ['currency'],
orderBy: [{ currency: 'asc' }],
select: { currency: true },
where: {
currency: {
not: null
}
}
})
).forEach((userSettings) => {
currencies.push(userSettings.currency);
});
( (
await this.prismaService.symbolProfile.findMany({ await this.prismaService.symbolProfile.findMany({
distinct: ['currency'], distinct: ['currency'],

View File

@ -1,7 +1,6 @@
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'; import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service'; import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';

View File

@ -14,8 +14,7 @@ import {
getDateFormatString, getDateFormatString,
getLocale getLocale
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces'; import { LineChartItem, User } from '@ghostfolio/common/interfaces';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { import {
addDays, addDays,

View File

@ -92,7 +92,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
if ( if (
params['assetProfileDialog'] && params['assetProfileDialog'] &&
params['dataSource'] && params['dataSource'] &&
params['dateOfFirstActivity'] &&
params['symbol'] params['symbol']
) { ) {
this.openAssetProfileDialog({ this.openAssetProfileDialog({
@ -170,12 +169,16 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
dateOfFirstActivity, dateOfFirstActivity,
symbol symbol
}: UniqueAsset & { dateOfFirstActivity: string }) { }: UniqueAsset & { dateOfFirstActivity: string }) {
try {
dateOfFirstActivity = format(parseISO(dateOfFirstActivity), DATE_FORMAT);
} catch {}
this.router.navigate([], { this.router.navigate([], {
queryParams: { queryParams: {
dateOfFirstActivity,
dataSource, dataSource,
symbol, symbol,
assetProfileDialog: true, assetProfileDialog: true
dateOfFirstActivity: format(parseISO(dateOfFirstActivity), DATE_FORMAT)
} }
}); });
} }

View File

@ -14,8 +14,8 @@ import { AdminOverviewComponent } from './admin-overview.component';
declarations: [AdminOverviewComponent], declarations: [AdminOverviewComponent],
exports: [], exports: [],
imports: [ imports: [
FormsModule,
CommonModule, CommonModule,
FormsModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,

View File

@ -0,0 +1,52 @@
<div class="row">
<div class="col-md-6 col-xs-12 d-flex">
<div class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate">
<span i18n>Benchmarks</span>
<sup i18n>Beta</sup>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator>
</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-label i18n>Compare with...</mat-label>
<mat-select
name="benchmark"
[compareWith]="compareUniqueAssets"
[value]="benchmark"
(selectionChange)="onChangeBenchmark($event.value)"
>
<mat-option
*ngFor="let currentBenchmark of benchmarks"
[value]="currentBenchmark"
>{{ currentBenchmark.symbol }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoading"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
<div class="chart-container">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '100%',
width: '100%'
}"
></ngx-skeleton-loader>
<canvas
#chartCanvas
class="h-100"
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
></canvas>
</div>

View File

@ -0,0 +1,11 @@
:host {
display: block;
.chart-container {
aspect-ratio: 16 / 9;
ngx-skeleton-loader {
height: 100%;
}
}
}

View File

@ -0,0 +1,237 @@
import 'chartjs-adapter-date-fns';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
ViewChild
} from '@angular/core';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import {
getTooltipOptions,
getTooltipPositionerMapTop,
getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import {
getBackgroundColor,
getDateFormatString,
getTextColor,
parseDate
} from '@ghostfolio/common/helper';
import {
LineChartItem,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import {
Chart,
LineController,
LineElement,
LinearScale,
PointElement,
TimeScale,
Tooltip
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
@Component({
selector: 'gf-benchmark-comparator',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './benchmark-comparator.component.html',
styleUrls: ['./benchmark-comparator.component.scss']
})
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Input() benchmarkDataItems: LineChartItem[] = [];
@Input() benchmark: UniqueAsset;
@Input() benchmarks: UniqueAsset[];
@Input() daysInMarket: number;
@Input() locale: string;
@Input() performanceDataItems: LineChartItem[];
@Input() user: User;
@Output() benchmarkChanged = new EventEmitter<UniqueAsset>();
@Output() dateRangeChanged = new EventEmitter<DateRange>();
@ViewChild('chartCanvas') chartCanvas;
public chart: Chart<any>;
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public isLoading = true;
public constructor() {
Chart.register(
annotationPlugin,
LinearScale,
LineController,
LineElement,
PointElement,
TimeScale,
Tooltip
);
Tooltip.positioners['top'] = (elements, position) =>
getTooltipPositionerMapTop(this.chart, position);
}
public ngOnChanges() {
if (this.performanceDataItems) {
this.initialize();
}
}
public compareUniqueAssets(
uniqueAsset1: UniqueAsset,
uniqueAsset2: UniqueAsset
) {
return (
uniqueAsset1?.dataSource === uniqueAsset2?.dataSource &&
uniqueAsset1?.symbol === uniqueAsset2?.symbol
);
}
public onChangeBenchmark(benchmark: UniqueAsset) {
this.benchmarkChanged.next(benchmark);
}
public onChangeDateRange(dateRange: DateRange) {
this.dateRangeChanged.next(dateRange);
}
public ngOnDestroy() {
this.chart?.destroy();
}
private initialize() {
this.isLoading = true;
const data = {
datasets: [
{
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2,
data: this.performanceDataItems.map(({ date, value }) => {
return { x: parseDate(date), y: value };
}),
label: $localize`Portfolio`
},
{
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderWidth: 2,
data: this.benchmarkDataItems.map(({ date, value }) => {
return { x: parseDate(date), y: value };
}),
label: $localize`Benchmark`
}
]
};
if (this.chartCanvas) {
if (this.chart) {
this.chart.data = data;
this.chart.options.plugins.tooltip = <unknown>(
this.getTooltipPluginConfiguration()
);
this.chart.update();
} else {
this.chart = new Chart(this.chartCanvas.nativeElement, {
data,
options: {
animation: false,
elements: {
line: {
tension: 0
},
point: {
hoverBackgroundColor: getBackgroundColor(),
hoverRadius: 2,
radius: 0
}
},
interaction: { intersect: false, mode: 'index' },
maintainAspectRatio: true,
plugins: <unknown>{
annotation: {
annotations: {
yAxis: {
borderColor: `rgba(${getTextColor()}, 0.1)`,
borderWidth: 1,
scaleID: 'y',
type: 'line',
value: 0
}
}
},
legend: {
display: false
},
tooltip: this.getTooltipPluginConfiguration(),
verticalHoverLine: {
color: `rgba(${getTextColor()}, 0.1)`
}
},
responsive: true,
scales: {
x: {
display: true,
grid: {
borderColor: `rgba(${getTextColor()}, 0.1)`,
borderWidth: 1,
color: `rgba(${getTextColor()}, 0.8)`,
display: false
},
type: 'time',
time: {
tooltipFormat: getDateFormatString(this.locale),
unit: 'year'
}
},
y: {
display: true,
grid: {
borderColor: `rgba(${getTextColor()}, 0.1)`,
color: `rgba(${getTextColor()}, 0.8)`,
display: false,
drawBorder: false
},
position: 'right',
ticks: {
callback: (value: number) => {
return `${value} %`;
},
display: true,
mirror: true,
z: 1
}
}
}
},
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
type: 'line'
});
}
}
this.isLoading = false;
}
private getTooltipPluginConfiguration() {
return {
...getTooltipOptions({
locale: this.locale,
unit: '%'
}),
mode: 'index',
position: <unknown>'top',
xAlign: 'center',
yAlign: 'bottom'
};
}
}

View File

@ -0,0 +1,22 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
@NgModule({
declarations: [BenchmarkComparatorComponent],
exports: [BenchmarkComparatorComponent],
imports: [
CommonModule,
FormsModule,
GfToggleModule,
MatSelectModule,
NgxSkeletonLoaderModule,
ReactiveFormsModule
]
})
export class GfBenchmarkComparatorModule {}

View File

@ -5,10 +5,6 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component'; import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
RANGE,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Position, User } from '@ghostfolio/common/interfaces'; import { Position, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +22,6 @@ import { PositionDetailDialogParams } from '../position/position-detail-dialog/i
templateUrl: './home-holdings.html' templateUrl: './home-holdings.html'
}) })
export class HomeHoldingsComponent implements OnDestroy, OnInit { export class HomeHoldingsComponent implements OnDestroy, OnInit {
public dateRange: DateRange;
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS; public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
@ -44,7 +39,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private settingsStorageService: SettingsStorageService,
private userService: UserService private userService: UserService
) { ) {
this.route.queryParams this.route.queryParams
@ -73,7 +67,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
permissions.createOrder permissions.createOrder
); );
this.changeDetectorRef.markForCheck(); this.update();
} }
}); });
} }
@ -88,18 +82,25 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.hasImpersonationId = !!aId; this.hasImpersonationId = !!aId;
}); });
this.dateRange =
this.user.settings.viewMode === 'ZEN'
? 'max'
: <DateRange>this.settingsStorageService.getSetting(RANGE) ?? 'max';
this.update(); this.update();
} }
public onChangeDateRange(aDateRange: DateRange) { public onChangeDateRange(dateRange: DateRange) {
this.dateRange = aDateRange; this.dataService
this.settingsStorageService.setSetting(RANGE, this.dateRange); .putUserSetting({ dateRange })
this.update(); .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -151,7 +152,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.positions = undefined; this.positions = undefined;
this.dataService this.dataService
.fetchPositions({ range: this.dateRange }) .fetchPositions({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe((response) => {
this.positions = response.positions; this.positions = response.positions;

View File

@ -1,7 +1,7 @@
<div class="container justify-content-center p-3"> <div class="container justify-content-center p-3">
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center"> <div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
<gf-toggle <gf-toggle
[defaultValue]="dateRange" [defaultValue]="user?.settings?.dateRange"
[isLoading]="positions === undefined" [isLoading]="positions === undefined"
[options]="dateRangeOptions" [options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)" (change)="onChangeDateRange($event.value)"
@ -17,7 +17,7 @@
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [positions]="positions"
[range]="dateRange" [range]="user?.settings?.dateRange"
></gf-positions> ></gf-positions>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View File

@ -2,19 +2,15 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component'; import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
RANGE,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
LineChartItem,
PortfolioPerformance, PortfolioPerformance,
UniqueAsset, UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -25,7 +21,6 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './home-overview.html' templateUrl: './home-overview.html'
}) })
export class HomeOverviewComponent implements OnDestroy, OnInit { export class HomeOverviewComponent implements OnDestroy, OnInit {
public dateRange: DateRange;
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS; public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string; public deviceType: string;
public errors: UniqueAsset[]; public errors: UniqueAsset[];
@ -47,7 +42,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private settingsStorageService: SettingsStorageService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -61,7 +55,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
permissions.createOrder permissions.createOrder
); );
this.changeDetectorRef.markForCheck(); this.update();
} }
}); });
} }
@ -78,11 +72,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.dateRange =
this.user.settings.viewMode === 'ZEN'
? 'max'
: <DateRange>this.settingsStorageService.getSetting(RANGE) ?? 'max';
this.showDetails = this.showDetails =
!this.hasImpersonationId && !this.hasImpersonationId &&
!this.user.settings.isRestrictedView && !this.user.settings.isRestrictedView &&
@ -91,10 +80,22 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.update(); this.update();
} }
public onChangeDateRange(aDateRange: DateRange) { public onChangeDateRange(dateRange: DateRange) {
this.dateRange = aDateRange; this.dataService
this.settingsStorageService.setSetting(RANGE, this.dateRange); .putUserSetting({ dateRange })
this.update(); .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -106,7 +107,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.isLoadingPerformance = true; this.isLoadingPerformance = true;
this.dataService this.dataService
.fetchChart({ range: this.dateRange }) .fetchChart({
range: this.user?.settings?.dateRange,
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => { .subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => { this.historicalDataItems = chartData.chart.map((chartDataItem) => {
@ -122,7 +126,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
}); });
this.dataService this.dataService
.fetchPortfolioPerformance({ range: this.dateRange }) .fetchPortfolioPerformance({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe((response) => {
this.errors = response.errors; this.errors = response.errors;

View File

@ -15,7 +15,7 @@
<gf-line-chart <gf-line-chart
class="position-absolute" class="position-absolute"
symbol="Performance" symbol="Performance"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.isExperimentalFeatures ? undefined : user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems" [historicalDataItems]="historicalDataItems"
[hidden]="historicalDataItems?.length === 0" [hidden]="historicalDataItems?.length === 0"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -24,6 +24,7 @@
[showLoader]="false" [showLoader]="false"
[showXAxis]="false" [showXAxis]="false"
[showYAxis]="false" [showYAxis]="false"
[unit]="user?.settings?.isExperimentalFeatures ? '%' : undefined"
></gf-line-chart> ></gf-line-chart>
</div> </div>
</div> </div>
@ -45,7 +46,7 @@
></gf-portfolio-performance> ></gf-portfolio-performance>
<div *ngIf="showDetails" class="text-center"> <div *ngIf="showDetails" class="text-center">
<gf-toggle <gf-toggle
[defaultValue]="dateRange" [defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoadingPerformance" [isLoading]="isLoadingPerformance"
[options]="dateRangeOptions" [options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)" (change)="onChangeDateRange($event.value)"

View File

@ -38,7 +38,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
permissions.updateUserSettings permissions.updateUserSettings
); );
this.changeDetectorRef.markForCheck(); this.update();
} }
}); });
} }
@ -59,7 +59,16 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
.putUserSetting({ emergencyFund }) .putUserSetting({ emergencyFund })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => { .subscribe(() => {
this.update(); this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
}); });
} }

View File

@ -57,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
public chart: Chart; public chart: Chart;
public isLoading = true; public isLoading = true;
private data: InvestmentItem[];
public constructor() { public constructor() {
Chart.register( Chart.register(
annotationPlugin, annotationPlugin,
@ -87,10 +89,13 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
if (!this.groupBy && this.investments?.length > 0) { // Create a clone
this.data = this.investments.map((a) => Object.assign({}, a));
if (!this.groupBy && this.data?.length > 0) {
// Extend chart by 5% of days in market (before) // Extend chart by 5% of days in market (before)
const firstItem = this.investments[0]; const firstItem = this.data[0];
this.investments.unshift({ this.data.unshift({
...firstItem, ...firstItem,
date: subDays( date: subDays(
parseISO(firstItem.date), parseISO(firstItem.date),
@ -100,8 +105,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}); });
// Extend chart by 5% of days in market (after) // Extend chart by 5% of days in market (after)
const lastItem = this.investments[this.investments.length - 1]; const lastItem = this.data[this.data.length - 1];
this.investments.push({ this.data.push({
...lastItem, ...lastItem,
date: addDays( date: addDays(
parseDate(lastItem.date), parseDate(lastItem.date),
@ -111,7 +116,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
} }
const data = { const data = {
labels: this.investments.map((investmentItem) => { labels: this.data.map((investmentItem) => {
return investmentItem.date; return investmentItem.date;
}), }),
datasets: [ datasets: [
@ -119,8 +124,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: this.groupBy ? 0 : 2, borderWidth: this.groupBy ? 0 : 2,
data: this.investments.map((position) => { data: this.data.map((position) => {
return position.investment; return this.isInPercent
? position.investment * 100
: position.investment;
}), }),
label: $localize`Deposit`, label: $localize`Deposit`,
segment: { segment: {
@ -249,10 +256,11 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration() {
return { return {
...getTooltipOptions( ...getTooltipOptions({
this.isInPercent ? undefined : this.currency, currency: this.isInPercent ? undefined : this.currency,
this.isInPercent ? undefined : this.locale locale: this.isInPercent ? undefined : this.locale,
), unit: this.isInPercent ? '%' : undefined
}),
mode: 'index', mode: 'index',
position: <unknown>'top', position: <unknown>'top',
xAlign: 'center', xAlign: 'center',

View File

@ -9,9 +9,11 @@ import {
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces'; import {
EnhancedSymbolProfile,
LineChartItem
} from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';

View File

@ -7,7 +7,6 @@ import {
} from '@angular/router'; } from '@angular/router';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ViewMode } from '@prisma/client';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@ -80,13 +79,13 @@ export class AuthGuard implements CanActivate {
return; return;
} else if ( } else if (
state.url.startsWith('/home') && state.url.startsWith('/home') &&
user.settings.viewMode === ViewMode.ZEN user.settings.viewMode === 'ZEN'
) { ) {
this.router.navigate(['/zen']); this.router.navigate(['/zen']);
resolve(false); resolve(false);
return; return;
} else if (state.url.startsWith('/start')) { } else if (state.url.startsWith('/start')) {
if (user.settings.viewMode === ViewMode.ZEN) { if (user.settings.viewMode === 'ZEN') {
this.router.navigate(['/zen']); this.router.navigate(['/zen']);
} else { } else {
this.router.navigate(['/home']); this.router.navigate(['/home']);
@ -96,7 +95,7 @@ export class AuthGuard implements CanActivate {
return; return;
} else if ( } else if (
state.url.startsWith('/zen') && state.url.startsWith('/zen') &&
user.settings.viewMode === ViewMode.DEFAULT user.settings.viewMode === 'DEFAULT'
) { ) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
resolve(false); resolve(false);

View File

@ -175,29 +175,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
}); });
} }
public onChangeUserSettings(aKey: string, aValue: string) {
const settings = { ...this.user.settings, [aKey]: aValue };
this.dataService
.putUserSettings({
baseCurrency: settings?.baseCurrency,
viewMode: settings?.viewMode
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onCheckout() { public onCheckout() {
this.dataService this.dataService
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId }) .createCheckoutSession({ couponId: this.couponId, priceId: this.priceId })
@ -226,6 +203,24 @@ export class AccountPageComponent implements OnDestroy, OnInit {
}); });
} }
public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) {
this.dataService
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onRedeemCoupon() { public onRedeemCoupon() {
let couponCode = prompt($localize`Please enter your coupon code:`); let couponCode = prompt($localize`Please enter your coupon code:`);
couponCode = couponCode?.trim(); couponCode = couponCode?.trim();
@ -249,7 +244,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
) )
.subscribe(() => { .subscribe(() => {
this.snackBarRef = this.snackBar.open( this.snackBarRef = this.snackBar.open(
'✅' + $localize`Coupon code has been redeemed`, '✅ ' + $localize`Coupon code has been redeemed`,
$localize`Reload`, $localize`Reload`,
{ {
duration: 3000 duration: 3000

View File

@ -99,7 +99,7 @@
name="baseCurrency" name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.baseCurrency" [value]="user.settings.baseCurrency"
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)" (selectionChange)="onChangeUserSetting('baseCurrency', $event.value)"
> >
<mat-option <mat-option
*ngFor="let currency of currencies" *ngFor="let currency of currencies"
@ -166,7 +166,7 @@
name="viewMode" name="viewMode"
[disabled]="!hasPermissionToUpdateViewMode" [disabled]="!hasPermissionToUpdateViewMode"
[value]="user.settings.viewMode" [value]="user.settings.viewMode"
(selectionChange)="onChangeUserSettings('viewMode', $event.value)" (selectionChange)="onChangeUserSetting('viewMode', $event.value)"
> >
<mat-option value="DEFAULT">Default</mat-option> <mat-option value="DEFAULT">Default</mat-option>
<mat-option value="ZEN">Zen</mat-option> <mat-option value="ZEN">Zen</mat-option>
@ -188,6 +188,22 @@
></mat-slide-toggle> ></mat-slide-toggle>
</div> </div>
</div> </div>
<div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="align-items-center d-flex mt-4 py-1"
>
<div class="pr-1 w-50">
<div i18n>Experimental Features</div>
</div>
<div class="pl-1 w-50">
<mat-slide-toggle
color="primary"
[checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)"
></mat-slide-toggle>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1"> <div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>User ID</div> <div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 w-50">{{ user?.id }}</div> <div class="pl-1 w-50">{{ user?.id }}</div>

View File

@ -2,9 +2,14 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Position, User } from '@ghostfolio/common/interfaces'; import {
HistoricalDataItem,
Position,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { GroupBy, ToggleOption } from '@ghostfolio/common/types'; import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
@ -18,9 +23,12 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './analysis-page.html' templateUrl: './analysis-page.html'
}) })
export class AnalysisPageComponent implements OnDestroy, OnInit { export class AnalysisPageComponent implements OnDestroy, OnInit {
public benchmarkDataItems: HistoricalDataItem[] = [];
public benchmarks: UniqueAsset[];
public bottom3: Position[]; public bottom3: Position[];
public daysInMarket: number; public daysInMarket: number;
public deviceType: string; public deviceType: string;
public firstOrderDate: Date;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public investments: InvestmentItem[]; public investments: InvestmentItem[];
public investmentsByMonth: InvestmentItem[]; public investmentsByMonth: InvestmentItem[];
@ -29,6 +37,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
{ label: $localize`Monthly`, value: 'month' }, { label: $localize`Monthly`, value: 'month' },
{ label: $localize`Accumulating`, value: undefined } { label: $localize`Accumulating`, value: undefined }
]; ];
public performanceDataItems: HistoricalDataItem[];
public top3: Position[]; public top3: Position[];
public user: User; public user: User;
@ -40,7 +49,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
) {} ) {
const { benchmarks } = this.dataService.fetchInfo();
this.benchmarks = benchmarks;
}
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -52,6 +64,77 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.hasImpersonationId = !!aId; this.hasImpersonationId = !!aId;
}); });
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.update();
}
});
}
public onChangeBenchmark(benchmark: UniqueAsset) {
this.dataService
.putUserSetting({ benchmark })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onChangeDateRange(dateRange: DateRange) {
this.dataService
.putUserSetting({ dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode;
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
if (this.user.settings.isExperimentalFeatures) {
this.dataService
.fetchChart({ range: this.user?.settings?.dateRange, version: 2 })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => {
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());
this.performanceDataItems = chart;
this.updateBenchmarkDataItems();
this.changeDetectorRef.markForCheck();
});
}
this.dataService this.dataService
.fetchInvestments() .fetchInvestments()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -91,23 +174,27 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.userService.stateChanged this.changeDetectorRef.markForCheck();
.pipe(takeUntil(this.unsubscribeSubject)) }
.subscribe((state) => {
if (state?.user) { private updateBenchmarkDataItems() {
this.user = state.user; if (this.user.settings.benchmark) {
this.dataService
.fetchBenchmarkBySymbol({
...this.user.settings.benchmark,
startDate: this.firstOrderDate
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketData }) => {
this.benchmarkDataItems = marketData.map(({ date, value }) => {
return {
date,
value
};
});
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} });
}); }
}
public onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode;
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
} }
} }

View File

@ -1,53 +1,23 @@
<div class="container"> <div class="container">
<div class="row"> <h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
<div *ngIf="user?.settings?.isExperimentalFeatures" class="mb-5 row">
<div class="col-lg"> <div class="col-lg">
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3> <gf-benchmark-comparator
<div class="mb-4"> class="h-100"
<div class="align-items-center d-flex mb-4"> [benchmark]="user?.settings?.benchmark"
<div [benchmarkDataItems]="benchmarkDataItems"
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate" [benchmarks]="benchmarks"
> [daysInMarket]="daysInMarket"
<span i18n>Investment Timeline</span> [locale]="user?.settings?.locale"
<gf-premium-indicator [performanceDataItems]="performanceDataItems"
*ngIf="user?.subscription?.type === 'Basic'" [user]="user"
class="ml-1" (benchmarkChanged)="onChangeBenchmark($event)"
></gf-premium-indicator> (dateRangeChanged)="onChangeDateRange($event)"
</div> ></gf-benchmark-comparator>
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="mode"
[isLoading]="false"
[options]="modeOptions"
(change)="onChangeGroupBy($event.value)"
></gf-toggle>
</div>
<div class="chart-container">
<gf-investment-chart
class="h-100"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': mode }"
></gf-investment-chart>
<gf-investment-chart
class="h-100"
groupBy="month"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investmentsByMonth"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': !mode }"
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
></gf-investment-chart>
</div>
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="mb-5 row">
<div class="col-md-6"> <div class="col-md-6">
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-header> <mat-card-header>
@ -124,4 +94,49 @@
</mat-card> </mat-card>
</div> </div>
</div> </div>
<div class="row">
<div class="col-lg">
<div class="align-items-center d-flex mb-4">
<div
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Investment Timeline</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator>
</div>
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="mode"
[isLoading]="false"
[options]="modeOptions"
(change)="onChangeGroupBy($event.value)"
></gf-toggle>
</div>
<div class="chart-container">
<gf-investment-chart
class="h-100"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': mode }"
></gf-investment-chart>
<gf-investment-chart
class="h-100"
groupBy="month"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investmentsByMonth"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': !mode }"
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
></gf-investment-chart>
</div>
</div>
</div>
</div> </div>

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { GfBenchmarkComparatorModule } from '@ghostfolio/client/components/benchmark-comparator/benchmark-comparator.module';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
@ -15,6 +16,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
imports: [ imports: [
AnalysisPageRoutingModule, AnalysisPageRoutingModule,
CommonModule, CommonModule,
GfBenchmarkComparatorModule,
GfInvestmentChartModule, GfInvestmentChartModule,
GfPremiumIndicatorModule, GfPremiumIndicatorModule,
GfToggleModule, GfToggleModule,

View File

@ -73,7 +73,18 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.putUserSetting({ savingsRate }) .putUserSetting({ savingsRate })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
} }
public ngOnDestroy() { public ngOnDestroy() {

View File

@ -4,9 +4,8 @@ import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service'; import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { Role } from '@prisma/client'; import { Role } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';

View File

@ -12,13 +12,14 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
Access, Access,
Accounts, Accounts,
AdminData, AdminData,
AdminMarketData, AdminMarketData,
BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
Export, Export,
Filter, Filter,
@ -31,12 +32,13 @@ import {
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { AccountWithValue, DateRange } from '@ghostfolio/common/types'; import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
import { DataSource, Order as OrderModel } from '@prisma/client'; import { DataSource, Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { cloneDeep, groupBy } from 'lodash'; import { cloneDeep, groupBy } from 'lodash';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -181,12 +183,27 @@ export class DataService {
return this.http.get<Access[]>('/api/v1/access'); return this.http.get<Access[]>('/api/v1/access');
} }
public fetchBenchmarkBySymbol({
dataSource,
startDate,
symbol
}: {
startDate: Date;
} & UniqueAsset): Observable<BenchmarkMarketDataDetails> {
return this.http.get<BenchmarkMarketDataDetails>(
`/api/v1/benchmark/${dataSource}/${symbol}/${format(
startDate,
DATE_FORMAT
)}`
);
}
public fetchBenchmarks() { public fetchBenchmarks() {
return this.http.get<BenchmarkResponse>('/api/v1/benchmark'); return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
} }
public fetchChart({ range }: { range: DateRange }) { public fetchChart({ range, version }: { range: DateRange; version: number }) {
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', { return this.http.get<PortfolioChart>(`/api/v${version}/portfolio/chart`, {
params: { range } params: { range }
}); });
} }
@ -430,10 +447,6 @@ export class DataService {
return this.http.put<User>(`/api/v1/user/setting`, aData); return this.http.put<User>(`/api/v1/user/setting`, aData);
} }
public putUserSettings(aData: UpdateUserSettingsDto) {
return this.http.put<User>(`/api/v1/user/settings`, aData);
}
public redeemCoupon(couponCode: string) { public redeemCoupon(couponCode: string) {
return this.http.post('/api/v1/subscription/redeem-coupon', { return this.http.post('/api/v1/subscription/redeem-coupon', {
couponCode couponCode

View File

@ -642,7 +642,7 @@
<target state="translated">Aktuelle Marktstimmung</target> <target state="translated">Aktuelle Marktstimmung</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">12</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html"> <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
@ -1058,7 +1058,7 @@
<target state="translated">Bitte gib den Betrag deines Notfallfonds ein:</target> <target state="translated">Bitte gib den Betrag deines Notfallfonds ein:</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html"> <trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
@ -1262,7 +1262,7 @@
<target state="translated">Bitte gebe deinen Gutscheincode ein:</target> <target state="translated">Bitte gebe deinen Gutscheincode ein:</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">248</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4420880039966769543" datatype="html"> <trans-unit id="4420880039966769543" datatype="html">
@ -1270,7 +1270,7 @@
<target state="translated">Gutscheincode konnte nicht eingelöst werden</target> <target state="translated">Gutscheincode konnte nicht eingelöst werden</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">240</context> <context context-type="linenumber">258</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4819099731531004979" datatype="html"> <trans-unit id="4819099731531004979" datatype="html">
@ -1278,7 +1278,7 @@
<target state="translated">Gutscheincode wurde eingelöst</target> <target state="translated">Gutscheincode wurde eingelöst</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">252</context> <context context-type="linenumber">270</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7967484035994732534" datatype="html"> <trans-unit id="7967484035994732534" datatype="html">
@ -1286,7 +1286,7 @@
<target state="translated">Neu laden</target> <target state="translated">Neu laden</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">253</context> <context context-type="linenumber">271</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7963559562180316948" datatype="html"> <trans-unit id="7963559562180316948" datatype="html">
@ -1294,7 +1294,7 @@
<target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target> <target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">299</context> <context context-type="linenumber">317</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html"> <trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1382,7 +1382,7 @@
<target state="new">Locale</target> <target state="new">Locale</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">134</context> <context context-type="linenumber">135</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html"> <trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
@ -1390,7 +1390,7 @@
<target state="translated">Datums- und Zahlenformat</target> <target state="translated">Datums- und Zahlenformat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html"> <trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
@ -1398,7 +1398,7 @@
<target state="translated">Ansicht</target> <target state="translated">Ansicht</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">160</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html"> <trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
@ -1406,7 +1406,7 @@
<target state="translated">Einloggen mit Fingerabdruck</target> <target state="translated">Einloggen mit Fingerabdruck</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">181</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html"> <trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
@ -1414,7 +1414,7 @@
<target state="translated">Benutzer ID</target> <target state="translated">Benutzer ID</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">191</context> <context context-type="linenumber">208</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html"> <trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
@ -1422,7 +1422,7 @@
<target state="translated">Zugangsberechtigung</target> <target state="translated">Zugangsberechtigung</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">200</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html"> <trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1694,7 +1694,7 @@
<target state="translated">Analyse</target> <target state="translated">Analyse</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">2</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
@ -1706,7 +1706,7 @@
<target state="translated">Zeitstrahl der Investitionen</target> <target state="translated">Zeitstrahl der Investitionen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">102</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html"> <trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
@ -1714,7 +1714,7 @@
<target state="translated">Gewinner</target> <target state="translated">Gewinner</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html"> <trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
@ -1722,7 +1722,7 @@
<target state="translated">Verlierer</target> <target state="translated">Verlierer</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">59</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5857197365507636437" datatype="html"> <trans-unit id="5857197365507636437" datatype="html">
@ -2268,6 +2268,10 @@
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html"> <trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
<source>Beta</source> <source>Beta</source>
<target state="translated">Beta</target> <target state="translated">Beta</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">116</context>
@ -2522,7 +2526,7 @@
<target state="translated">Monatlich</target> <target state="translated">Monatlich</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1975246224413290232" datatype="html"> <trans-unit id="1975246224413290232" datatype="html">
@ -2530,15 +2534,19 @@
<target state="translated">Aufsummiert</target> <target state="translated">Aufsummiert</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5213771062241898526" datatype="html"> <trans-unit id="5213771062241898526" datatype="html">
<source>Deposit</source> <source>Deposit</source>
<target state="translated">Einlage</target> <target state="translated">Einlage</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">131</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
<context context-type="linenumber">125</context> <context context-type="linenumber">130</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
@ -2641,6 +2649,38 @@
<context context-type="linenumber">4,7</context> <context context-type="linenumber">4,7</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
<source>Experimental Features</source>
<target state="translated">Experimentelle Funktionen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context>
</context-group>
</trans-unit>
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
<source>Benchmarks</source>
<target state="translated">Benchmarks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="44fcf77e86dc038202ebad6b46d1d833d60d781b" datatype="html">
<source>Compare with...</source>
<target state="translated">Vergleichen mit...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
<trans-unit id="1931353503905413384" datatype="html">
<source>Benchmark</source>
<target state="translated">Benchmark</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -583,7 +583,7 @@
<source>Current Market Mood</source> <source>Current Market Mood</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">12</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html"> <trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
@ -957,7 +957,7 @@
<source>Please enter the amount of your emergency fund:</source> <source>Please enter the amount of your emergency fund:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html"> <trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
@ -1137,35 +1137,35 @@
<source>Please enter your coupon code:</source> <source>Please enter your coupon code:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">248</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4420880039966769543" datatype="html"> <trans-unit id="4420880039966769543" datatype="html">
<source>Could not redeem coupon code</source> <source>Could not redeem coupon code</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">240</context> <context context-type="linenumber">258</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4819099731531004979" datatype="html"> <trans-unit id="4819099731531004979" datatype="html">
<source>Coupon code has been redeemed</source> <source>Coupon code has been redeemed</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">252</context> <context context-type="linenumber">270</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7967484035994732534" datatype="html"> <trans-unit id="7967484035994732534" datatype="html">
<source>Reload</source> <source>Reload</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">253</context> <context context-type="linenumber">271</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7963559562180316948" datatype="html"> <trans-unit id="7963559562180316948" datatype="html">
<source>Do you really want to remove this sign in method?</source> <source>Do you really want to remove this sign in method?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">299</context> <context context-type="linenumber">317</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html"> <trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1243,42 +1243,42 @@
<source>Locale</source> <source>Locale</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">134</context> <context context-type="linenumber">135</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html"> <trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
<source>Date and number format</source> <source>Date and number format</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html"> <trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
<source>View Mode</source> <source>View Mode</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">160</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html"> <trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
<source>Sign in with fingerprint</source> <source>Sign in with fingerprint</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">181</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html"> <trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
<source>User ID</source> <source>User ID</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">191</context> <context context-type="linenumber">208</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html"> <trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
<source>Granted Access</source> <source>Granted Access</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">200</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html"> <trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1520,7 +1520,7 @@
<source>Analysis</source> <source>Analysis</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">2</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
@ -1531,21 +1531,21 @@
<source>Investment Timeline</source> <source>Investment Timeline</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">102</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html"> <trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
<source>Top</source> <source>Top</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html"> <trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
<source>Bottom</source> <source>Bottom</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">59</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5857197365507636437" datatype="html"> <trans-unit id="5857197365507636437" datatype="html">
@ -2027,6 +2027,10 @@
</trans-unit> </trans-unit>
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html"> <trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
<source>Beta</source> <source>Beta</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">116</context>
@ -2246,7 +2250,7 @@
<source>Accumulating</source> <source>Accumulating</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2937311350146031865" datatype="html"> <trans-unit id="2937311350146031865" datatype="html">
@ -2265,9 +2269,13 @@
</trans-unit> </trans-unit>
<trans-unit id="5213771062241898526" datatype="html"> <trans-unit id="5213771062241898526" datatype="html">
<source>Deposit</source> <source>Deposit</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">131</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
<context context-type="linenumber">125</context> <context context-type="linenumber">130</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
@ -2285,7 +2293,7 @@
<source>Monthly</source> <source>Monthly</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html"> <trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html">
@ -2359,6 +2367,34 @@
<context context-type="linenumber">6</context> <context context-type="linenumber">6</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
<source>Experimental Features</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context>
</context-group>
</trans-unit>
<trans-unit id="1931353503905413384" datatype="html">
<source>Benchmark</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
<source>Benchmarks</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="44fcf77e86dc038202ebad6b46d1d833d60d781b" datatype="html">
<source>Compare with...</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -2,7 +2,15 @@ import { Chart, TooltipPosition } from 'chart.js';
import { getBackgroundColor, getTextColor } from './helper'; import { getBackgroundColor, getTextColor } from './helper';
export function getTooltipOptions(currency = '', locale = '') { export function getTooltipOptions({
currency = '',
locale = '',
unit = ''
}: {
currency?: string;
locale?: string;
unit?: string;
} = {}) {
return { return {
backgroundColor: getBackgroundColor(), backgroundColor: getBackgroundColor(),
bodyColor: `rgb(${getTextColor()})`, bodyColor: `rgb(${getTextColor()})`,
@ -20,6 +28,8 @@ export function getTooltipOptions(currency = '', locale = '') {
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2 minimumFractionDigits: 2
})} ${currency}`; })} ${currency}`;
} else if (unit) {
label += `${context.parsed.y.toFixed(2)} ${unit}`;
} else { } else {
label += context.parsed.y.toFixed(2); label += context.parsed.y.toFixed(2);
} }

View File

@ -42,7 +42,11 @@ export function downloadAsFile({
} }
export function encodeDataSource(aDataSource: DataSource) { export function encodeDataSource(aDataSource: DataSource) {
return Buffer.from(aDataSource, 'utf-8').toString('hex'); if (aDataSource) {
return Buffer.from(aDataSource, 'utf-8').toString('hex');
}
return undefined;
} }
export function extractNumberFromString(aString: string): number { export function extractNumberFromString(aString: string): number {

View File

@ -0,0 +1,5 @@
import { LineChartItem } from './line-chart-item.interface';
export interface BenchmarkMarketDataDetails {
marketData: LineChartItem[];
}

View File

@ -7,6 +7,7 @@ import {
AdminMarketData, AdminMarketData,
AdminMarketDataItem AdminMarketDataItem
} from './admin-market-data.interface'; } from './admin-market-data.interface';
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import { Benchmark } from './benchmark.interface'; import { Benchmark } from './benchmark.interface';
import { Coupon } from './coupon.interface'; import { Coupon } from './coupon.interface';
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
@ -15,6 +16,7 @@ import { FilterGroup } from './filter-group.interface';
import { Filter } from './filter.interface'; import { Filter } from './filter.interface';
import { HistoricalDataItem } from './historical-data-item.interface'; import { HistoricalDataItem } from './historical-data-item.interface';
import { InfoItem } from './info-item.interface'; import { InfoItem } from './info-item.interface';
import { LineChartItem } from './line-chart-item.interface';
import { PortfolioChart } from './portfolio-chart.interface'; import { PortfolioChart } from './portfolio-chart.interface';
import { PortfolioDetails } from './portfolio-details.interface'; import { PortfolioDetails } from './portfolio-details.interface';
import { PortfolioInvestments } from './portfolio-investments.interface'; import { PortfolioInvestments } from './portfolio-investments.interface';
@ -47,6 +49,7 @@ export {
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem, AdminMarketDataItem,
Benchmark, Benchmark,
BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
Coupon, Coupon,
EnhancedSymbolProfile, EnhancedSymbolProfile,
@ -55,6 +58,7 @@ export {
FilterGroup, FilterGroup,
HistoricalDataItem, HistoricalDataItem,
InfoItem, InfoItem,
LineChartItem,
OAuthResponse, OAuthResponse,
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,

View File

@ -2,9 +2,11 @@ import { Tag } from '@prisma/client';
import { Statistics } from './statistics.interface'; import { Statistics } from './statistics.interface';
import { Subscription } from './subscription.interface'; import { Subscription } from './subscription.interface';
import { UniqueAsset } from './unique-asset.interface';
export interface InfoItem { export interface InfoItem {
baseCurrency: string; baseCurrency: string;
benchmarks: UniqueAsset[];
currencies: string[]; currencies: string[];
demoAuthToken: string; demoAuthToken: string;
fearAndGreedDataSource?: string; fearAndGreedDataSource?: string;

View File

@ -1,9 +1,15 @@
import { ViewMode } from '@prisma/client'; import { DateRange, ViewMode } from '@ghostfolio/common/types';
import { UniqueAsset } from './unique-asset.interface';
export interface UserSettings { export interface UserSettings {
baseCurrency?: string; baseCurrency?: string;
benchmark?: UniqueAsset;
dateRange?: DateRange;
emergencyFund?: number;
isExperimentalFeatures?: boolean;
isRestrictedView?: boolean; isRestrictedView?: boolean;
language?: string; language?: string;
locale: string; locale?: string;
viewMode?: ViewMode; viewMode?: ViewMode;
} }

View File

@ -1,10 +1,11 @@
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Account, Settings, User } from '@prisma/client'; import { Account, Settings, User } from '@prisma/client';
import { UserSettings } from './user-settings.interface';
export type UserWithSettings = User & { export type UserWithSettings = User & {
Account: Account[]; Account: Account[];
permissions?: string[]; permissions?: string[];
Settings: Settings; Settings: Settings & { settings: UserSettings };
subscription?: { subscription?: {
expiresAt?: Date; expiresAt?: Date;
type: SubscriptionType; type: SubscriptionType;

View File

@ -8,6 +8,7 @@ import { Market } from './market.type';
import type { OrderWithAccount } from './order-with-account.type'; import type { OrderWithAccount } from './order-with-account.type';
import type { RequestWithUser } from './request-with-user.type'; import type { RequestWithUser } from './request-with-user.type';
import { ToggleOption } from './toggle-option.type'; import { ToggleOption } from './toggle-option.type';
import type { ViewMode } from './view-mode.type';
export type { export type {
AccessWithGranteeUser, AccessWithGranteeUser,
@ -19,5 +20,6 @@ export type {
MarketState, MarketState,
OrderWithAccount, OrderWithAccount,
RequestWithUser, RequestWithUser,
ToggleOption ToggleOption,
ViewMode
}; };

View File

@ -0,0 +1 @@
export type ViewMode = 'DEFAULT' | 'ZEN';

View File

@ -25,6 +25,7 @@ import {
getDateFormatString, getDateFormatString,
getTextColor getTextColor
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/common/interfaces';
import { import {
Chart, Chart,
Filler, Filler,
@ -36,8 +37,6 @@ import {
Tooltip Tooltip
} from 'chart.js'; } from 'chart.js';
import { LineChartItem } from './interfaces/line-chart.interface';
@Component({ @Component({
selector: 'gf-line-chart', selector: 'gf-line-chart',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -56,6 +55,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() showXAxis = false; @Input() showXAxis = false;
@Input() showYAxis = false; @Input() showYAxis = false;
@Input() symbol: string; @Input() symbol: string;
@Input() unit: string;
@Input() yMax: number; @Input() yMax: number;
@Input() yMaxLabel: string; @Input() yMaxLabel: string;
@Input() yMin: number; @Input() yMin: number;
@ -259,7 +259,11 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration() {
return { return {
...getTooltipOptions(this.currency, this.locale), ...getTooltipOptions({
currency: this.currency,
locale: this.locale,
unit: this.unit
}),
mode: 'index', mode: 'index',
position: <unknown>'top', position: <unknown>'top',
xAlign: 'center', xAlign: 'center',

View File

@ -349,7 +349,10 @@ export class PortfolioProportionChartComponent
private getTooltipPluginConfiguration(data: ChartConfiguration['data']) { private getTooltipPluginConfiguration(data: ChartConfiguration['data']) {
return { return {
...getTooltipOptions(this.baseCurrency, this.locale), ...getTooltipOptions({
currency: this.baseCurrency,
locale: this.locale
}),
callbacks: { callbacks: {
label: (context) => { label: (context) => {
const labelIndex = const labelIndex =

View File

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

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Settings" DROP COLUMN "currency",
DROP COLUMN "viewMode";

View File

@ -102,10 +102,8 @@ model Property {
} }
model Settings { model Settings {
currency String?
settings Json? settings Json?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
viewMode ViewMode?
userId String @id userId String @id
User User @relation(fields: [userId], references: [id]) User User @relation(fields: [userId], references: [id])
} }