Compare commits

...

61 Commits

Author SHA1 Message Date
5c9fa71d95 Release 1.199.1 (#1301) 2022-09-27 20:44:32 +02:00
fefbfa31d1 Release 1.199.0 (#1300) 2022-09-27 20:28:46 +02:00
93a1fae51c Feature/support sectors of mutual funds (#1298)
* Support sectors

* Update changelog

Co-authored-by: Mitchell <5503199+m11tch@users.noreply.github.com>
2022-09-27 17:38:53 +02:00
3715edd9ba Extract locales (#1297) 2022-09-26 19:44:19 +02:00
e3916e1ba3 Feature/setup espanol (#1293)
* Setup Español

* Update changelog
2022-09-26 18:39:11 +02:00
76ceac4edc Add spanish translation (#1296)
Co-Authored-By: alfredonodo <41476198+alfredonodo@users.noreply.github.com>
Co-Authored-By: casitu <25199636+casitu@users.noreply.github.com>
2022-09-26 18:14:53 +02:00
333b63bfe2 Release 1.198.0 (#1294) 2022-09-25 21:46:19 +02:00
3006c21b12 Add dutch translation (#1291)
* Add dutch translation
2022-09-25 18:12:33 +02:00
f01a3f893d Exclude accounts (#1289)
* Exclude accounts

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

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

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

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

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

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

* Update changelog
2022-09-22 20:52:03 +02:00
54bf4c7a43 Update messages.it.xlf (#1280) 2022-09-22 20:51:31 +02:00
c0ace51ee9 Release 1.195.0 (#1277) 2022-09-20 20:23:41 +02:00
b1b5689242 Feature/improve performance of chart calculation (#1271)
* Improve performance chart calculation

Co-Authored-By: gizmodus <11334553+gizmodus@users.noreply.github.com>

* Update changelog

Co-Authored-By: gizmodus <11334553+gizmodus@users.noreply.github.com>

* Improve chart tooltip of benchmark comparator

* Update changelog

Co-authored-by: gizmodus <11334553+gizmodus@users.noreply.github.com>
2022-09-20 20:22:01 +02:00
b68cdaf8ea Add issue template (#1275) 2022-09-19 21:22:54 +02:00
b387a80a0d Add bullet (#1270) 2022-09-17 21:29:37 +02:00
6e4660295a Release 1.194.0 (#1269) 2022-09-17 21:26:08 +02:00
d4c3a9d1e8 Feature/add percentage visualization of the current filter (#1268)
* Add percentage visualization of the active filter

* Update changelog
2022-09-17 21:24:09 +02:00
263f6b32f2 Bugfix/fix performance chart calculation (#1267)
* Respect end date in performance chart calculation

Co-Authored-By: gizmodus <11334553+gizmodus@users.noreply.github.com>

* Update changelog

Co-Authored-By: gizmodus <11334553+gizmodus@users.noreply.github.com>
2022-09-17 08:33:04 +02:00
637f31ae3b Add instruction for NODE_ENV: production (#1266) 2022-09-17 08:31:56 +02:00
547e27c7a1 Feature/set node env in docker compose files (#1261)
* Set NODE_ENV to production

* Update changelog
2022-09-15 17:20:03 +02:00
f10dc176f2 Feature/clean up german localization (#1260)
* Clean up German localization

* Set up Italian

* Update changelog
2022-09-15 16:58:00 +02:00
0a966e46cd Improve translation (#1258) 2022-09-15 10:37:07 +02:00
4f281d25e1 Release 1.193.0 (#1257) 2022-09-14 20:11:47 +02:00
aaba8c35c2 Feature/extend pricing page with referral section (#1256)
* Add referral section

* Update changelog
2022-09-14 20:10:35 +02:00
7d27cb3398 Bugfix/use base currency in exchange rate service instead of usd (#1255)
* Change from USD to base currency

* Update changelog
2022-09-14 19:56:18 +02:00
91678028b5 Bugfix/fix missing assets during local development (#1253)
* Extend setup for development (missing assets)

* Update changelog
2022-09-14 19:26:59 +02:00
5e3cac8ac9 Feature/sort benchmarks by name (#1250)
* Sort benchmarks by name

* Update changelog
2022-09-12 13:34:06 +02:00
33f20b6b48 Release 1.192.0 (#1249) 2022-09-11 09:21:48 +02:00
e4fd255dd7 Bugfix/improve loading indicator of benchmark comparator (#1247)
* Improve loading indicator

* Update changelog
2022-09-11 09:20:15 +02:00
e320aa91f7 Feature/simplify benchmark configuration (#1248)
* Simplify benchmark configuration

* Update changelog
2022-09-11 09:19:50 +02:00
0fcfa6c1bd Bugfix/improve error handling in benchmark calculation (#1246)
* Improve error handling

* Update changelog
2022-09-10 18:58:31 +02:00
42d32ed652 Feature/upgrade yahoo finance2 to version 2.3.6 (#1245)
* Upgrade yahoo-finance2 to version 2.3.6

* Update changelog
2022-09-10 18:57:41 +02:00
21b4b0ef24 Release 1.191.0 (#1244) 2022-09-10 16:13:22 +02:00
4f8fe83a16 Feature/clean up user database schema (#1242)
* Clean up user database schema

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

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

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

* Refactor currency and view mode in the user user settings

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

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

* Distinguish between currency and unit in tooltip

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

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

* Update changelog
2022-09-03 21:41:06 +02:00
0cc42ffd7c Add end date parameter (#1224)
* Add end date parameter
2022-09-03 21:28:57 +02:00
3ccb812ac3 Release 1.186.2 (#1223) 2022-09-03 11:37:14 +02:00
0a8549db3e Release 1.186.1 (#1222) 2022-09-03 11:19:31 +02:00
129 changed files with 10766 additions and 897 deletions

37
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,37 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: ''
assignees: ''
---
The Issue tracker is **ONLY** used for reporting bugs. New features should be discussed on our [Slack channel](https://ghostfolio.slack.com) or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions).
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
4.
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Logs**
<!-- If applicable, add logs to help explain your problem. -->
**Environment (please complete the following information):**
- Ghostfolio Version [e.g. 1.194.0]
- Browser [e.g. chrome]
- OS
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@ -5,7 +5,142 @@ 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.0 - 03.09.2022
## 1.199.1 - 27.09.2022
### Added
- Set up the language localization for Español (`es`)
- Added support for sectors in mutual funds
## 1.198.0 - 25.09.2022
### Added
- Added support to exclude an account from analysis
- Set up the language localization for Nederlands (`nl`)
## 1.197.0 - 24.09.2022
### Added
- Added the value of the active filter in percentage on the allocations page
- Extended the feature overview page by multi-language support (English, German, Italian)
### Changed
- Combined the performance and chart calculation
- Improved the style of various selectors (density)
## 1.196.0 - 22.09.2022
### Added
- Set up the language localization for Italiano (`it`)
- Extended the landing page
## 1.195.0 - 20.09.2022
### Changed
- Improved the algorithm of the performance chart calculation
### Fixed
- Improved the chart tooltip of the benchmark comparator
## 1.194.0 - 17.09.2022
### Added
- Added `NODE_ENV: production` to the `docker-compose` files (`docker-compose.yml` and `docker-compose.build.yml`)
- Visualized the percentage of the active filter on the allocations page
### Changed
- Improved the language localization for German (`de`)
### Fixed
- Respected the end date in the performance chart calculation
### Todo
- Set `NODE_ENV: production` as in [docker-compose.yml](https://github.com/ghostfolio/ghostfolio/blob/main/docker/docker-compose.yml)
## 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
@ -16,7 +151,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Made the environment variables `REDIS_HOST` and `REDIS_PORT` mandatory
- Fixed the environment variables `REDIS_HOST`, `REDIS_PASSWORD` and `REDIS_PORT` in the Redis configuration
- Handled errors in the portfolio calculation if there is no internet connection
- Fixed the _GitHub_ contributors count on the about page
@ -93,7 +228,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
- Set up language localization for German (`de`)
- Set up the language localization for German (`de`)
- Resolved the feature graphic of the blog post
### Changed

View File

@ -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))

View File

@ -136,6 +136,18 @@
"baseHref": "/en/",
"localize": ["en"]
},
"development-es": {
"baseHref": "/es/",
"localize": ["es"]
},
"development-it": {
"baseHref": "/it/",
"localize": ["it"]
},
"development-nl": {
"baseHref": "/nl/",
"localize": ["nl"]
},
"production": {
"fileReplacements": [
{
@ -180,6 +192,15 @@
"development-en": {
"browserTarget": "client:build:development-en"
},
"development-es": {
"browserTarget": "client:build:development-es"
},
"development-it": {
"browserTarget": "client:build:development-it"
},
"development-nl": {
"browserTarget": "client:build:development-nl"
},
"production": {
"browserTarget": "client:build:production"
}
@ -191,7 +212,12 @@
"browserTarget": "client:build",
"includeContext": true,
"outputPath": "src/locales",
"targetFiles": ["messages.de.xlf"]
"targetFiles": [
"messages.de.xlf",
"messages.es.xlf",
"messages.it.xlf",
"messages.nl.xlf"
]
}
},
"lint": {
@ -214,6 +240,18 @@
"de": {
"baseHref": "/de/",
"translation": "apps/client/src/locales/messages.de.xlf"
},
"es": {
"baseHref": "/es/",
"translation": "apps/client/src/locales/messages.es.xlf"
},
"it": {
"baseHref": "/it/",
"translation": "apps/client/src/locales/messages.it.xlf"
},
"nl": {
"baseHref": "/nl/",
"translation": "apps/client/src/locales/messages.nl.xlf"
}
},
"sourceLocale": "en"

View File

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

View File

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

View File

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

View File

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

View File

@ -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
});
}
}

View File

@ -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]

View File

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

View File

@ -1,12 +1,23 @@
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 {
MAX_CHART_ITEMS,
PROPERTY_BENCHMARKS
} from '@ghostfolio/common/config';
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';
@Injectable()
@ -18,9 +29,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 +58,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 +109,97 @@ 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
}
}
})
]);
const step = Math.round(
marketDataItems.length / Math.min(marketDataItems.length, MAX_CHART_ITEMS)
);
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
const response = {
marketData: [
...marketDataItems
.filter((marketDataItem, index) => {
return index % step === 0;
})
.map((marketDataItem) => {
return {
date: format(marketDataItem.date, DATE_FORMAT),
value:
marketPriceAtStartDate === 0
? 0
: this.calculateChangeInPercentage(
marketPriceAtStartDate,
marketDataItem.marketPrice
) * 100
};
})
]
};
if (currentSymbolItem?.marketPrice) {
response.marketData.push({
date: format(new Date(), DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
) * 100
});
}
return response;
}
private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
}
}

View File

@ -4,22 +4,51 @@ 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 indexHtmlEs = '';
public indexHtmlIt = '';
public indexHtmlNl = '';
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'
);
this.indexHtmlEs = fs.readFileSync(
this.getPathOfIndexHtmlFile('es'),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
this.indexHtmlNl = fs.readFileSync(
this.getPathOfIndexHtmlFile('nl'),
'utf8'
);
} catch {}
}
public use(req: Request, res: Response, next: NextFunction) {
let featureGraphicPath = 'assets/cover.png';
@ -31,7 +60,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/')) {
@ -43,6 +76,33 @@ export class FrontendMiddleware implements NestMiddleware {
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/es' || req.path.startsWith('/es/')) {
res.send(
this.interpolate(this.indexHtmlEs, {
featureGraphicPath,
languageCode: 'es',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/it' || req.path.startsWith('/it/')) {
res.send(
this.interpolate(this.indexHtmlIt, {
featureGraphicPath,
languageCode: 'it',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/nl' || req.path.startsWith('/nl/')) {
res.send(
this.interpolate(this.indexHtmlNl, {
featureGraphicPath,
languageCode: 'nl',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
res.send(
this.interpolate(this.indexHtmlEn, {

View File

@ -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();
}

View File

@ -1,3 +1,4 @@
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { 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,

View File

@ -1,3 +1,4 @@
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { 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(),

View File

@ -103,13 +103,14 @@ 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,
userCurrency,
includeDrafts: true,
userId: impersonationUserId || this.request.user.id
userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true
});
if (

View File

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

View File

@ -20,7 +20,7 @@ import {
min,
set
} from 'date-fns';
import { first, flatten, isNumber, sortBy } from 'lodash';
import { first, flatten, isNumber, last, sortBy } from 'lodash';
import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface';
@ -167,13 +167,143 @@ export class PortfolioCalculator {
this.transactionPoints = transactionPoints;
}
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
if (!this.transactionPoints?.length) {
public async getChartData(start: Date, end = new Date(Date.now()), step = 1) {
const symbols: { [symbol: string]: boolean } = {};
const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
const firstIndex = transactionPointsBeforeEndDate.length;
const dates: Date[] = [];
const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: string } = {};
let day = start;
while (isBefore(day, end)) {
dates.push(resetHours(day));
day = addDays(day, step);
}
dates.push(resetHours(end));
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
currencies[item.symbol] = item.currency;
symbols[item.symbol] = true;
}
const marketSymbols = await this.currentRateService.getValues({
currencies,
dataGatheringItems,
dateQuery: {
in: dates
},
userCurrency: this.currency
});
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
for (const marketSymbol of marketSymbols) {
const dateString = format(marketSymbol.date, DATE_FORMAT);
if (!marketSymbolMap[dateString]) {
marketSymbolMap[dateString] = {};
}
if (marketSymbol.marketPriceInBaseCurrency) {
marketSymbolMap[dateString][marketSymbol.symbol] = new Big(
marketSymbol.marketPriceInBaseCurrency
);
}
}
const netPerformanceValuesBySymbol: {
[symbol: string]: { [date: string]: Big };
} = {};
const investmentValuesBySymbol: {
[symbol: string]: { [date: string]: Big };
} = {};
const totalNetPerformanceValues: { [date: string]: Big } = {};
const totalInvestmentValues: { [date: string]: Big } = {};
for (const symbol of Object.keys(symbols)) {
const { netPerformanceValues, investmentValues } = this.getSymbolMetrics({
end,
marketSymbolMap,
start,
step,
symbol,
isChartMode: true
});
netPerformanceValuesBySymbol[symbol] = netPerformanceValues;
investmentValuesBySymbol[symbol] = investmentValues;
}
for (const currentDate of dates) {
const dateString = format(currentDate, DATE_FORMAT);
for (const symbol of Object.keys(netPerformanceValuesBySymbol)) {
totalNetPerformanceValues[dateString] =
totalNetPerformanceValues[dateString] ?? new Big(0);
if (netPerformanceValuesBySymbol[symbol]?.[dateString]) {
totalNetPerformanceValues[dateString] = totalNetPerformanceValues[
dateString
].add(netPerformanceValuesBySymbol[symbol][dateString]);
}
totalInvestmentValues[dateString] =
totalInvestmentValues[dateString] ?? new Big(0);
if (investmentValuesBySymbol[symbol]?.[dateString]) {
totalInvestmentValues[dateString] = totalInvestmentValues[
dateString
].add(investmentValuesBySymbol[symbol][dateString]);
}
}
}
return Object.keys(totalNetPerformanceValues).map((date) => {
const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
? 0
: totalNetPerformanceValues[date]
.div(totalInvestmentValues[date])
.mul(100)
.toNumber();
return {
date,
netPerformanceInPercentage,
netPerformance: totalNetPerformanceValues[date].toNumber(),
value: netPerformanceInPercentage
};
});
}
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 +312,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 +370,7 @@ export class PortfolioCalculator {
}
}
const todayString = format(today, DATE_FORMAT);
const endDateString = format(end, DATE_FORMAT);
if (firstIndex > 0) {
firstIndex--;
@ -254,7 +383,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 +393,7 @@ export class PortfolioCalculator {
netPerformance,
netPerformancePercentage
} = this.getSymbolMetrics({
end,
marketSymbolMap,
start,
symbol: item.symbol
@ -700,14 +830,20 @@ export class PortfolioCalculator {
}
private getSymbolMetrics({
end,
isChartMode = false,
marketSymbolMap,
start,
step = 1,
symbol
}: {
end: Date;
isChartMode?: boolean;
marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
};
start: Date;
step?: number;
symbol: string;
}) {
let orders: PortfolioOrderItem[] = this.orders.filter((order) => {
@ -726,13 +862,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 ||
@ -757,10 +892,12 @@ export class PortfolioCalculator {
let grossPerformanceFromSells = new Big(0);
let initialValue: Big;
let investmentAtStartDate: Big;
const investmentValues: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0);
let lastTransactionInvestment = new Big(0);
let lastValueOfInvestmentBeforeTransaction = new Big(0);
let maxTotalInvestment = new Big(0);
const netPerformanceValues: { [date: string]: Big } = {};
let timeWeightedGrossPerformancePercentage = new Big(1);
let timeWeightedNetPerformancePercentage = new Big(1);
let totalInvestment = new Big(0);
@ -785,7 +922,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',
@ -795,6 +932,41 @@ export class PortfolioCalculator {
unitPrice: unitPriceAtEndDate
});
let day = start;
let lastUnitPrice: Big;
if (isChartMode) {
const datesWithOrders = {};
for (const order of orders) {
datesWithOrders[order.date] = true;
}
while (isBefore(day, end)) {
const hasDate = datesWithOrders[format(day, DATE_FORMAT)];
if (!hasDate) {
orders.push({
symbol,
currency: null,
date: format(day, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
name: '',
quantity: new Big(0),
type: TypeOfOrder.BUY,
unitPrice:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
lastUnitPrice
});
}
lastUnitPrice = last(orders).unitPrice;
day = addDays(day, step);
}
}
// Sort orders so that the start and end placeholder order are at the right
// position
orders = sortBy(orders, (order) => {
@ -957,6 +1129,18 @@ export class PortfolioCalculator {
feesAtStartDate = fees;
grossPerformanceAtStartDate = grossPerformance;
}
if (isChartMode && i > indexOfStartOrder) {
netPerformanceValues[order.date] = grossPerformance
.minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate));
investmentValues[order.date] = totalInvestment;
}
if (i === indexOfEndOrder) {
break;
}
}
timeWeightedGrossPerformancePercentage =
@ -1042,7 +1226,9 @@ export class PortfolioCalculator {
return {
initialValue,
grossPerformancePercentage,
investmentValues,
netPerformancePercentage,
netPerformanceValues,
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
netPerformance: totalNetPerformance,
grossPerformance: totalGrossPerformance

View File

@ -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';
@ -148,18 +148,29 @@ export class PortfolioController {
})
];
const { accounts, holdings, hasErrors } =
await this.portfolioService.getDetails(
impersonationId,
this.request.user.id,
range,
filters
);
let portfolioSummary: PortfolioSummary;
const {
accounts,
filteredValueInBaseCurrency,
filteredValueInPercentage,
hasErrors,
holdings,
summary,
totalValueInBaseCurrency
} = await this.portfolioService.getDetails(
impersonationId,
this.request.user.id,
range,
filters
);
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
hasError = true;
}
portfolioSummary = summary;
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
@ -175,7 +186,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);
@ -193,6 +204,22 @@ export class PortfolioController {
accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment;
}
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
}
let hasDetails = true;
@ -214,8 +241,12 @@ export class PortfolioController {
return {
accounts,
filteredValueInBaseCurrency,
filteredValueInPercentage,
hasError,
holdings
holdings,
totalValueInBaseCurrency,
summary: hasDetails ? portfolioSummary : undefined
};
}
@ -278,7 +309,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(
@ -290,6 +321,35 @@ export class PortfolioController {
return performanceInformation;
}
@Get('performance')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
public async getPerformanceV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange
): Promise<PortfolioPerformanceResponse> {
const performanceInformation = await this.portfolioService.getPerformanceV2(
{
dateRange,
impersonationId
}
);
if (
impersonationId ||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user)
) {
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
['currentGrossPerformance', 'currentNetPerformance', 'currentValue']
);
}
return performanceInformation;
}
@Get('positions')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@ -358,7 +418,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);
@ -381,46 +442,6 @@ export class PortfolioController {
return portfolioPublicDetails;
}
@Get('summary')
@UseGuards(AuthGuard('jwt'))
public async getSummary(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioSummary> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let summary = await this.portfolioService.getSummary(impersonationId);
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
summary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
}
return summary;
}
@Get('position/:dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)

View File

@ -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';
@ -22,6 +21,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import {
ASSET_SUB_CLASS_EMERGENCY_FUND,
MAX_CHART_ITEMS,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
@ -35,7 +35,8 @@ import {
PortfolioReport,
PortfolioSummary,
Position,
TimelinePosition
TimelinePosition,
UserSettings
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type {
@ -49,8 +50,11 @@ import type {
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import {
Account,
AssetClass,
DataSource,
Order,
Platform,
Prisma,
Tag,
Type as TypeOfOrder
@ -105,7 +109,8 @@ export class PortfolioService {
public async getAccounts(
aUserId: string,
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId };
@ -119,10 +124,16 @@ export class PortfolioService {
include: { Order: true, Platform: true },
orderBy: { name: 'asc' }
}),
this.getDetails(aUserId, aUserId, undefined, aFilters)
this.getDetails(
aUserId,
aUserId,
undefined,
aFilters,
withExcludedAccounts
)
]);
const userCurrency = this.request.user.Settings.currency;
const userCurrency = this.request.user.Settings.settings.baseCurrency;
return accounts.map((account) => {
let transactionCount = 0;
@ -159,9 +170,14 @@ export class PortfolioService {
public async getAccountsWithAggregations(
aUserId: string,
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters);
const accounts = await this.getAccounts(
aUserId,
aFilters,
withExcludedAccounts
);
let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0;
@ -197,7 +213,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 +293,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,11 +370,63 @@ export class PortfolioService {
};
}
public async getChartV2({
dateRange = 'max',
impersonationId
}: {
dateRange?: DateRange;
impersonationId: string;
}): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
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(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)
);
const items = await portfolioCalculator.getChartData(
startDate,
endDate,
step
);
return {
items,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
public async getDetails(
aImpersonationId: string,
aUserId: string,
aDateRange: DateRange = 'max',
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId });
@ -367,13 +435,14 @@ 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 } =
await this.getTransactionPoints({
userId,
withExcludedAccounts,
filters: aFilters
});
@ -400,12 +469,21 @@ export class PortfolioService {
});
const holdings: PortfolioDetails['holdings'] = {};
const totalInvestment = currentPositions.totalInvestment.plus(
cashDetails.balanceInBaseCurrency
);
const totalValue = currentPositions.currentValue.plus(
const totalInvestmentInBaseCurrency = currentPositions.totalInvestment.plus(
cashDetails.balanceInBaseCurrency
);
let filteredValueInBaseCurrency = currentPositions.currentValue;
if (
aFilters?.length === 0 ||
(aFilters?.length === 1 &&
aFilters[0].type === 'ASSET_CLASS' &&
aFilters[0].id === 'CASH')
) {
filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus(
cashDetails.balanceInBaseCurrency
);
}
const dataGatheringItems = currentPositions.positions.map((position) => {
return {
@ -466,10 +544,12 @@ export class PortfolioService {
holdings[item.symbol] = {
markets,
allocationCurrent: totalValue.eq(0)
allocationCurrent: filteredValueInBaseCurrency.eq(0)
? 0
: value.div(totalValue).toNumber(),
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
: value.div(filteredValueInBaseCurrency).toNumber(),
allocationInvestment: item.investment
.div(totalInvestmentInBaseCurrency)
.toNumber(),
assetClass: symbolProfile.assetClass,
assetSubClass: symbolProfile.assetSubClass,
countries: symbolProfile.countries,
@ -503,8 +583,8 @@ export class PortfolioService {
cashDetails,
emergencyFund,
userCurrency,
investment: totalInvestment,
value: totalValue
investment: totalInvestmentInBaseCurrency,
value: filteredValueInBaseCurrency
});
for (const symbol of Object.keys(cashPositions)) {
@ -517,10 +597,23 @@ export class PortfolioService {
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts,
filters: aFilters
});
return { accounts, holdings, hasErrors: currentPositions.hasErrors };
const summary = await this.getSummary(aImpersonationId);
return {
accounts,
holdings,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
: 0,
hasErrors: currentPositions.hasErrors,
totalValueInBaseCurrency: summary.netWorth
};
}
public async getPosition(
@ -528,11 +621,15 @@ 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 = (
await this.orderService.getOrders({ userCurrency, userId })
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ SymbolProfile }) => {
return (
SymbolProfile.dataSource === aDataSource &&
@ -781,7 +878,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 +954,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
});
@ -916,8 +1013,107 @@ export class PortfolioService {
};
}
public async getPerformanceV2({
dateRange = 'max',
impersonationId
}: {
dateRange?: DateRange;
impersonationId: string;
}): Promise<PortfolioPerformanceResponse> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
if (transactionPoints?.length <= 0) {
return {
chart: [],
hasErrors: false,
performance: {
currentGrossPerformance: 0,
currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0
}
};
}
portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate
);
const hasErrors = currentPositions.hasErrors;
const currentValue = currentPositions.currentValue.toNumber();
const currentGrossPerformance = currentPositions.grossPerformance;
const currentGrossPerformancePercent =
currentPositions.grossPerformancePercentage;
let currentNetPerformance = currentPositions.netPerformance;
let currentNetPerformancePercent =
currentPositions.netPerformancePercentage;
// if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1);
// }
// if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentNetPerformancePercent = currentNetPerformancePercent.mul(-1);
// }
const historicalDataContainer = await this.getChartV2({
dateRange,
impersonationId
});
const itemOfToday = historicalDataContainer.items.find((item) => {
return item.date === format(new Date(), DATE_FORMAT);
});
if (itemOfToday) {
currentNetPerformance = new Big(itemOfToday.netPerformance);
currentNetPerformancePercent = new Big(
itemOfToday.netPerformanceInPercentage
).div(100);
}
return {
chart: historicalDataContainer.items.map(
({ date, netPerformanceInPercentage }) => {
return {
date,
value: netPerformanceInPercentage
};
}
),
errors: currentPositions.errors,
hasErrors: currentPositions.hasErrors || hasErrors,
performance: {
currentValue,
currentGrossPerformance: currentGrossPerformance.toNumber(),
currentGrossPerformancePercent:
currentGrossPerformancePercent.toNumber(),
currentNetPerformance: currentNetPerformance.toNumber(),
currentNetPerformancePercent: currentNetPerformancePercent.toNumber()
}
};
}
public async getReport(impersonationId: string): Promise<PortfolioReport> {
const currency = this.request.user.Settings.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 +1167,7 @@ export class PortfolioService {
accounts
)
],
{ baseCurrency: currency }
<UserSettings>this.request.user.Settings.settings
),
currencyClusterRisk: await this.rulesService.evaluate(
[
@ -992,7 +1188,7 @@ export class PortfolioService {
currentPositions
)
],
{ baseCurrency: currency }
<UserSettings>this.request.user.Settings.settings
),
fees: await this.rulesService.evaluate(
[
@ -1002,80 +1198,12 @@ 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 userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getCashPositions({
cashDetails,
emergencyFund,
@ -1183,7 +1311,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 +1330,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 +1352,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(
@ -1251,27 +1379,131 @@ export class PortfolioService {
return portfolioStart;
}
private async getSummary(
aImpersonationId: string
): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const excludedActivities = (
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ Account: account }) => {
return account?.isExcluded ?? false;
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const totalOfExcludedActivities = new Big(
this.getTotalByType(excludedActivities, userCurrency, 'BUY')
).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL'));
const cashDetailsWithExcludedAccounts =
await this.accountService.getCashDetails({
userId,
currency: userCurrency,
withExcludedAccounts: true
});
const excludedBalanceInBaseCurrency = new Big(
cashDetailsWithExcludedAccounts.balanceInBaseCurrency
).minus(balanceInBaseCurrency);
const excludedAccountsAndActivities = excludedBalanceInBaseCurrency
.plus(totalOfExcludedActivities)
.toNumber();
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.plus(excludedAccountsAndActivities)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
excludedAccountsAndActivities,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getTransactionPoints({
filters,
includeDrafts = false,
userId
userId,
withExcludedAccounts
}: {
filters?: Filter[];
includeDrafts?: boolean;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<{
transactionPoints: TransactionPoint[];
orders: OrderWithAccount[];
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,
includeDrafts,
userCurrency,
userId,
withExcludedAccounts,
types: ['BUY', 'SELL']
});
@ -1323,17 +1555,22 @@ export class PortfolioService {
orders,
portfolioItemsNow,
userCurrency,
userId
userId,
withExcludedAccounts
}: {
filters?: Filter[];
orders: OrderWithAccount[];
portfolioItemsNow: { [p: string]: TimelinePosition };
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}) {
const accounts: PortfolioDetails['accounts'] = {};
let currentAccounts = [];
let currentAccounts: (Account & {
Order?: Order[];
Platform?: Platform;
})[] = [];
if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId);
@ -1353,6 +1590,10 @@ export class PortfolioService {
});
}
currentAccounts = currentAccounts.filter((account) => {
return withExcludedAccounts || account.isExcluded === false;
});
for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id;

View File

@ -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) => {

View File

@ -12,7 +12,10 @@ import { RedisCacheService } from './redis-cache.service';
inject: [ConfigurationService],
useFactory: async (configurationService: ConfigurationService) => {
return <CacheManagerOptions>{
host: configurationService.get('REDIS_HOST'),
max: configurationService.get('MAX_ITEM_IN_CACHE'),
password: configurationService.get('REDIS_PASSWORD'),
port: configurationService.get('REDIS_PORT'),
store: redisStore,
ttl: configurationService.get('CACHE_TTL')
};

View File

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

View File

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

View File

@ -1,10 +1,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;
}

View File

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

View File

@ -1,7 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { 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);
}
}

View File

@ -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 = [];

View File

@ -41,6 +41,14 @@ export class RedactValuesInResponseInterceptor<T>
return activity;
});
}
if (data.filteredValueInBaseCurrency) {
data.filteredValueInBaseCurrency = null;
}
if (data.totalValueInBaseCurrency) {
data.totalValueInBaseCurrency = null;
}
}
return data;

View File

@ -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);
}

View File

@ -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';

View File

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

View File

@ -1,8 +1,7 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { 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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -39,9 +39,9 @@ export class ConfigurationService {
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
PORT: port({ default: 3333 }),
RAKUTEN_RAPID_API_KEY: str({ default: '' }),
REDIS_HOST: host(),
REDIS_HOST: host({ default: 'localhost' }),
REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port(),
REDIS_PORT: port({ default: 6379 }),
ROOT_URL: str({ default: 'http://localhost:4200' }),
STRIPE_PUBLIC_KEY: str({ default: '' }),
STRIPE_SECRET_KEY: str({ default: '' }),

View File

@ -6,6 +6,7 @@ import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
@ -90,7 +91,7 @@ export class YahooFinanceService implements DataProviderInterface {
try {
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
const assetProfile = await yahooFinance.quoteSummary(symbol, {
modules: ['price', 'summaryProfile']
modules: ['price', 'summaryProfile', 'topHoldings']
});
const { assetClass, assetSubClass } = this.parseAssetClass(
@ -109,7 +110,16 @@ export class YahooFinanceService implements DataProviderInterface {
});
response.symbol = aSymbol;
if (
if (assetSubClass === AssetSubClass.MUTUALFUND) {
response.sectors = [];
for (const sectorWeighting of assetProfile.topHoldings
?.sectorWeightings ?? []) {
for (const [sector, weight] of Object.entries(sectorWeighting)) {
response.sectors.push({ weight, name: this.parseSector(sector) });
}
}
} else if (
assetSubClass === AssetSubClass.STOCK &&
assetProfile.summaryProfile?.country
) {
@ -183,10 +193,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 +256,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 +269,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 +283,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(),
@ -434,4 +447,46 @@ export class YahooFinanceService implements DataProviderInterface {
return { assetClass, assetSubClass };
}
private parseSector(aString: string): string {
let sector = UNKNOWN_KEY;
switch (aString) {
case 'basic_materials':
sector = 'Basic Materials';
break;
case 'communication_services':
sector = 'Communication Services';
break;
case 'consumer_cyclical':
sector = 'Consumer Cyclical';
break;
case 'consumer_defensive':
sector = 'Consumer Staples';
break;
case 'energy':
sector = 'Energy';
break;
case 'financial_services':
sector = 'Financial Services';
break;
case 'healthcare':
sector = 'Healthcare';
break;
case 'industrials':
sector = 'Industrials';
break;
case 'realestate':
sector = 'Real Estate';
break;
case 'technology':
sector = 'Technology';
break;
case 'utilities':
sector = 'Utilities';
break;
}
return sector;
}
}

View File

@ -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'],

View File

@ -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
*/

View File

@ -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';

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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
}
});
}

View File

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

View File

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

View File

@ -0,0 +1,55 @@
<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 without-hint"
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="my-2 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoading"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
<div class="chart-container">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '100%',
width: '100%'
}"
></ngx-skeleton-loader>
<canvas
#chartCanvas
class="h-100"
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
></canvas>
</div>

View File

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

View File

@ -0,0 +1,220 @@
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, 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: 'x',
position: <unknown>'top',
xAlign: 'center',
yAlign: 'bottom'
};
}
}

View File

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

View File

@ -5,10 +5,6 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { 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;

View File

@ -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>

View File

@ -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,23 +72,28 @@ 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 &&
this.user.settings.viewMode !== 'ZEN';
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() {
@ -103,26 +102,14 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
}
private update() {
this.historicalDataItems = null;
this.isLoadingPerformance = true;
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.fetchPortfolioPerformance({
range: this.user?.settings?.dateRange,
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.errors = response.errors;
@ -130,6 +117,36 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.performance = response.performance;
this.isLoadingPerformance = false;
if (this.user?.settings?.isExperimentalFeatures) {
this.historicalDataItems = response.chart.map(({ date, value }) => {
return {
date,
value
};
});
} else {
this.dataService
.fetchChart({
range: this.user?.settings?.dateRange,
version: 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map(
({ date, value }) => {
return {
date,
value
};
}
);
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
});

View File

@ -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)"

View File

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

View File

@ -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',

View File

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

View File

@ -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';

View File

@ -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);

View File

@ -108,7 +108,6 @@
<div class="row">
<div class="col-xs-12 col-md-4 my-2">
<gf-value
i18n
size="large"
subLabel="(Last 24 hours)"
[value]="statistics?.activeUsers1d ?? '-'"
@ -117,7 +116,6 @@
</div>
<div class="col-xs-12 col-md-4 my-2">
<gf-value
i18n
size="large"
subLabel="(Last 30 days)"
[value]="statistics?.newUsers30d ?? '-'"
@ -126,7 +124,6 @@
</div>
<div class="col-xs-12 col-md-4 my-2">
<gf-value
i18n
size="large"
subLabel="(Last 30 days)"
[value]="statistics?.activeUsers30d ?? '-'"
@ -139,7 +136,6 @@
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
>
<gf-value
i18n
size="large"
[value]="statistics?.slackCommunityUsers ?? '-'"
>Users in Slack community</gf-value
@ -152,7 +148,6 @@
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
>
<gf-value
i18n
size="large"
[value]="statistics?.gitHubContributors ?? '-'"
>Contributors on GitHub</gf-value
@ -165,7 +160,6 @@
href="https://github.com/ghostfolio/ghostfolio/stargazers"
>
<gf-value
i18n
size="large"
[value]="statistics?.gitHubStargazers ?? '-'"
>Stars on GitHub</gf-value

View File

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

View File

@ -94,12 +94,15 @@
<ng-container i18n>Base Currency</ng-container>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.baseCurrency"
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)"
(selectionChange)="onChangeUserSetting('baseCurrency', $event.value)"
>
<mat-option
*ngFor="let currency of currencies"
@ -116,7 +119,10 @@
<div class="hint-text text-muted" i18n>Beta</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -126,6 +132,9 @@
<mat-option [value]="null"></mat-option>
<mat-option value="de">Deutsch</mat-option>
<mat-option value="en">English</mat-option>
<mat-option value="es">Español</mat-option>
<mat-option value="it">Italiano</mat-option>
<mat-option value="nl">Nederlands</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -138,7 +147,10 @@
</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="locale"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -161,12 +173,15 @@
</div>
<div class="pl-1 w-50">
<div class="align-items-center d-flex overflow-hidden">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="viewMode"
[disabled]="!hasPermissionToUpdateViewMode"
[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 +203,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,30 @@
></gf-activities-filter>
</div>
</div>
<div class="row">
<div class="col">
<mat-card class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n
>Proportion of Net Worth</mat-card-title
>
<gf-value
class="align-items-end flex-grow-1 ml-2"
size="medium"
[isPercent]="true"
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
></gf-value>
</mat-card-header>
<mat-card-content>
<mat-progress-bar
mode="determinate"
[title]="(portfolioDetails?.filteredValueInPercentage * 100).toFixed(2) + '%'"
[value]="portfolioDetails?.filteredValueInPercentage * 100"
></mat-progress-bar>
</mat-card-content>
</mat-card>
</div>
</div>
<div class="proportion-charts row">
<div class="col-md-4">
<mat-card class="mb-3">

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
@ -22,7 +23,8 @@ import { AllocationsPageComponent } from './allocations-page.component';
GfToggleModule,
GfWorldMapChartModule,
GfValueModule,
MatCardModule
MatCardModule,
MatProgressBarModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

View File

@ -20,6 +20,7 @@
::ng-deep {
.mat-card-header-text {
flex: 1 1 auto;
overflow: hidden;
}
}
@ -28,4 +29,33 @@
}
}
}
.mat-progress-bar {
border-radius: 0.25rem;
height: 0.5rem;
::ng-deep {
.mat-progress-bar-background {
fill: rgb(var(--palette-background-unselected-chip));
}
.mat-progress-bar-buffer {
background-color: rgb(var(--palette-background-unselected-chip));
}
}
}
}
:host-context(.is-dark-theme) {
.mat-progress-bar {
::ng-deep {
.mat-progress-bar-background {
fill: rgb(var(--palette-background-unselected-chip-dark));
}
.mat-progress-bar-buffer {
background-color: rgb(var(--palette-background-unselected-chip-dark));
}
}
}
}

View File

@ -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,82 @@ 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
.fetchPortfolioPerformance({
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 +180,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;
}
}
}

View File

@ -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>

View File

@ -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,

View File

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

View File

@ -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

View File

@ -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';

View File

@ -12,13 +12,14 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { 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,
@ -30,13 +31,13 @@ import {
PortfolioPerformanceResponse,
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 +182,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 }
});
}
@ -285,7 +301,11 @@ export class DataService {
);
}
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
public fetchPortfolioDetails({
filters
}: {
filters?: Filter[];
}): Observable<PortfolioDetails> {
let params = new HttpParams();
if (filters?.length > 0) {
@ -331,17 +351,32 @@ export class DataService {
}
}
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
params
});
return this.http
.get<any>('/api/v1/portfolio/details', {
params
})
.pipe(
map((response) => {
if (response.summary?.firstOrderDate) {
response.summary.firstOrderDate = parseISO(
response.summary.firstOrderDate
);
}
return response;
})
);
}
public fetchPortfolioPerformance(params: { [param: string]: any }) {
public fetchPortfolioPerformance({
range,
version
}: {
range: DateRange;
version: number;
}) {
return this.http.get<PortfolioPerformanceResponse>(
'/api/v1/portfolio/performance',
{
params
}
`/api/v${version}/portfolio/performance`,
{ params: { range } }
);
}
@ -355,18 +390,6 @@ export class DataService {
return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
}
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
return this.http.get<any>('/api/v1/portfolio/summary').pipe(
map((summary) => {
if (summary.firstOrderDate) {
summary.firstOrderDate = parseISO(summary.firstOrderDate);
}
return summary;
})
);
}
public fetchPositionDetail({
dataSource,
symbol
@ -430,10 +453,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

View File

@ -15,7 +15,7 @@
</trans-unit>
<trans-unit id="ccb2a809018b32a96c813ae69126ce05976109ce" datatype="html">
<source>The risk of loss in trading can be substantial. It is not advisable to invest money you may need in the short term.</source>
<target state="translated">Das Ausfallrisiko beim Börsenhandel kann erheblich sein. Es ist nicht ratsam, Geld zu investieren, welches sie kurzfristig benötigen.</target>
<target state="translated">Das Ausfallrisiko beim Börsenhandel kann erheblich sein. Es ist nicht ratsam, Geld zu investieren, welches Sie kurzfristig benötigen.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/app.component.html</context>
<context context-type="linenumber">55,56</context>
@ -38,7 +38,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">28</context>
<context context-type="linenumber">31</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -86,7 +86,7 @@
<target state="translated">Aktivitäten</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">33</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
@ -210,7 +210,7 @@
<target state="translated">Jobs löschen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
@ -218,7 +218,7 @@
<target state="translated">Symbol</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">29</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -238,7 +238,7 @@
<target state="translated">Datenquelle</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">33</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -254,7 +254,7 @@
<target state="translated">Versuche</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">31</context>
<context context-type="linenumber">34</context>
</context-group>
</trans-unit>
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
@ -262,7 +262,7 @@
<target state="translated">Erstellt</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">32</context>
<context context-type="linenumber">35</context>
</context-group>
</trans-unit>
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
@ -270,7 +270,7 @@
<target state="translated">Abgeschlossen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">33</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
@ -278,7 +278,7 @@
<target state="translated">Status</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
@ -286,7 +286,7 @@
<target state="translated">Anlageprofil</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">49</context>
<context context-type="linenumber">52</context>
</context-group>
</trans-unit>
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
@ -294,7 +294,7 @@
<target state="translated">Historische Marktdaten</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">54</context>
<context context-type="linenumber">57</context>
</context-group>
</trans-unit>
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
@ -302,7 +302,7 @@
<target state="translated">Daten anzeigen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
@ -310,7 +310,7 @@
<target state="translated">Stacktrace anzeigen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
@ -318,7 +318,7 @@
<target state="translated">Job löschen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">119</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
@ -370,7 +370,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">66</context>
<context context-type="linenumber">74</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -394,7 +394,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">81</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -574,7 +574,7 @@
<target state="translated">Hinzufügen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">187</context>
</context-group>
</trans-unit>
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
@ -582,7 +582,7 @@
<target state="translated">Verwaltung</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">190</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
@ -590,7 +590,7 @@
<target state="translated">Cache leeren</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">194</context>
<context context-type="linenumber">198</context>
</context-group>
</trans-unit>
<trans-unit id="2817099043823177227" datatype="html">
@ -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">
@ -1031,10 +1031,10 @@
</trans-unit>
<trans-unit id="67251f04518ae452230c68a748b3fa2838b4db74" datatype="html">
<source>Net Worth</source>
<target state="translated">Reinvermögen</target>
<target state="translated">Gesamtvermögen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">179</context>
<context context-type="linenumber">190</context>
</context-group>
</trans-unit>
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
@ -1042,7 +1042,7 @@
<target state="translated">Performance pro Jahr</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">190</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
@ -1050,7 +1050,7 @@
<target state="translated">Dividenden</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">206</context>
<context context-type="linenumber">217</context>
</context-group>
</trans-unit>
<trans-unit id="6785405835169448749" 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">
@ -1172,6 +1172,10 @@
<trans-unit id="3041670542776846470" datatype="html">
<source>This feature requires a subscription.</source>
<target state="translated">Diese Funktion erfordert ein Abonnement.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">112</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">67</context>
@ -1180,6 +1184,10 @@
<trans-unit id="5499742151525073097" datatype="html">
<source>Upgrade Plan</source>
<target state="translated">Abonnement abschliessen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">69</context>
@ -1262,7 +1270,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">225</context>
</context-group>
</trans-unit>
<trans-unit id="4420880039966769543" datatype="html">
@ -1270,7 +1278,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">235</context>
</context-group>
</trans-unit>
<trans-unit id="4819099731531004979" datatype="html">
@ -1278,7 +1286,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">247</context>
</context-group>
</trans-unit>
<trans-unit id="7967484035994732534" datatype="html">
@ -1286,7 +1294,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">248</context>
</context-group>
</trans-unit>
<trans-unit id="7963559562180316948" datatype="html">
@ -1294,7 +1302,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">294</context>
</context-group>
</trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1323,7 +1331,7 @@
</trans-unit>
<trans-unit id="f147d0f7f965cccee2e77294cba8e1b88021fa08" datatype="html">
<source>Upgrade</source>
<target state="new">Upgrade</target>
<target state="translated">Upgrade</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">37</context>
@ -1379,10 +1387,10 @@
</trans-unit>
<trans-unit id="6b939b00e8481ed8aa8a24d8add7a209d7116759" datatype="html">
<source>Locale</source>
<target state="new">Locale</target>
<target state="translated">Lokalität</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">144</context>
</context-group>
</trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
@ -1390,7 +1398,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">146</context>
</context-group>
</trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
@ -1398,7 +1406,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">172</context>
</context-group>
</trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
@ -1406,7 +1414,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">196</context>
</context-group>
</trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
@ -1414,7 +1422,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">223</context>
</context-group>
</trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
@ -1422,7 +1430,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">232</context>
</context-group>
</trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1516,6 +1524,10 @@
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
<source>Platform</source>
<target state="translated">Plattform</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">29</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
<context context-type="linenumber">35</context>
@ -1530,7 +1542,7 @@
<target state="translated">Konto ID</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">55</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4979019387603946865" datatype="html">
@ -1618,7 +1630,7 @@
<target state="translated">Nach Konto</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">17</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
@ -1626,7 +1638,7 @@
<target state="translated">Nach Währung</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">42</context>
<context context-type="linenumber">66</context>
</context-group>
</trans-unit>
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
@ -1634,7 +1646,7 @@
<target state="translated">Nach Asset Class</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">70</context>
<context context-type="linenumber">94</context>
</context-group>
</trans-unit>
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
@ -1642,7 +1654,7 @@
<target state="translated">Nach Position</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">98</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
@ -1650,7 +1662,7 @@
<target state="translated">Nach Sektor</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit>
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
@ -1658,7 +1670,7 @@
<target state="translated">Nach Kontinent</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">155</context>
<context context-type="linenumber">179</context>
</context-group>
</trans-unit>
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
@ -1666,7 +1678,7 @@
<target state="translated">Nach Land</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">207</context>
</context-group>
</trans-unit>
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
@ -1674,7 +1686,7 @@
<target state="translated">Regionen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">238</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -1694,7 +1706,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 +1718,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">105</context>
</context-group>
</trans-unit>
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
@ -1714,7 +1726,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">26</context>
</context-group>
</trans-unit>
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
@ -1722,7 +1734,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">62</context>
</context-group>
</trans-unit>
<trans-unit id="5857197365507636437" datatype="html">
@ -2036,6 +2048,10 @@
<trans-unit id="9201103587777813545" datatype="html">
<source>Portfolio</source>
<target state="translated">Portfolio</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">107</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
<context context-type="linenumber">12</context>
@ -2059,7 +2075,7 @@
</trans-unit>
<trans-unit id="a3d148b40a389fda0665eb583c9e434ec5ee1ced" datatype="html">
<source> Ghostfolio empowers you to keep track of your wealth. </source>
<target state="new"/>
<target state="translated">Ghostfolio verschafft Ihnen den Überblick über Ihr Vermögen.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
<context context-type="linenumber">132,134</context>
@ -2268,9 +2284,13 @@
<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">5</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>
<context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
@ -2286,7 +2306,7 @@
<target state="translated">Sprache</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">115</context>
<context context-type="linenumber">118</context>
</context-group>
</trans-unit>
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
@ -2410,7 +2430,7 @@
<target state="translated">Entwickelte Länder</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">264</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2422,7 +2442,7 @@
<target state="translated">Schwellenländer</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">249</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2434,7 +2454,7 @@
<target state="translated">Andere Länder</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">258</context>
<context context-type="linenumber">282</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2449,50 +2469,6 @@
<context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit id="1de491c923555d6422bc6f1146357eb2b47853da" datatype="html">
<source>Active Users</source>
<target state="new">Active Users</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">115</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">133</context>
</context-group>
</trans-unit>
<trans-unit id="8c4cfd77b7b3d7917de13bec98a8a74890f95618" datatype="html">
<source>New Users</source>
<target state="new">New Users</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
<trans-unit id="c0eb011366e597e23542be386e8bc0d53470b520" datatype="html">
<source>Users in Slack community</source>
<target state="new">Users in Slack community</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">145</context>
</context-group>
</trans-unit>
<trans-unit id="be99161cc904867871ab172df77b736d3b27dfc5" datatype="html">
<source>Contributors on GitHub</source>
<target state="new">Contributors on GitHub</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="8d3932a9eba50bc101c2b8c329e7b4ea033cde97" datatype="html">
<source>Stars on GitHub</source>
<target state="new">Stars on GitHub</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">171</context>
</context-group>
</trans-unit>
<trans-unit id="e34e2478d2d30c9d01758d01b7212411171b9bd5" datatype="html">
<source>Projected Total Amount</source>
<target state="translated">Projizierter Gesamtbetrag</target>
@ -2503,7 +2479,7 @@
</trans-unit>
<trans-unit id="2937311350146031865" datatype="html">
<source>Initial</source>
<target state="new">Beginn</target>
<target state="translated">Beginn</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts</context>
<context context-type="linenumber">57</context>
@ -2522,7 +2498,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">38</context>
</context-group>
</trans-unit>
<trans-unit id="1975246224413290232" datatype="html">
@ -2530,7 +2506,7 @@
<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">39</context>
</context-group>
</trans-unit>
<trans-unit id="5213771062241898526" datatype="html">
@ -2538,7 +2514,7 @@
<target state="translated">Einlage</target>
<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">132</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
@ -2555,7 +2531,7 @@
</trans-unit>
<trans-unit id="1054498214311181686" datatype="html">
<source>Savings</source>
<target state="new">Ersparnisse</target>
<target state="translated">Ersparnisse</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
<context context-type="linenumber">296</context>
@ -2598,7 +2574,7 @@
<target state="translated">Filtern nach...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">128</context>
</context-group>
</trans-unit>
<trans-unit id="2078421919111943467" datatype="html">
@ -2641,6 +2617,62 @@
<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">211</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">4</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">18</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">116</context>
</context-group>
</trans-unit>
<trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html">
<source>Proportion of Net Worth</source>
<target state="translated">Anteil am Gesamtvermögen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="ac598d664f86ba5783915d65f2664a7f38a9d23a" datatype="html">
<source>Account Type</source>
<target state="new">Account Type</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="98fc3013bfcbf452b9f37bbfcdb77b9b882866e3" datatype="html">
<source>Excluded from Analysis</source>
<target state="new">Excluded from Analysis</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">176</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -35,7 +35,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">28</context>
<context context-type="linenumber">31</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -79,7 +79,7 @@
<source>Activities</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">33</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
@ -196,14 +196,14 @@
<source>Delete Jobs</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
<source>Symbol</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">29</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -222,7 +222,7 @@
<source>Data Source</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">30</context>
<context context-type="linenumber">33</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -237,63 +237,63 @@
<source>Attempts</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">31</context>
<context context-type="linenumber">34</context>
</context-group>
</trans-unit>
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
<source>Created</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">32</context>
<context context-type="linenumber">35</context>
</context-group>
</trans-unit>
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
<source>Finished</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">33</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
<source>Status</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
<source>Asset Profile</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">49</context>
<context context-type="linenumber">52</context>
</context-group>
</trans-unit>
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
<source>Historical Market Data</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">54</context>
<context context-type="linenumber">57</context>
</context-group>
</trans-unit>
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
<source>View Data</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
<source>View Stacktrace</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
<source>Delete Job</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">119</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
@ -341,7 +341,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">66</context>
<context context-type="linenumber">74</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -364,7 +364,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">81</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -523,21 +523,21 @@
<source>Add</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">187</context>
</context-group>
</trans-unit>
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
<source>Housekeeping</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">190</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
<source>Flush Cache</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">194</context>
<context context-type="linenumber">198</context>
</context-group>
</trans-unit>
<trans-unit id="2817099043823177227" datatype="html">
@ -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">
@ -936,28 +936,28 @@
<source>Net Worth</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">179</context>
<context context-type="linenumber">190</context>
</context-group>
</trans-unit>
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
<source>Annualized Performance</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">190</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
<source>Dividend</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">206</context>
<context context-type="linenumber">217</context>
</context-group>
</trans-unit>
<trans-unit id="6785405835169448749" datatype="html">
<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">
@ -1058,6 +1058,10 @@
</trans-unit>
<trans-unit id="3041670542776846470" datatype="html">
<source>This feature requires a subscription.</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">112</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">67</context>
@ -1065,6 +1069,10 @@
</trans-unit>
<trans-unit id="5499742151525073097" datatype="html">
<source>Upgrade Plan</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">69</context>
@ -1137,35 +1145,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">225</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">235</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">247</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">248</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">294</context>
</context-group>
</trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1243,42 +1251,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">144</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">146</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">172</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">196</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">223</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">232</context>
</context-group>
</trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1362,6 +1370,10 @@
</trans-unit>
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
<source>Platform</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">29</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
<context context-type="linenumber">35</context>
@ -1375,7 +1387,7 @@
<source>Account ID</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">55</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4979019387603946865" datatype="html">
@ -1453,56 +1465,56 @@
<source>By Account</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">17</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
<source>By Currency</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">42</context>
<context context-type="linenumber">66</context>
</context-group>
</trans-unit>
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
<source>By Asset Class</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">70</context>
<context context-type="linenumber">94</context>
</context-group>
</trans-unit>
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
<source>By Holding</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">98</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
<source>By Sector</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit>
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
<source>By Continent</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">155</context>
<context context-type="linenumber">179</context>
</context-group>
</trans-unit>
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
<source>By Country</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">207</context>
</context-group>
</trans-unit>
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
<source>Regions</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">238</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -1520,7 +1532,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 +1543,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">105</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">26</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">62</context>
</context-group>
</trans-unit>
<trans-unit id="5857197365507636437" datatype="html">
@ -1824,6 +1836,10 @@
</trans-unit>
<trans-unit id="9201103587777813545" datatype="html">
<source>Portfolio</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">107</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
<context context-type="linenumber">12</context>
@ -2027,9 +2043,13 @@
</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">5</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>
<context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
@ -2043,7 +2063,7 @@
<source>Language</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">115</context>
<context context-type="linenumber">118</context>
</context-group>
</trans-unit>
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
@ -2096,7 +2116,7 @@
<source>Developed Markets</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">264</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2136,7 +2156,7 @@
<source>Other Markets</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">258</context>
<context context-type="linenumber">282</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2147,7 +2167,7 @@
<source>Emerging Markets</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">249</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2189,45 +2209,6 @@
<context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit id="1de491c923555d6422bc6f1146357eb2b47853da" datatype="html">
<source>Active Users</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">115</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">133</context>
</context-group>
</trans-unit>
<trans-unit id="8c4cfd77b7b3d7917de13bec98a8a74890f95618" datatype="html">
<source>New Users</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
<trans-unit id="8d3932a9eba50bc101c2b8c329e7b4ea033cde97" datatype="html">
<source>Stars on GitHub</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">171</context>
</context-group>
</trans-unit>
<trans-unit id="be99161cc904867871ab172df77b736d3b27dfc5" datatype="html">
<source>Contributors on GitHub</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="c0eb011366e597e23542be386e8bc0d53470b520" datatype="html">
<source>Users in Slack community</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/about-page.html</context>
<context context-type="linenumber">145</context>
</context-group>
</trans-unit>
<trans-unit id="e34e2478d2d30c9d01758d01b7212411171b9bd5" datatype="html">
<source>Projected Total Amount</source>
<context-group purpose="location">
@ -2246,7 +2227,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">39</context>
</context-group>
</trans-unit>
<trans-unit id="2937311350146031865" datatype="html">
@ -2267,7 +2248,7 @@
<source>Deposit</source>
<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">132</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
@ -2285,7 +2266,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">38</context>
</context-group>
</trans-unit>
<trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html">
@ -2331,7 +2312,7 @@
<source>Filter by...</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">128</context>
</context-group>
</trans-unit>
<trans-unit id="303469635941752458" datatype="html">
@ -2359,6 +2340,55 @@
<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">211</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">116</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">4</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">18</context>
</context-group>
</trans-unit>
<trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html">
<source>Proportion of Net Worth</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="98fc3013bfcbf452b9f37bbfcdb77b9b882866e3" datatype="html">
<source>Excluded from Analysis</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">176</context>
</context-group>
</trans-unit>
<trans-unit id="ac598d664f86ba5783915d65f2664a7f38a9d23a" datatype="html">
<source>Account Type</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

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

View File

@ -6,6 +6,7 @@ services:
- ../.env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=prefer
NODE_ENV: production
REDIS_HOST: 'redis'
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:

View File

@ -6,6 +6,7 @@ services:
- ../.env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=prefer
NODE_ENV: production
REDIS_HOST: 'redis'
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:

View File

@ -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);
}

View File

@ -67,6 +67,8 @@ export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = {
}
};
export const MAX_CHART_ITEMS = 365;
export const PROPERTY_BENCHMARKS = 'BENCHMARKS';
export const PROPERTY_COUPONS = 'COUPONS';
export const PROPERTY_CURRENCIES = 'CURRENCIES';

View File

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

View File

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

View File

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

View File

@ -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,

Some files were not shown because too many files have changed in this diff Show More