Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
4f281d25e1 | |||
aaba8c35c2 | |||
7d27cb3398 | |||
91678028b5 | |||
5e3cac8ac9 | |||
33f20b6b48 | |||
e4fd255dd7 | |||
e320aa91f7 | |||
0fcfa6c1bd | |||
42d32ed652 | |||
21b4b0ef24 | |||
4f8fe83a16 | |||
980ad1028c | |||
0d5bc3f51b | |||
aece76d98f | |||
fc4bb71184 | |||
20bc7ef99c | |||
7a733ae49b | |||
376ce88492 | |||
c4d83aabe7 | |||
d4e2cec77e | |||
75db7bf79a | |||
3ad99c9991 | |||
00e402d286 | |||
4ac0484025 | |||
75d61bff6d | |||
0de28d733e | |||
3b2f13850c | |||
0cc42ffd7c | |||
3ccb812ac3 |
75
CHANGELOG.md
75
CHANGELOG.md
@ -5,7 +5,80 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.186.1 - 03.09.2022
|
||||
## 1.193.0 - 14.09.2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Sorted the benchmarks by name
|
||||
- Extended the pricing page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the calculations of the exchange rate service by changing `USD` to the base currency
|
||||
- Fixed the missing assets during the local development
|
||||
|
||||
## 1.192.0 - 11.09.2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Simplified the configuration of the benchmarks: `symbolProfileId` instead of `dataSource` and `symbol`
|
||||
- Upgraded `yahoo-finance2` from version `2.3.3` to `2.3.6`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the loading indicator of the benchmark comparator
|
||||
- Improved the error handling in the benchmark calculation
|
||||
|
||||
## 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
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -153,6 +153,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
|
||||
### Setup
|
||||
|
||||
1. Run `yarn install`
|
||||
1. Run `yarn build:dev` to build the source code including the assets
|
||||
1. Run `docker-compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
||||
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
||||
1. Start the server and the client (see [_Development_](#Development))
|
||||
|
@ -1,7 +1,18 @@
|
||||
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 { BenchmarkResponse } from '@ghostfolio/common/interfaces';
|
||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||
import {
|
||||
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';
|
||||
|
||||
@ -17,4 +28,21 @@ export class BenchmarkController {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
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 { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||
@ -18,6 +19,7 @@ import { BenchmarkService } from './benchmark.service';
|
||||
MarketDataModule,
|
||||
PropertyModule,
|
||||
RedisCacheModule,
|
||||
SymbolModule,
|
||||
SymbolProfileModule
|
||||
],
|
||||
providers: [BenchmarkService]
|
||||
|
15
apps/api/src/app/benchmark/benchmark.service.spec.ts
Normal file
15
apps/api/src/app/benchmark/benchmark.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -1,13 +1,22 @@
|
||||
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 { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
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 { SymbolProfile } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { format } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class BenchmarkService {
|
||||
@ -18,9 +27,18 @@ export class BenchmarkService {
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
private readonly symbolProfileService: SymbolProfileService,
|
||||
private readonly symbolService: SymbolService
|
||||
) {}
|
||||
|
||||
public calculateChangeInPercentage(baseValue: number, currentValue: number) {
|
||||
if (baseValue && currentValue) {
|
||||
return new Big(currentValue).div(baseValue).minus(1).toNumber();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async getBenchmarks({ useCache = true } = {}): Promise<
|
||||
BenchmarkResponse['benchmarks']
|
||||
> {
|
||||
@ -38,47 +56,43 @@ export class BenchmarkService {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const benchmarkAssets: UniqueAsset[] =
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as UniqueAsset[]) ?? [];
|
||||
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles();
|
||||
|
||||
const promises: Promise<number>[] = [];
|
||||
|
||||
const [quotes, assetProfiles] = await Promise.all([
|
||||
this.dataProviderService.getQuotes(benchmarkAssets),
|
||||
this.symbolProfileService.getSymbolProfiles(benchmarkAssets)
|
||||
]);
|
||||
const quotes = await this.dataProviderService.getQuotes(
|
||||
benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
||||
return { dataSource, symbol };
|
||||
})
|
||||
);
|
||||
|
||||
for (const benchmarkAsset of benchmarkAssets) {
|
||||
promises.push(this.marketDataService.getMax(benchmarkAsset));
|
||||
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
||||
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
|
||||
}
|
||||
|
||||
const allTimeHighs = await Promise.all(promises);
|
||||
|
||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
||||
const { marketPrice } =
|
||||
quotes[benchmarkAssetProfiles[index].symbol] ?? {};
|
||||
|
||||
let performancePercentFromAllTimeHigh = new Big(0);
|
||||
let performancePercentFromAllTimeHigh = 0;
|
||||
|
||||
if (allTimeHigh) {
|
||||
performancePercentFromAllTimeHigh = new Big(marketPrice)
|
||||
.div(allTimeHigh)
|
||||
.minus(1);
|
||||
if (allTimeHigh && marketPrice) {
|
||||
performancePercentFromAllTimeHigh = this.calculateChangeInPercentage(
|
||||
allTimeHigh,
|
||||
marketPrice
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
marketCondition: this.getMarketCondition(
|
||||
performancePercentFromAllTimeHigh
|
||||
),
|
||||
name: assetProfiles.find(({ dataSource, symbol }) => {
|
||||
return (
|
||||
dataSource === benchmarkAssets[index].dataSource &&
|
||||
symbol === benchmarkAssets[index].symbol
|
||||
);
|
||||
})?.name,
|
||||
name: benchmarkAssetProfiles[index].name,
|
||||
performances: {
|
||||
allTimeHigh: {
|
||||
performancePercent: performancePercentFromAllTimeHigh.toNumber()
|
||||
performancePercent: performancePercentFromAllTimeHigh
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -93,7 +107,81 @@ export class BenchmarkService {
|
||||
return benchmarks;
|
||||
}
|
||||
|
||||
private getMarketCondition(aPerformanceInPercent: Big) {
|
||||
return aPerformanceInPercent.lte(-0.2) ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
||||
const symbolProfileIds: string[] = (
|
||||
((await this.propertyService.getByKey(PROPERTY_BENCHMARKS)) as {
|
||||
symbolProfileId: string;
|
||||
}[]) ?? []
|
||||
).map(({ symbolProfileId }) => {
|
||||
return symbolProfileId;
|
||||
});
|
||||
|
||||
const assetProfiles =
|
||||
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);
|
||||
|
||||
return assetProfiles
|
||||
.map(({ dataSource, id, name, symbol }) => {
|
||||
return {
|
||||
dataSource,
|
||||
id,
|
||||
name,
|
||||
symbol
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
@ -4,22 +4,36 @@ import * as path from 'path';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class FrontendMiddleware implements NestMiddleware {
|
||||
public indexHtmlDe = fs.readFileSync(
|
||||
this.getPathOfIndexHtmlFile('de'),
|
||||
'utf8'
|
||||
);
|
||||
public indexHtmlEn = fs.readFileSync(
|
||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
||||
'utf8'
|
||||
);
|
||||
public indexHtmlDe = '';
|
||||
public indexHtmlEn = '';
|
||||
public isProduction: boolean;
|
||||
|
||||
public constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {}
|
||||
) {
|
||||
const NODE_ENV =
|
||||
this.configService.get<'development' | 'production'>('NODE_ENV') ??
|
||||
'development';
|
||||
|
||||
this.isProduction = NODE_ENV === 'production';
|
||||
|
||||
try {
|
||||
this.indexHtmlDe = fs.readFileSync(
|
||||
this.getPathOfIndexHtmlFile('de'),
|
||||
'utf8'
|
||||
);
|
||||
this.indexHtmlEn = fs.readFileSync(
|
||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
||||
'utf8'
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
public use(req: Request, res: Response, next: NextFunction) {
|
||||
let featureGraphicPath = 'assets/cover.png';
|
||||
@ -31,7 +45,11 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
|
||||
}
|
||||
|
||||
if (req.path.startsWith('/api/') || this.isFileRequest(req.url)) {
|
||||
if (
|
||||
req.path.startsWith('/api/') ||
|
||||
this.isFileRequest(req.url) ||
|
||||
!this.isProduction
|
||||
) {
|
||||
// Skip
|
||||
next();
|
||||
} else if (req.path === '/de' || req.path.startsWith('/de/')) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||
|
||||
import { InfoService } from './info.service';
|
||||
|
||||
@ -8,6 +9,7 @@ export class InfoController {
|
||||
public constructor(private readonly infoService: InfoService) {}
|
||||
|
||||
@Get()
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getInfo(): Promise<InfoItem> {
|
||||
return this.infoService.get();
|
||||
}
|
||||
|
@ -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 { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||
@ -16,6 +17,7 @@ import { InfoService } from './info.service';
|
||||
@Module({
|
||||
controllers: [InfoController],
|
||||
imports: [
|
||||
BenchmarkModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
|
@ -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 { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
@ -31,6 +32,7 @@ export class InfoService {
|
||||
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
||||
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly jwtService: JwtService,
|
||||
@ -108,6 +110,7 @@ export class InfoService {
|
||||
platforms,
|
||||
systemMessage,
|
||||
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
||||
benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(),
|
||||
currencies: this.exchangeRateDataService.getCurrencies(),
|
||||
demoAuthToken: this.getDemoAuthToken(),
|
||||
statistics: await this.getStatistics(),
|
||||
|
@ -103,7 +103,7 @@ export class OrderController {
|
||||
impersonationId,
|
||||
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({
|
||||
filters,
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
isBefore,
|
||||
isSameMonth,
|
||||
isSameYear,
|
||||
isWithinInterval,
|
||||
max,
|
||||
min,
|
||||
set
|
||||
@ -167,13 +168,21 @@ export class PortfolioCalculator {
|
||||
this.transactionPoints = transactionPoints;
|
||||
}
|
||||
|
||||
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
|
||||
if (!this.transactionPoints?.length) {
|
||||
public async getCurrentPositions(
|
||||
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 {
|
||||
currentValue: new Big(0),
|
||||
hasErrors: false,
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
hasErrors: false,
|
||||
netPerformance: new Big(0),
|
||||
netPerformancePercentage: new Big(0),
|
||||
positions: [],
|
||||
@ -182,39 +191,38 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
const lastTransactionPoint =
|
||||
this.transactionPoints[this.transactionPoints.length - 1];
|
||||
|
||||
// use Date.now() to use the mock for today
|
||||
const today = new Date(Date.now());
|
||||
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
|
||||
|
||||
let firstTransactionPoint: TransactionPoint = null;
|
||||
let firstIndex = this.transactionPoints.length;
|
||||
let firstIndex = transactionPointsBeforeEndDate.length;
|
||||
const dates = [];
|
||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||
const currencies: { [symbol: string]: string } = {};
|
||||
|
||||
dates.push(resetHours(start));
|
||||
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
||||
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
||||
dataGatheringItems.push({
|
||||
dataSource: item.dataSource,
|
||||
symbol: item.symbol
|
||||
});
|
||||
currencies[item.symbol] = item.currency;
|
||||
}
|
||||
for (let i = 0; i < this.transactionPoints.length; i++) {
|
||||
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
|
||||
if (
|
||||
!isBefore(parseDate(this.transactionPoints[i].date), start) &&
|
||||
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
|
||||
firstTransactionPoint === null
|
||||
) {
|
||||
firstTransactionPoint = this.transactionPoints[i];
|
||||
firstTransactionPoint = transactionPointsBeforeEndDate[i];
|
||||
firstIndex = i;
|
||||
}
|
||||
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({
|
||||
currencies,
|
||||
@ -241,7 +249,7 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
const todayString = format(today, DATE_FORMAT);
|
||||
const endDateString = format(end, DATE_FORMAT);
|
||||
|
||||
if (firstIndex > 0) {
|
||||
firstIndex--;
|
||||
@ -254,7 +262,7 @@ export class PortfolioCalculator {
|
||||
const errors: ResponseError['errors'] = [];
|
||||
|
||||
for (const item of lastTransactionPoint.items) {
|
||||
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
||||
const marketValue = marketSymbolMap[endDateString]?.[item.symbol];
|
||||
|
||||
const {
|
||||
grossPerformance,
|
||||
@ -264,6 +272,7 @@ export class PortfolioCalculator {
|
||||
netPerformance,
|
||||
netPerformancePercentage
|
||||
} = this.getSymbolMetrics({
|
||||
end,
|
||||
marketSymbolMap,
|
||||
start,
|
||||
symbol: item.symbol
|
||||
@ -700,10 +709,12 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
private getSymbolMetrics({
|
||||
end,
|
||||
marketSymbolMap,
|
||||
start,
|
||||
symbol
|
||||
}: {
|
||||
end: Date;
|
||||
marketSymbolMap: {
|
||||
[date: string]: { [symbol: string]: Big };
|
||||
};
|
||||
@ -726,13 +737,12 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
const dateOfFirstTransaction = new Date(first(orders).date);
|
||||
const endDate = new Date(Date.now());
|
||||
|
||||
const unitPriceAtStartDate =
|
||||
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
|
||||
|
||||
const unitPriceAtEndDate =
|
||||
marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol];
|
||||
marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol];
|
||||
|
||||
if (
|
||||
!unitPriceAtEndDate ||
|
||||
@ -785,7 +795,7 @@ export class PortfolioCalculator {
|
||||
orders.push({
|
||||
symbol,
|
||||
currency: null,
|
||||
date: format(endDate, DATE_FORMAT),
|
||||
date: format(end, DATE_FORMAT),
|
||||
dataSource: null,
|
||||
fee: new Big(0),
|
||||
itemType: 'end',
|
||||
|
@ -35,11 +35,11 @@ import {
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
UseInterceptors,
|
||||
Version
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ViewMode } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
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')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@ -175,7 +195,7 @@ export class PortfolioController {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||
portfolioPosition.currency,
|
||||
this.request.user.Settings.currency
|
||||
this.request.user.Settings.settings.baseCurrency
|
||||
);
|
||||
})
|
||||
.reduce((a, b) => a + b, 0);
|
||||
@ -278,7 +298,7 @@ export class PortfolioController {
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
this.request.user.Settings.viewMode === ViewMode.ZEN ||
|
||||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
performanceInformation.performance = nullifyValuesInObject(
|
||||
@ -358,7 +378,8 @@ export class PortfolioController {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||
portfolioPosition.currency,
|
||||
this.request.user?.Settings?.currency ?? this.baseCurrency
|
||||
this.request.user?.Settings?.settings.baseCurrency ??
|
||||
this.baseCurrency
|
||||
);
|
||||
})
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
@ -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 { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.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 { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
||||
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
||||
@ -35,7 +34,8 @@ import {
|
||||
PortfolioReport,
|
||||
PortfolioSummary,
|
||||
Position,
|
||||
TimelinePosition
|
||||
TimelinePosition,
|
||||
UserSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import type {
|
||||
@ -57,6 +57,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import {
|
||||
addDays,
|
||||
differenceInDays,
|
||||
endOfToday,
|
||||
format,
|
||||
@ -71,7 +72,7 @@ import {
|
||||
subDays,
|
||||
subYears
|
||||
} from 'date-fns';
|
||||
import { isEmpty, sortBy, uniq, uniqBy } from 'lodash';
|
||||
import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash';
|
||||
|
||||
import {
|
||||
HistoricalDataContainer,
|
||||
@ -85,6 +86,7 @@ const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
||||
|
||||
@Injectable()
|
||||
export class PortfolioService {
|
||||
private static readonly MAX_CHART_ITEMS = 250;
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
@ -122,7 +124,7 @@ export class PortfolioService {
|
||||
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) => {
|
||||
let transactionCount = 0;
|
||||
@ -197,7 +199,7 @@ export class PortfolioService {
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: this.request.user.Settings.currency,
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
@ -277,7 +279,7 @@ export class PortfolioService {
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: this.request.user.Settings.currency,
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
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(
|
||||
aImpersonationId: string,
|
||||
aUserId: string,
|
||||
@ -367,8 +441,8 @@ export class PortfolioService {
|
||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||
);
|
||||
const userCurrency =
|
||||
user.Settings?.currency ??
|
||||
this.request.user?.Settings?.currency ??
|
||||
user.Settings?.settings.baseCurrency ??
|
||||
this.request.user?.Settings?.settings.baseCurrency ??
|
||||
this.baseCurrency;
|
||||
|
||||
const { orders, portfolioOrders, transactionPoints } =
|
||||
@ -528,7 +602,7 @@ export class PortfolioService {
|
||||
aImpersonationId: string,
|
||||
aSymbol: string
|
||||
): 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 orders = (
|
||||
@ -781,7 +855,7 @@ export class PortfolioService {
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: this.request.user.Settings.currency,
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
@ -857,7 +931,7 @@ export class PortfolioService {
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: this.request.user.Settings.currency,
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
@ -917,7 +991,7 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
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 { orders, portfolioOrders, transactionPoints } =
|
||||
@ -971,7 +1045,7 @@ export class PortfolioService {
|
||||
accounts
|
||||
)
|
||||
],
|
||||
{ baseCurrency: currency }
|
||||
<UserSettings>this.request.user.Settings.settings
|
||||
),
|
||||
currencyClusterRisk: await this.rulesService.evaluate(
|
||||
[
|
||||
@ -992,7 +1066,7 @@ export class PortfolioService {
|
||||
currentPositions
|
||||
)
|
||||
],
|
||||
{ baseCurrency: currency }
|
||||
<UserSettings>this.request.user.Settings.settings
|
||||
),
|
||||
fees: await this.rulesService.evaluate(
|
||||
[
|
||||
@ -1002,14 +1076,14 @@ export class PortfolioService {
|
||||
this.getFees(orders).toNumber()
|
||||
)
|
||||
],
|
||||
{ baseCurrency: currency }
|
||||
<UserSettings>this.request.user.Settings.settings
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 user = await this.userService.user({ id: userId });
|
||||
|
||||
@ -1183,7 +1257,7 @@ export class PortfolioService {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||
order.SymbolProfile.currency,
|
||||
this.request.user.Settings.currency
|
||||
this.request.user.Settings.settings.baseCurrency
|
||||
);
|
||||
})
|
||||
.reduce(
|
||||
@ -1202,7 +1276,7 @@ export class PortfolioService {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
order.fee,
|
||||
order.SymbolProfile.currency,
|
||||
this.request.user.Settings.currency
|
||||
this.request.user.Settings.settings.baseCurrency
|
||||
);
|
||||
})
|
||||
.reduce(
|
||||
@ -1224,7 +1298,7 @@ export class PortfolioService {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||
order.SymbolProfile.currency,
|
||||
this.request.user.Settings.currency
|
||||
this.request.user.Settings.settings.baseCurrency
|
||||
);
|
||||
})
|
||||
.reduce(
|
||||
@ -1265,7 +1339,7 @@ export class PortfolioService {
|
||||
portfolioOrders: PortfolioOrder[];
|
||||
}> {
|
||||
const userCurrency =
|
||||
this.request.user?.Settings?.currency ?? this.baseCurrency;
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? this.baseCurrency;
|
||||
|
||||
const orders = await this.orderService.getOrders({
|
||||
filters,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
@ -8,7 +9,7 @@ export class RulesService {
|
||||
|
||||
public async evaluate<T extends RuleSettings>(
|
||||
aRules: Rule<T>[],
|
||||
aUserSettings: { baseCurrency: string }
|
||||
aUserSettings: UserSettings
|
||||
) {
|
||||
return aRules
|
||||
.filter((rule) => {
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { ViewMode } from '@prisma/client';
|
||||
|
||||
export interface UserSettingsParams {
|
||||
currency?: string;
|
||||
userId: string;
|
||||
viewMode?: ViewMode;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export interface UserSettings {
|
||||
emergencyFund?: number;
|
||||
locale?: string;
|
||||
isRestrictedView?: boolean;
|
||||
}
|
@ -1,10 +1,33 @@
|
||||
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import type { DateRange, ViewMode } from '@ghostfolio/common/types';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateUserSettingDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
baseCurrency?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
benchmark?: string;
|
||||
|
||||
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
|
||||
@IsOptional()
|
||||
dateRange?: DateRange;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
emergencyFund?: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isExperimentalFeatures?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isRestrictedView?: boolean;
|
||||
@ -20,4 +43,8 @@ export class UpdateUserSettingDto {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
savingsRate?: number;
|
||||
|
||||
@IsIn(<ViewMode[]>['DEFAULT', 'ZEN'])
|
||||
@IsOptional()
|
||||
viewMode?: ViewMode;
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { ViewMode } from '@prisma/client';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class UpdateUserSettingsDto {
|
||||
@IsString()
|
||||
baseCurrency: string;
|
||||
|
||||
@IsString()
|
||||
viewMode: ViewMode;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
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 type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
@ -22,12 +22,10 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { User as UserModel } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import { size } from 'lodash';
|
||||
|
||||
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 { UpdateUserSettingsDto } from './update-user-settings.dto';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Controller('user')
|
||||
@ -103,6 +101,12 @@ export class UserController {
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
||||
if (
|
||||
size(data) === 1 &&
|
||||
(data.benchmark || data.dateRange) &&
|
||||
this.request.user.role === 'DEMO'
|
||||
) {
|
||||
// Allow benchmark or date range change for demo user
|
||||
} else if (
|
||||
!hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.updateUserSettings
|
||||
@ -130,33 +134,4 @@ export class UserController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -4,19 +4,20 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
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 {
|
||||
getPermissions,
|
||||
hasRole,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
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 { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||
import { UserSettings } from './interfaces/user-settings.interface';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
@Injectable()
|
||||
@ -69,9 +70,7 @@ export class UserService {
|
||||
accounts: Account,
|
||||
settings: {
|
||||
...(<UserSettings>Settings.settings),
|
||||
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
||||
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale,
|
||||
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
|
||||
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -89,7 +88,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
public isRestrictedView(aUser: UserWithSettings) {
|
||||
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false;
|
||||
return aUser.Settings.settings.isRestrictedView ?? false;
|
||||
}
|
||||
|
||||
public async user(
|
||||
@ -126,21 +125,35 @@ export class UserService {
|
||||
};
|
||||
|
||||
if (user?.Settings) {
|
||||
if (!user.Settings.currency) {
|
||||
// Set default currency if needed
|
||||
user.Settings.currency = UserService.DEFAULT_CURRENCY;
|
||||
if (!user.Settings.settings) {
|
||||
user.Settings.settings = {};
|
||||
}
|
||||
} else if (user) {
|
||||
// Set default settings if needed
|
||||
user.Settings = {
|
||||
currency: UserService.DEFAULT_CURRENCY,
|
||||
settings: null,
|
||||
settings: {},
|
||||
updatedAt: new Date(),
|
||||
userId: user?.id,
|
||||
viewMode: ViewMode.DEFAULT
|
||||
userId: user?.id
|
||||
};
|
||||
}
|
||||
|
||||
// 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')) {
|
||||
user.subscription =
|
||||
this.subscriptionService.getSubscription(Subscription);
|
||||
@ -221,7 +234,9 @@ export class UserService {
|
||||
},
|
||||
Settings: {
|
||||
create: {
|
||||
currency: this.baseCurrency
|
||||
settings: {
|
||||
currency: this.baseCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -295,7 +310,7 @@ export class UserService {
|
||||
userId: string;
|
||||
userSettings: UserSettings;
|
||||
}) {
|
||||
const settings = userSettings as Prisma.JsonObject;
|
||||
const settings = userSettings as unknown as Prisma.JsonObject;
|
||||
|
||||
await this.prismaService.settings.upsert({
|
||||
create: {
|
||||
@ -317,33 +332,6 @@ export class UserService {
|
||||
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) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const result = [];
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Injectable,
|
||||
NestInterceptor
|
||||
} from '@nestjs/common';
|
||||
import { isArray } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
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) {
|
||||
data.dataSource = encodeDataSource(data.dataSource);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
export interface UserSettings {
|
||||
baseCurrency: string;
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
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 { 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 { RuleInterface } from './interfaces/rule.interface';
|
||||
|
@ -1,9 +1,9 @@
|
||||
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 {
|
||||
PortfolioDetails,
|
||||
PortfolioPosition
|
||||
PortfolioPosition,
|
||||
UserSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,9 +1,9 @@
|
||||
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 {
|
||||
PortfolioDetails,
|
||||
PortfolioPosition
|
||||
PortfolioPosition,
|
||||
UserSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,7 +1,6 @@
|
||||
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 { PortfolioDetails } from '@ghostfolio/common/interfaces';
|
||||
import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.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 { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.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 { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.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 { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.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 { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -183,10 +183,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
for (const historicalItem of historicalResult) {
|
||||
let marketPrice = historicalItem.close;
|
||||
|
||||
if (symbol === 'USDGBp') {
|
||||
if (symbol === `${this.baseCurrency}GBp`) {
|
||||
// Convert GPB to GBp (pence)
|
||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||
} else if (symbol === 'USDILA') {
|
||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||
// Convert ILS to ILA
|
||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||
}
|
||||
@ -246,9 +246,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
marketPrice: quote.regularMarketPrice || 0
|
||||
};
|
||||
|
||||
if (symbol === 'USDGBP' && yahooFinanceSymbols.includes('USDGBp=X')) {
|
||||
if (
|
||||
symbol === `${this.baseCurrency}GBP` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}GBp=X`)
|
||||
) {
|
||||
// Convert GPB to GBp (pence)
|
||||
response['USDGBp'] = {
|
||||
response[`${this.baseCurrency}GBp`] = {
|
||||
...response[symbol],
|
||||
currency: 'GBp',
|
||||
marketPrice: new Big(response[symbol].marketPrice)
|
||||
@ -256,11 +259,11 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
.toNumber()
|
||||
};
|
||||
} else if (
|
||||
symbol === 'USDILS' &&
|
||||
yahooFinanceSymbols.includes('USDILA=X')
|
||||
symbol === `${this.baseCurrency}ILS` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}ILA=X`)
|
||||
) {
|
||||
// Convert ILS to ILA
|
||||
response['USDILA'] = {
|
||||
response[`${this.baseCurrency}ILA`] = {
|
||||
...response[symbol],
|
||||
currency: 'ILA',
|
||||
marketPrice: new Big(response[symbol].marketPrice)
|
||||
@ -270,9 +273,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (yahooFinanceSymbols.includes('USDUSX=X')) {
|
||||
if (yahooFinanceSymbols.includes(`${this.baseCurrency}USX=X`)) {
|
||||
// Convert USD to USX (cent)
|
||||
response['USDUSX'] = {
|
||||
response[`${this.baseCurrency}USX`] = {
|
||||
currency: 'USX',
|
||||
dataSource: this.getName(),
|
||||
marketPrice: new Big(1).mul(100).toNumber(),
|
||||
|
@ -99,10 +99,12 @@ export class ExchangeRateDataService {
|
||||
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
||||
|
||||
if (!this.exchangeRates[symbol]) {
|
||||
// Not found, calculate indirectly via USD
|
||||
// Not found, calculate indirectly via base currency
|
||||
this.exchangeRates[symbol] =
|
||||
resultExtended[`${currency1}${'USD'}`]?.[date]?.marketPrice *
|
||||
resultExtended[`${'USD'}${currency2}`]?.[date]?.marketPrice;
|
||||
resultExtended[`${currency1}${this.baseCurrency}`]?.[date]
|
||||
?.marketPrice *
|
||||
resultExtended[`${this.baseCurrency}${currency2}`]?.[date]
|
||||
?.marketPrice;
|
||||
|
||||
// Calculate the opposite direction
|
||||
this.exchangeRates[`${currency2}${currency1}`] =
|
||||
@ -126,9 +128,11 @@ export class ExchangeRateDataService {
|
||||
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
||||
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
||||
} else {
|
||||
// Calculate indirectly via USD
|
||||
const factor1 = this.exchangeRates[`${aFromCurrency}${'USD'}`];
|
||||
const factor2 = this.exchangeRates[`${'USD'}${aToCurrency}`];
|
||||
// Calculate indirectly via base currency
|
||||
const factor1 =
|
||||
this.exchangeRates[`${aFromCurrency}${this.baseCurrency}`];
|
||||
const factor2 =
|
||||
this.exchangeRates[`${this.baseCurrency}${aToCurrency}`];
|
||||
|
||||
factor = factor1 * factor2;
|
||||
|
||||
@ -166,21 +170,6 @@ export class ExchangeRateDataService {
|
||||
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({
|
||||
distinct: ['currency'],
|
||||
|
@ -64,6 +64,23 @@ export class SymbolProfileService {
|
||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||
}
|
||||
|
||||
public async getSymbolProfilesByIds(
|
||||
symbolProfileIds: string[]
|
||||
): Promise<EnhancedSymbolProfile[]> {
|
||||
return this.prismaService.symbolProfile
|
||||
.findMany({
|
||||
include: { SymbolProfileOverrides: true },
|
||||
where: {
|
||||
id: {
|
||||
in: symbolProfileIds.map((symbolProfileId) => {
|
||||
return symbolProfileId;
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.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 { Module } from '@nestjs/common';
|
||||
|
||||
|
@ -14,8 +14,7 @@ import {
|
||||
getDateFormatString,
|
||||
getLocale
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||
import { LineChartItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import {
|
||||
addDays,
|
||||
|
@ -92,7 +92,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
if (
|
||||
params['assetProfileDialog'] &&
|
||||
params['dataSource'] &&
|
||||
params['dateOfFirstActivity'] &&
|
||||
params['symbol']
|
||||
) {
|
||||
this.openAssetProfileDialog({
|
||||
@ -170,12 +169,16 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
dateOfFirstActivity,
|
||||
symbol
|
||||
}: UniqueAsset & { dateOfFirstActivity: string }) {
|
||||
try {
|
||||
dateOfFirstActivity = format(parseISO(dateOfFirstActivity), DATE_FORMAT);
|
||||
} catch {}
|
||||
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
dateOfFirstActivity,
|
||||
dataSource,
|
||||
symbol,
|
||||
assetProfileDialog: true,
|
||||
dateOfFirstActivity: format(parseISO(dateOfFirstActivity), DATE_FORMAT)
|
||||
assetProfileDialog: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
||||
declarations: [AdminOverviewComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
|
@ -0,0 +1,51 @@
|
||||
<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"
|
||||
[value]="benchmark"
|
||||
(selectionChange)="onChangeBenchmark($event.value)"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let symbolProfile of benchmarks"
|
||||
[value]="symbolProfile.id"
|
||||
>{{ symbolProfile.name }}</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>
|
@ -0,0 +1,11 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
ngx-skeleton-loader {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
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';
|
||||
import { SymbolProfile } from '@prisma/client';
|
||||
|
||||
@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: string;
|
||||
@Input() benchmarks: Partial<SymbolProfile>[];
|
||||
@Input() daysInMarket: number;
|
||||
@Input() isLoading: boolean;
|
||||
@Input() locale: string;
|
||||
@Input() performanceDataItems: LineChartItem[];
|
||||
@Input() user: User;
|
||||
|
||||
@Output() benchmarkChanged = new EventEmitter<string>();
|
||||
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public chart: Chart<any>;
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
|
||||
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 onChangeBenchmark(symbolProfileId: string) {
|
||||
this.benchmarkChanged.next(symbolProfileId);
|
||||
}
|
||||
|
||||
public onChangeDateRange(dateRange: DateRange) {
|
||||
this.dateRangeChanged.next(dateRange);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.chart?.destroy();
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
return {
|
||||
...getTooltipOptions({
|
||||
locale: this.locale,
|
||||
unit: '%'
|
||||
}),
|
||||
mode: 'index',
|
||||
position: <unknown>'top',
|
||||
xAlign: 'center',
|
||||
yAlign: 'bottom'
|
||||
};
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -5,10 +5,6 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos
|
||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.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 { Position, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
@ -26,7 +22,6 @@ import { PositionDetailDialogParams } from '../position/position-detail-dialog/i
|
||||
templateUrl: './home-holdings.html'
|
||||
})
|
||||
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
public dateRange: DateRange;
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
@ -44,7 +39,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.route.queryParams
|
||||
@ -73,7 +67,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -88,18 +82,25 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
this.hasImpersonationId = !!aId;
|
||||
});
|
||||
|
||||
this.dateRange =
|
||||
this.user.settings.viewMode === 'ZEN'
|
||||
? 'max'
|
||||
: <DateRange>this.settingsStorageService.getSetting(RANGE) ?? 'max';
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
public onChangeDateRange(aDateRange: DateRange) {
|
||||
this.dateRange = aDateRange;
|
||||
this.settingsStorageService.setSetting(RANGE, this.dateRange);
|
||||
this.update();
|
||||
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 ngOnDestroy() {
|
||||
@ -151,7 +152,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
this.positions = undefined;
|
||||
|
||||
this.dataService
|
||||
.fetchPositions({ range: this.dateRange })
|
||||
.fetchPositions({ range: this.user?.settings?.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.positions = response.positions;
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container justify-content-center p-3">
|
||||
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
|
||||
<gf-toggle
|
||||
[defaultValue]="dateRange"
|
||||
[defaultValue]="user?.settings?.dateRange"
|
||||
[isLoading]="positions === undefined"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
@ -17,7 +17,7 @@
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="dateRange"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-positions>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
@ -2,19 +2,15 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.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 {
|
||||
LineChartItem,
|
||||
PortfolioPerformance,
|
||||
UniqueAsset,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -25,7 +21,6 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './home-overview.html'
|
||||
})
|
||||
export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
public dateRange: DateRange;
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public deviceType: string;
|
||||
public errors: UniqueAsset[];
|
||||
@ -47,7 +42,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.userService.stateChanged
|
||||
@ -61,7 +55,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -78,11 +72,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dateRange =
|
||||
this.user.settings.viewMode === 'ZEN'
|
||||
? 'max'
|
||||
: <DateRange>this.settingsStorageService.getSetting(RANGE) ?? 'max';
|
||||
|
||||
this.showDetails =
|
||||
!this.hasImpersonationId &&
|
||||
!this.user.settings.isRestrictedView &&
|
||||
@ -91,10 +80,22 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
this.update();
|
||||
}
|
||||
|
||||
public onChangeDateRange(aDateRange: DateRange) {
|
||||
this.dateRange = aDateRange;
|
||||
this.settingsStorageService.setSetting(RANGE, this.dateRange);
|
||||
this.update();
|
||||
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 ngOnDestroy() {
|
||||
@ -106,7 +107,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
this.isLoadingPerformance = true;
|
||||
|
||||
this.dataService
|
||||
.fetchChart({ range: this.dateRange })
|
||||
.fetchChart({
|
||||
range: this.user?.settings?.dateRange,
|
||||
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((chartData) => {
|
||||
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
||||
@ -122,7 +126,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||
.fetchPortfolioPerformance({ range: this.user?.settings?.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.errors = response.errors;
|
||||
|
@ -15,7 +15,7 @@
|
||||
<gf-line-chart
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[currency]="user?.settings?.isExperimentalFeatures ? undefined : user?.settings?.baseCurrency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[hidden]="historicalDataItems?.length === 0"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -24,6 +24,7 @@
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
[unit]="user?.settings?.isExperimentalFeatures ? '%' : undefined"
|
||||
></gf-line-chart>
|
||||
</div>
|
||||
</div>
|
||||
@ -45,7 +46,7 @@
|
||||
></gf-portfolio-performance>
|
||||
<div *ngIf="showDetails" class="text-center">
|
||||
<gf-toggle
|
||||
[defaultValue]="dateRange"
|
||||
[defaultValue]="user?.settings?.dateRange"
|
||||
[isLoading]="isLoadingPerformance"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
|
@ -38,7 +38,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
permissions.updateUserSettings
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -59,7 +59,16 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
.putUserSetting({ emergencyFund })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.update();
|
||||
this.userService.remove();
|
||||
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -57,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
public chart: Chart;
|
||||
public isLoading = true;
|
||||
|
||||
private data: InvestmentItem[];
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
annotationPlugin,
|
||||
@ -87,10 +89,13 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
private initialize() {
|
||||
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)
|
||||
const firstItem = this.investments[0];
|
||||
this.investments.unshift({
|
||||
const firstItem = this.data[0];
|
||||
this.data.unshift({
|
||||
...firstItem,
|
||||
date: subDays(
|
||||
parseISO(firstItem.date),
|
||||
@ -100,8 +105,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
});
|
||||
|
||||
// Extend chart by 5% of days in market (after)
|
||||
const lastItem = this.investments[this.investments.length - 1];
|
||||
this.investments.push({
|
||||
const lastItem = this.data[this.data.length - 1];
|
||||
this.data.push({
|
||||
...lastItem,
|
||||
date: addDays(
|
||||
parseDate(lastItem.date),
|
||||
@ -111,7 +116,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: this.investments.map((investmentItem) => {
|
||||
labels: this.data.map((investmentItem) => {
|
||||
return investmentItem.date;
|
||||
}),
|
||||
datasets: [
|
||||
@ -119,8 +124,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderWidth: this.groupBy ? 0 : 2,
|
||||
data: this.investments.map((position) => {
|
||||
return position.investment;
|
||||
data: this.data.map((position) => {
|
||||
return this.isInPercent
|
||||
? position.investment * 100
|
||||
: position.investment;
|
||||
}),
|
||||
label: $localize`Deposit`,
|
||||
segment: {
|
||||
@ -249,10 +256,11 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
return {
|
||||
...getTooltipOptions(
|
||||
this.isInPercent ? undefined : this.currency,
|
||||
this.isInPercent ? undefined : this.locale
|
||||
),
|
||||
...getTooltipOptions({
|
||||
currency: this.isInPercent ? undefined : this.currency,
|
||||
locale: this.isInPercent ? undefined : this.locale,
|
||||
unit: this.isInPercent ? '%' : undefined
|
||||
}),
|
||||
mode: 'index',
|
||||
position: <unknown>'top',
|
||||
xAlign: 'center',
|
||||
|
@ -9,9 +9,11 @@ import {
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
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 { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||
import { Subject } from 'rxjs';
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/router';
|
||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { ViewMode } from '@prisma/client';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@ -80,13 +79,13 @@ export class AuthGuard implements CanActivate {
|
||||
return;
|
||||
} else if (
|
||||
state.url.startsWith('/home') &&
|
||||
user.settings.viewMode === ViewMode.ZEN
|
||||
user.settings.viewMode === 'ZEN'
|
||||
) {
|
||||
this.router.navigate(['/zen']);
|
||||
resolve(false);
|
||||
return;
|
||||
} else if (state.url.startsWith('/start')) {
|
||||
if (user.settings.viewMode === ViewMode.ZEN) {
|
||||
if (user.settings.viewMode === 'ZEN') {
|
||||
this.router.navigate(['/zen']);
|
||||
} else {
|
||||
this.router.navigate(['/home']);
|
||||
@ -96,7 +95,7 @@ export class AuthGuard implements CanActivate {
|
||||
return;
|
||||
} else if (
|
||||
state.url.startsWith('/zen') &&
|
||||
user.settings.viewMode === ViewMode.DEFAULT
|
||||
user.settings.viewMode === 'DEFAULT'
|
||||
) {
|
||||
this.router.navigate(['/home']);
|
||||
resolve(false);
|
||||
|
@ -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() {
|
||||
this.dataService
|
||||
.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() {
|
||||
let couponCode = prompt($localize`Please enter your coupon code:`);
|
||||
couponCode = couponCode?.trim();
|
||||
@ -249,7 +244,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.snackBarRef = this.snackBar.open(
|
||||
'✅' + $localize`Coupon code has been redeemed`,
|
||||
'✅ ' + $localize`Coupon code has been redeemed`,
|
||||
$localize`Reload`,
|
||||
{
|
||||
duration: 3000
|
||||
|
@ -99,7 +99,7 @@
|
||||
name="baseCurrency"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
[value]="user.settings.baseCurrency"
|
||||
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)"
|
||||
(selectionChange)="onChangeUserSetting('baseCurrency', $event.value)"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let currency of currencies"
|
||||
@ -166,7 +166,7 @@
|
||||
name="viewMode"
|
||||
[disabled]="!hasPermissionToUpdateViewMode"
|
||||
[value]="user.settings.viewMode"
|
||||
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
||||
(selectionChange)="onChangeUserSetting('viewMode', $event.value)"
|
||||
>
|
||||
<mat-option value="DEFAULT">Default</mat-option>
|
||||
<mat-option value="ZEN">Zen</mat-option>
|
||||
@ -188,6 +188,22 @@
|
||||
></mat-slide-toggle>
|
||||
</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="pr-1 w-50" i18n>User ID</div>
|
||||
<div class="pl-1 w-50">{{ user?.id }}</div>
|
||||
|
@ -2,9 +2,14 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
HistoricalDataItem,
|
||||
Position,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
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 { SymbolProfile } from '@prisma/client';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { sortBy } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
@ -18,17 +23,22 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './analysis-page.html'
|
||||
})
|
||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public benchmarkDataItems: HistoricalDataItem[] = [];
|
||||
public benchmarks: Partial<SymbolProfile>[];
|
||||
public bottom3: Position[];
|
||||
public daysInMarket: number;
|
||||
public deviceType: string;
|
||||
public firstOrderDate: Date;
|
||||
public hasImpersonationId: boolean;
|
||||
public investments: InvestmentItem[];
|
||||
public investmentsByMonth: InvestmentItem[];
|
||||
public isLoadingBenchmarkComparator: boolean;
|
||||
public mode: GroupBy;
|
||||
public modeOptions: ToggleOption[] = [
|
||||
{ label: $localize`Monthly`, value: 'month' },
|
||||
{ label: $localize`Accumulating`, value: undefined }
|
||||
];
|
||||
public performanceDataItems: HistoricalDataItem[];
|
||||
public top3: Position[];
|
||||
public user: User;
|
||||
|
||||
@ -40,7 +50,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
private deviceService: DeviceDetectorService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
) {
|
||||
const { benchmarks } = this.dataService.fetchInfo();
|
||||
this.benchmarks = benchmarks;
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
@ -52,6 +65,79 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.hasImpersonationId = !!aId;
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeBenchmark(symbolProfileId: string) {
|
||||
this.dataService
|
||||
.putUserSetting({ benchmark: symbolProfileId })
|
||||
.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.isLoadingBenchmarkComparator = true;
|
||||
|
||||
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
|
||||
.fetchInvestments()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -91,23 +177,37 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
private updateBenchmarkDataItems() {
|
||||
if (this.user.settings.benchmark) {
|
||||
const { dataSource, symbol } =
|
||||
this.benchmarks.find(({ id }) => {
|
||||
return id === this.user.settings.benchmark;
|
||||
}) ?? {};
|
||||
|
||||
this.dataService
|
||||
.fetchBenchmarkBySymbol({
|
||||
dataSource,
|
||||
symbol,
|
||||
startDate: this.firstOrderDate
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketData }) => {
|
||||
this.benchmarkDataItems = marketData.map(({ date, value }) => {
|
||||
return {
|
||||
date,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
this.isLoadingBenchmarkComparator = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeGroupBy(aMode: GroupBy) {
|
||||
this.mode = aMode;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
});
|
||||
} else {
|
||||
this.isLoadingBenchmarkComparator = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +1,24 @@
|
||||
<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">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
||||
<div class="mb-4">
|
||||
<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>
|
||||
<gf-benchmark-comparator
|
||||
class="h-100"
|
||||
[benchmark]="user?.settings?.benchmark"
|
||||
[benchmarkDataItems]="benchmarkDataItems"
|
||||
[benchmarks]="benchmarks"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isLoading]="isLoadingBenchmarkComparator"
|
||||
[locale]="user?.settings?.locale"
|
||||
[performanceDataItems]="performanceDataItems"
|
||||
[user]="user"
|
||||
(benchmarkChanged)="onChangeBenchmark($event)"
|
||||
(dateRangeChanged)="onChangeDateRange($event)"
|
||||
></gf-benchmark-comparator>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-5 row">
|
||||
<div class="col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
@ -124,4 +95,49 @@
|
||||
</mat-card>
|
||||
</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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
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 { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
@ -15,6 +16,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
||||
imports: [
|
||||
AnalysisPageRoutingModule,
|
||||
CommonModule,
|
||||
GfBenchmarkComparatorModule,
|
||||
GfInvestmentChartModule,
|
||||
GfPremiumIndicatorModule,
|
||||
GfToggleModule,
|
||||
|
@ -73,7 +73,18 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
||||
this.dataService
|
||||
.putUserSetting({ savingsRate })
|
||||
.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() {
|
||||
|
@ -10,6 +10,15 @@
|
||||
get started. Due to the time it saves, this will be the best option
|
||||
for most people. The revenue is used for covering the hosting costs.
|
||||
</p>
|
||||
<p *ngIf="user?.subscription?.type === 'Basic'">
|
||||
If you plan to open an account at <i>DEGIRO</i>, <i>frankly</i>,
|
||||
<i>Interactive Brokers</i>, <i>Swissquote</i>, or <i>VIAC</i>, please
|
||||
<a href="mailto:hi@ghostfol.io?Subject=Referral link for..."
|
||||
>contact us</a
|
||||
>
|
||||
to use our referral link and get a Ghostfolio Premium membership for
|
||||
one year.
|
||||
</p>
|
||||
<p>
|
||||
If you prefer to run Ghostfolio on your own infrastructure, please
|
||||
find the source code and further instructions on
|
||||
|
@ -4,9 +4,8 @@ import { Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.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 { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||
import { Role } from '@prisma/client';
|
||||
import { format } from 'date-fns';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
|
@ -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 { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||
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 { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Access,
|
||||
Accounts,
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
BenchmarkMarketDataDetails,
|
||||
BenchmarkResponse,
|
||||
Export,
|
||||
Filter,
|
||||
@ -31,12 +32,13 @@ import {
|
||||
PortfolioPublicDetails,
|
||||
PortfolioReport,
|
||||
PortfolioSummary,
|
||||
UniqueAsset,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
||||
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 { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
@ -181,12 +183,27 @@ export class DataService {
|
||||
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() {
|
||||
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
||||
}
|
||||
|
||||
public fetchChart({ range }: { range: DateRange }) {
|
||||
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
|
||||
public fetchChart({ range, version }: { range: DateRange; version: number }) {
|
||||
return this.http.get<PortfolioChart>(`/api/v${version}/portfolio/chart`, {
|
||||
params: { range }
|
||||
});
|
||||
}
|
||||
@ -430,10 +447,6 @@ export class DataService {
|
||||
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) {
|
||||
return this.http.post('/api/v1/subscription/redeem-coupon', {
|
||||
couponCode
|
||||
|
@ -642,7 +642,7 @@
|
||||
<target state="translated">Aktuelle Marktstimmung</target>
|
||||
<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="linenumber">11</context>
|
||||
<context context-type="linenumber">12</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
||||
@ -1058,7 +1058,7 @@
|
||||
<target state="translated">Bitte gib den Betrag deines Notfallfonds ein:</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
||||
@ -1262,7 +1262,7 @@
|
||||
<target state="translated">Bitte gebe deinen Gutscheincode ein:</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4420880039966769543" datatype="html">
|
||||
@ -1270,7 +1270,7 @@
|
||||
<target state="translated">Gutscheincode konnte nicht eingelöst werden</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4819099731531004979" datatype="html">
|
||||
@ -1278,7 +1278,7 @@
|
||||
<target state="translated">Gutscheincode wurde eingelöst</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7967484035994732534" datatype="html">
|
||||
@ -1286,7 +1286,7 @@
|
||||
<target state="translated">Neu laden</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7963559562180316948" datatype="html">
|
||||
@ -1294,7 +1294,7 @@
|
||||
<target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
||||
@ -1382,7 +1382,7 @@
|
||||
<target state="new">Locale</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||
@ -1390,7 +1390,7 @@
|
||||
<target state="translated">Datums- und Zahlenformat</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||
@ -1398,7 +1398,7 @@
|
||||
<target state="translated">Ansicht</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||
@ -1406,7 +1406,7 @@
|
||||
<target state="translated">Einloggen mit Fingerabdruck</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||
@ -1414,7 +1414,7 @@
|
||||
<target state="translated">Benutzer ID</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||
@ -1422,7 +1422,7 @@
|
||||
<target state="translated">Zugangsberechtigung</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||
@ -1694,7 +1694,7 @@
|
||||
<target state="translated">Analyse</target>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
|
||||
@ -1714,7 +1714,7 @@
|
||||
<target state="translated">Gewinner</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
|
||||
@ -1722,7 +1722,7 @@
|
||||
<target state="translated">Verlierer</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5857197365507636437" datatype="html">
|
||||
@ -2268,6 +2268,10 @@
|
||||
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
|
||||
<source>Beta</source>
|
||||
<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 context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">116</context>
|
||||
@ -2522,7 +2526,7 @@
|
||||
<target state="translated">Monatlich</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1975246224413290232" datatype="html">
|
||||
@ -2530,15 +2534,19 @@
|
||||
<target state="translated">Aufsummiert</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5213771062241898526" datatype="html">
|
||||
<source>Deposit</source>
|
||||
<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 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 purpose="location">
|
||||
<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-group>
|
||||
</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>
|
||||
</file>
|
||||
</xliff>
|
||||
|
@ -583,7 +583,7 @@
|
||||
<source>Current Market Mood</source>
|
||||
<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="linenumber">11</context>
|
||||
<context context-type="linenumber">12</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
||||
@ -957,7 +957,7 @@
|
||||
<source>Please enter the amount of your emergency fund:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
||||
@ -1137,35 +1137,35 @@
|
||||
<source>Please enter your coupon code:</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4420880039966769543" datatype="html">
|
||||
<source>Could not redeem coupon code</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4819099731531004979" datatype="html">
|
||||
<source>Coupon code has been redeemed</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7967484035994732534" datatype="html">
|
||||
<source>Reload</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7963559562180316948" datatype="html">
|
||||
<source>Do you really want to remove this sign in method?</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
||||
@ -1243,42 +1243,42 @@
|
||||
<source>Locale</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||
<source>Date and number format</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||
<source>View Mode</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||
<source>Sign in with fingerprint</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||
<source>User ID</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||
<source>Granted Access</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||
@ -1520,7 +1520,7 @@
|
||||
<source>Analysis</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
|
||||
@ -1531,21 +1531,21 @@
|
||||
<source>Investment Timeline</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
|
||||
<source>Top</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
|
||||
<source>Bottom</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5857197365507636437" datatype="html">
|
||||
@ -2027,6 +2027,10 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
|
||||
<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 context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">116</context>
|
||||
@ -2246,7 +2250,7 @@
|
||||
<source>Accumulating</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2937311350146031865" datatype="html">
|
||||
@ -2265,9 +2269,13 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="5213771062241898526" datatype="html">
|
||||
<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 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 purpose="location">
|
||||
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
|
||||
@ -2285,7 +2293,7 @@
|
||||
<source>Monthly</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html">
|
||||
@ -2359,6 +2367,34 @@
|
||||
<context context-type="linenumber">6</context>
|
||||
</context-group>
|
||||
</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>
|
||||
</file>
|
||||
</xliff>
|
@ -2,7 +2,15 @@ import { Chart, TooltipPosition } from 'chart.js';
|
||||
|
||||
import { getBackgroundColor, getTextColor } from './helper';
|
||||
|
||||
export function getTooltipOptions(currency = '', locale = '') {
|
||||
export function getTooltipOptions({
|
||||
currency = '',
|
||||
locale = '',
|
||||
unit = ''
|
||||
}: {
|
||||
currency?: string;
|
||||
locale?: string;
|
||||
unit?: string;
|
||||
} = {}) {
|
||||
return {
|
||||
backgroundColor: getBackgroundColor(),
|
||||
bodyColor: `rgb(${getTextColor()})`,
|
||||
@ -20,6 +28,8 @@ export function getTooltipOptions(currency = '', locale = '') {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 2
|
||||
})} ${currency}`;
|
||||
} else if (unit) {
|
||||
label += `${context.parsed.y.toFixed(2)} ${unit}`;
|
||||
} else {
|
||||
label += context.parsed.y.toFixed(2);
|
||||
}
|
||||
|
@ -42,7 +42,11 @@ export function downloadAsFile({
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { LineChartItem } from './line-chart-item.interface';
|
||||
|
||||
export interface BenchmarkMarketDataDetails {
|
||||
marketData: LineChartItem[];
|
||||
}
|
@ -7,6 +7,7 @@ import {
|
||||
AdminMarketData,
|
||||
AdminMarketDataItem
|
||||
} from './admin-market-data.interface';
|
||||
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
|
||||
import { Benchmark } from './benchmark.interface';
|
||||
import { Coupon } from './coupon.interface';
|
||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||
@ -15,6 +16,7 @@ import { FilterGroup } from './filter-group.interface';
|
||||
import { Filter } from './filter.interface';
|
||||
import { HistoricalDataItem } from './historical-data-item.interface';
|
||||
import { InfoItem } from './info-item.interface';
|
||||
import { LineChartItem } from './line-chart-item.interface';
|
||||
import { PortfolioChart } from './portfolio-chart.interface';
|
||||
import { PortfolioDetails } from './portfolio-details.interface';
|
||||
import { PortfolioInvestments } from './portfolio-investments.interface';
|
||||
@ -47,6 +49,7 @@ export {
|
||||
AdminMarketDataDetails,
|
||||
AdminMarketDataItem,
|
||||
Benchmark,
|
||||
BenchmarkMarketDataDetails,
|
||||
BenchmarkResponse,
|
||||
Coupon,
|
||||
EnhancedSymbolProfile,
|
||||
@ -55,6 +58,7 @@ export {
|
||||
FilterGroup,
|
||||
HistoricalDataItem,
|
||||
InfoItem,
|
||||
LineChartItem,
|
||||
OAuthResponse,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Tag } from '@prisma/client';
|
||||
import { SymbolProfile, Tag } from '@prisma/client';
|
||||
|
||||
import { Statistics } from './statistics.interface';
|
||||
import { Subscription } from './subscription.interface';
|
||||
|
||||
export interface InfoItem {
|
||||
baseCurrency: string;
|
||||
benchmarks: Partial<SymbolProfile>[];
|
||||
currencies: string[];
|
||||
demoAuthToken: string;
|
||||
fearAndGreedDataSource?: string;
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { ViewMode } from '@prisma/client';
|
||||
import { DateRange, ViewMode } from '@ghostfolio/common/types';
|
||||
|
||||
export interface UserSettings {
|
||||
baseCurrency?: string;
|
||||
benchmark?: string;
|
||||
dateRange?: DateRange;
|
||||
emergencyFund?: number;
|
||||
isExperimentalFeatures?: boolean;
|
||||
isRestrictedView?: boolean;
|
||||
language?: string;
|
||||
locale: string;
|
||||
locale?: string;
|
||||
viewMode?: ViewMode;
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||
import { Account, Settings, User } from '@prisma/client';
|
||||
import { UserSettings } from './user-settings.interface';
|
||||
|
||||
export type UserWithSettings = User & {
|
||||
Account: Account[];
|
||||
permissions?: string[];
|
||||
Settings: Settings;
|
||||
Settings: Settings & { settings: UserSettings };
|
||||
subscription?: {
|
||||
expiresAt?: Date;
|
||||
type: SubscriptionType;
|
||||
|
@ -8,6 +8,7 @@ import { Market } from './market.type';
|
||||
import type { OrderWithAccount } from './order-with-account.type';
|
||||
import type { RequestWithUser } from './request-with-user.type';
|
||||
import { ToggleOption } from './toggle-option.type';
|
||||
import type { ViewMode } from './view-mode.type';
|
||||
|
||||
export type {
|
||||
AccessWithGranteeUser,
|
||||
@ -19,5 +20,6 @@ export type {
|
||||
MarketState,
|
||||
OrderWithAccount,
|
||||
RequestWithUser,
|
||||
ToggleOption
|
||||
ToggleOption,
|
||||
ViewMode
|
||||
};
|
||||
|
1
libs/common/src/lib/types/view-mode.type.ts
Normal file
1
libs/common/src/lib/types/view-mode.type.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ViewMode = 'DEFAULT' | 'ZEN';
|
@ -25,6 +25,7 @@ import {
|
||||
getDateFormatString,
|
||||
getTextColor
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { LineChartItem } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
Chart,
|
||||
Filler,
|
||||
@ -36,8 +37,6 @@ import {
|
||||
Tooltip
|
||||
} from 'chart.js';
|
||||
|
||||
import { LineChartItem } from './interfaces/line-chart.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-line-chart',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -56,6 +55,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
@Input() showXAxis = false;
|
||||
@Input() showYAxis = false;
|
||||
@Input() symbol: string;
|
||||
@Input() unit: string;
|
||||
@Input() yMax: number;
|
||||
@Input() yMaxLabel: string;
|
||||
@Input() yMin: number;
|
||||
@ -259,7 +259,11 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
return {
|
||||
...getTooltipOptions(this.currency, this.locale),
|
||||
...getTooltipOptions({
|
||||
currency: this.currency,
|
||||
locale: this.locale,
|
||||
unit: this.unit
|
||||
}),
|
||||
mode: 'index',
|
||||
position: <unknown>'top',
|
||||
xAlign: 'center',
|
||||
|
@ -349,7 +349,10 @@ export class PortfolioProportionChartComponent
|
||||
|
||||
private getTooltipPluginConfiguration(data: ChartConfiguration['data']) {
|
||||
return {
|
||||
...getTooltipOptions(this.baseCurrency, this.locale),
|
||||
...getTooltipOptions({
|
||||
currency: this.baseCurrency,
|
||||
locale: this.locale
|
||||
}),
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const labelIndex =
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.186.0",
|
||||
"version": "1.193.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -126,7 +126,7 @@
|
||||
"svgmap": "2.6.0",
|
||||
"twitter-api-v2": "1.10.3",
|
||||
"uuid": "8.3.2",
|
||||
"yahoo-finance2": "2.3.3",
|
||||
"yahoo-finance2": "2.3.6",
|
||||
"zone.js": "0.11.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Settings" DROP COLUMN "currency",
|
||||
DROP COLUMN "viewMode";
|
@ -102,10 +102,8 @@ model Property {
|
||||
}
|
||||
|
||||
model Settings {
|
||||
currency String?
|
||||
settings Json?
|
||||
updatedAt DateTime @updatedAt
|
||||
viewMode ViewMode?
|
||||
userId String @id
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
@ -20374,10 +20374,10 @@ y18n@^5.0.5:
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yahoo-finance2@2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.3.tgz#a3b537f3bec31b99f075aa44125c9520a42cdfe3"
|
||||
integrity sha512-qvgo5tFkrYRh1anbTargIY7Fa7FRiYZfb+iTnY1SUfBZ9HP8OvreTYDfLz/1w23PGgFi4w9uysGEwVrUVZ5Hlw==
|
||||
yahoo-finance2@2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.6.tgz#4ba27d33385f5520752e96da3cf4df93ad26ce7a"
|
||||
integrity sha512-qE4Nu4DY4XSAL+RzYXSaWFQBIYyBbMDaND1VoMGnmekWBGS8+/3GxfajXrzEfxxwFvT4tM/1i4G64OVxKE0EMA==
|
||||
dependencies:
|
||||
ajv "8.10.0"
|
||||
ajv-formats "2.1.1"
|
||||
|
Reference in New Issue
Block a user