Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
5c9fa71d95 | |||
fefbfa31d1 | |||
93a1fae51c | |||
3715edd9ba | |||
e3916e1ba3 | |||
76ceac4edc | |||
333b63bfe2 | |||
3006c21b12 | |||
f01a3f893d | |||
72974e888f | |||
0cee7a0b35 | |||
f3d337b044 | |||
7667af059c | |||
1095b47f45 | |||
dacd7271eb | |||
e093041184 | |||
8f2caa508a | |||
862f670ccf | |||
54bf4c7a43 | |||
c0ace51ee9 | |||
b1b5689242 | |||
b68cdaf8ea | |||
b387a80a0d | |||
6e4660295a | |||
d4c3a9d1e8 | |||
263f6b32f2 | |||
637f31ae3b | |||
547e27c7a1 | |||
f10dc176f2 | |||
0a966e46cd | |||
4f281d25e1 | |||
aaba8c35c2 | |||
7d27cb3398 | |||
91678028b5 | |||
5e3cac8ac9 | |||
33f20b6b48 | |||
e4fd255dd7 | |||
e320aa91f7 | |||
0fcfa6c1bd | |||
42d32ed652 | |||
21b4b0ef24 | |||
4f8fe83a16 | |||
980ad1028c | |||
0d5bc3f51b | |||
aece76d98f | |||
fc4bb71184 | |||
20bc7ef99c | |||
7a733ae49b | |||
376ce88492 | |||
c4d83aabe7 | |||
d4e2cec77e | |||
75db7bf79a | |||
3ad99c9991 | |||
00e402d286 | |||
4ac0484025 | |||
75d61bff6d | |||
0de28d733e | |||
3b2f13850c | |||
0cc42ffd7c | |||
3ccb812ac3 | |||
0a8549db3e |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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. -->
|
141
CHANGELOG.md
141
CHANGELOG.md
@ -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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## 1.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
|
### Changed
|
||||||
|
|
||||||
@ -16,7 +151,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Fixed
|
### 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
|
- Handled errors in the portfolio calculation if there is no internet connection
|
||||||
- Fixed the _GitHub_ contributors count on the about page
|
- 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
|
### Added
|
||||||
|
|
||||||
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
|
- 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
|
- Resolved the feature graphic of the blog post
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -153,6 +153,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Run `yarn install`
|
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 `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. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
||||||
1. Start the server and the client (see [_Development_](#Development))
|
1. Start the server and the client (see [_Development_](#Development))
|
||||||
|
40
angular.json
40
angular.json
@ -136,6 +136,18 @@
|
|||||||
"baseHref": "/en/",
|
"baseHref": "/en/",
|
||||||
"localize": ["en"]
|
"localize": ["en"]
|
||||||
},
|
},
|
||||||
|
"development-es": {
|
||||||
|
"baseHref": "/es/",
|
||||||
|
"localize": ["es"]
|
||||||
|
},
|
||||||
|
"development-it": {
|
||||||
|
"baseHref": "/it/",
|
||||||
|
"localize": ["it"]
|
||||||
|
},
|
||||||
|
"development-nl": {
|
||||||
|
"baseHref": "/nl/",
|
||||||
|
"localize": ["nl"]
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
{
|
{
|
||||||
@ -180,6 +192,15 @@
|
|||||||
"development-en": {
|
"development-en": {
|
||||||
"browserTarget": "client:build: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": {
|
"production": {
|
||||||
"browserTarget": "client:build:production"
|
"browserTarget": "client:build:production"
|
||||||
}
|
}
|
||||||
@ -191,7 +212,12 @@
|
|||||||
"browserTarget": "client:build",
|
"browserTarget": "client:build",
|
||||||
"includeContext": true,
|
"includeContext": true,
|
||||||
"outputPath": "src/locales",
|
"outputPath": "src/locales",
|
||||||
"targetFiles": ["messages.de.xlf"]
|
"targetFiles": [
|
||||||
|
"messages.de.xlf",
|
||||||
|
"messages.es.xlf",
|
||||||
|
"messages.it.xlf",
|
||||||
|
"messages.nl.xlf"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
@ -214,6 +240,18 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"baseHref": "/de/",
|
"baseHref": "/de/",
|
||||||
"translation": "apps/client/src/locales/messages.de.xlf"
|
"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"
|
"sourceLocale": "en"
|
||||||
|
@ -96,7 +96,9 @@ export class AccountController {
|
|||||||
|
|
||||||
let accountsWithAggregations =
|
let accountsWithAggregations =
|
||||||
await this.portfolioService.getAccountsWithAggregations(
|
await this.portfolioService.getAccountsWithAggregations(
|
||||||
impersonationUserId || this.request.user.id
|
impersonationUserId || this.request.user.id,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -139,7 +141,8 @@ export class AccountController {
|
|||||||
let accountsWithAggregations =
|
let accountsWithAggregations =
|
||||||
await this.portfolioService.getAccountsWithAggregations(
|
await this.portfolioService.getAccountsWithAggregations(
|
||||||
impersonationUserId || this.request.user.id,
|
impersonationUserId || this.request.user.id,
|
||||||
[{ id, type: 'ACCOUNT' }]
|
[{ id, type: 'ACCOUNT' }],
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -107,15 +107,23 @@ export class AccountService {
|
|||||||
public async getCashDetails({
|
public async getCashDetails({
|
||||||
currency,
|
currency,
|
||||||
filters = [],
|
filters = [],
|
||||||
userId
|
userId,
|
||||||
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
currency: string;
|
currency: string;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
userId: string;
|
userId: string;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<CashDetails> {
|
}): Promise<CashDetails> {
|
||||||
let totalCashBalanceInBaseCurrency = new Big(0);
|
let totalCashBalanceInBaseCurrency = new Big(0);
|
||||||
|
|
||||||
const where: Prisma.AccountWhereInput = { userId };
|
const where: Prisma.AccountWhereInput = {
|
||||||
|
userId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (withExcludedAccounts === false) {
|
||||||
|
where.isExcluded = false;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ACCOUNT: filtersByAccount,
|
ACCOUNT: filtersByAccount,
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateIf
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class CreateAccountDto {
|
export class CreateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@ -11,6 +17,10 @@ export class CreateAccountDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isExcluded?: boolean;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateIf
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class UpdateAccountDto {
|
export class UpdateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@ -14,6 +20,10 @@ export class UpdateAccountDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isExcluded?: boolean;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { BenchmarkResponse } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
BenchmarkMarketDataDetails,
|
||||||
|
BenchmarkResponse
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
import { BenchmarkService } from './benchmark.service';
|
import { BenchmarkService } from './benchmark.service';
|
||||||
|
|
||||||
@ -17,4 +28,21 @@ export class BenchmarkController {
|
|||||||
benchmarks: await this.benchmarkService.getBenchmarks()
|
benchmarks: await this.benchmarkService.getBenchmarks()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':dataSource/:symbol/:startDateString')
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getBenchmarkMarketDataBySymbol(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('startDateString') startDateString: string,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<BenchmarkMarketDataDetails> {
|
||||||
|
const startDate = new Date(startDateString);
|
||||||
|
|
||||||
|
return this.benchmarkService.getMarketDataBySymbol({
|
||||||
|
dataSource,
|
||||||
|
startDate,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||||
@ -18,6 +19,7 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
PropertyModule,
|
PropertyModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
|
SymbolModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule
|
||||||
],
|
],
|
||||||
providers: [BenchmarkService]
|
providers: [BenchmarkService]
|
||||||
|
15
apps/api/src/app/benchmark/benchmark.service.spec.ts
Normal file
15
apps/api/src/app/benchmark/benchmark.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { BenchmarkService } from './benchmark.service';
|
||||||
|
|
||||||
|
describe('BenchmarkService', () => {
|
||||||
|
let benchmarkService: BenchmarkService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
benchmarkService = new BenchmarkService(null, null, null, null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculateChangeInPercentage', async () => {
|
||||||
|
expect(benchmarkService.calculateChangeInPercentage(1, 2)).toEqual(1);
|
||||||
|
expect(benchmarkService.calculateChangeInPercentage(2, 2)).toEqual(0);
|
||||||
|
expect(benchmarkService.calculateChangeInPercentage(2, 1)).toEqual(-0.5);
|
||||||
|
});
|
||||||
|
});
|
@ -1,12 +1,23 @@
|
|||||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
import {
|
||||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
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 { Injectable } from '@nestjs/common';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
import { format } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -18,9 +29,18 @@ export class BenchmarkService {
|
|||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly redisCacheService: RedisCacheService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService,
|
||||||
|
private readonly symbolService: SymbolService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public calculateChangeInPercentage(baseValue: number, currentValue: number) {
|
||||||
|
if (baseValue && currentValue) {
|
||||||
|
return new Big(currentValue).div(baseValue).minus(1).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
public async getBenchmarks({ useCache = true } = {}): Promise<
|
public async getBenchmarks({ useCache = true } = {}): Promise<
|
||||||
BenchmarkResponse['benchmarks']
|
BenchmarkResponse['benchmarks']
|
||||||
> {
|
> {
|
||||||
@ -38,47 +58,43 @@ export class BenchmarkService {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const benchmarkAssets: UniqueAsset[] =
|
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles();
|
||||||
((await this.propertyService.getByKey(
|
|
||||||
PROPERTY_BENCHMARKS
|
|
||||||
)) as UniqueAsset[]) ?? [];
|
|
||||||
const promises: Promise<number>[] = [];
|
const promises: Promise<number>[] = [];
|
||||||
|
|
||||||
const [quotes, assetProfiles] = await Promise.all([
|
const quotes = await this.dataProviderService.getQuotes(
|
||||||
this.dataProviderService.getQuotes(benchmarkAssets),
|
benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
||||||
this.symbolProfileService.getSymbolProfiles(benchmarkAssets)
|
return { dataSource, symbol };
|
||||||
]);
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (const benchmarkAsset of benchmarkAssets) {
|
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
||||||
promises.push(this.marketDataService.getMax(benchmarkAsset));
|
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTimeHighs = await Promise.all(promises);
|
const allTimeHighs = await Promise.all(promises);
|
||||||
|
|
||||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
const { marketPrice } =
|
||||||
|
quotes[benchmarkAssetProfiles[index].symbol] ?? {};
|
||||||
|
|
||||||
let performancePercentFromAllTimeHigh = new Big(0);
|
let performancePercentFromAllTimeHigh = 0;
|
||||||
|
|
||||||
if (allTimeHigh) {
|
if (allTimeHigh && marketPrice) {
|
||||||
performancePercentFromAllTimeHigh = new Big(marketPrice)
|
performancePercentFromAllTimeHigh = this.calculateChangeInPercentage(
|
||||||
.div(allTimeHigh)
|
allTimeHigh,
|
||||||
.minus(1);
|
marketPrice
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketCondition: this.getMarketCondition(
|
marketCondition: this.getMarketCondition(
|
||||||
performancePercentFromAllTimeHigh
|
performancePercentFromAllTimeHigh
|
||||||
),
|
),
|
||||||
name: assetProfiles.find(({ dataSource, symbol }) => {
|
name: benchmarkAssetProfiles[index].name,
|
||||||
return (
|
|
||||||
dataSource === benchmarkAssets[index].dataSource &&
|
|
||||||
symbol === benchmarkAssets[index].symbol
|
|
||||||
);
|
|
||||||
})?.name,
|
|
||||||
performances: {
|
performances: {
|
||||||
allTimeHigh: {
|
allTimeHigh: {
|
||||||
performancePercent: performancePercentFromAllTimeHigh.toNumber()
|
performancePercent: performancePercentFromAllTimeHigh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -93,7 +109,97 @@ export class BenchmarkService {
|
|||||||
return benchmarks;
|
return benchmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMarketCondition(aPerformanceInPercent: Big) {
|
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
||||||
return aPerformanceInPercent.lte(-0.2) ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,22 +4,51 @@ import * as path from 'path';
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FrontendMiddleware implements NestMiddleware {
|
export class FrontendMiddleware implements NestMiddleware {
|
||||||
public indexHtmlDe = fs.readFileSync(
|
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'),
|
this.getPathOfIndexHtmlFile('de'),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
public indexHtmlEn = fs.readFileSync(
|
this.indexHtmlEn = fs.readFileSync(
|
||||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
|
this.indexHtmlEs = fs.readFileSync(
|
||||||
public constructor(
|
this.getPathOfIndexHtmlFile('es'),
|
||||||
private readonly configurationService: ConfigurationService
|
'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) {
|
public use(req: Request, res: Response, next: NextFunction) {
|
||||||
let featureGraphicPath = 'assets/cover.png';
|
let featureGraphicPath = 'assets/cover.png';
|
||||||
@ -31,7 +60,11 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
|
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
|
// Skip
|
||||||
next();
|
next();
|
||||||
} else if (req.path === '/de' || req.path.startsWith('/de/')) {
|
} else if (req.path === '/de' || req.path.startsWith('/de/')) {
|
||||||
@ -43,6 +76,33 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
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 {
|
} else {
|
||||||
res.send(
|
res.send(
|
||||||
this.interpolate(this.indexHtmlEn, {
|
this.interpolate(this.indexHtmlEn, {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||||
|
|
||||||
import { InfoService } from './info.service';
|
import { InfoService } from './info.service';
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ export class InfoController {
|
|||||||
public constructor(private readonly infoService: InfoService) {}
|
public constructor(private readonly infoService: InfoService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getInfo(): Promise<InfoItem> {
|
public async getInfo(): Promise<InfoItem> {
|
||||||
return this.infoService.get();
|
return this.infoService.get();
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
@ -16,6 +17,7 @@ import { InfoService } from './info.service';
|
|||||||
@Module({
|
@Module({
|
||||||
controllers: [InfoController],
|
controllers: [InfoController],
|
||||||
imports: [
|
imports: [
|
||||||
|
BenchmarkModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
@ -31,6 +32,7 @@ export class InfoService {
|
|||||||
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly benchmarkService: BenchmarkService,
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
@ -108,6 +110,7 @@ export class InfoService {
|
|||||||
platforms,
|
platforms,
|
||||||
systemMessage,
|
systemMessage,
|
||||||
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
||||||
|
benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(),
|
||||||
currencies: this.exchangeRateDataService.getCurrencies(),
|
currencies: this.exchangeRateDataService.getCurrencies(),
|
||||||
demoAuthToken: this.getDemoAuthToken(),
|
demoAuthToken: this.getDemoAuthToken(),
|
||||||
statistics: await this.getStatistics(),
|
statistics: await this.getStatistics(),
|
||||||
|
@ -103,13 +103,14 @@ export class OrderController {
|
|||||||
impersonationId,
|
impersonationId,
|
||||||
this.request.user.id
|
this.request.user.id
|
||||||
);
|
);
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
|
||||||
let activities = await this.orderService.getOrders({
|
let activities = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
includeDrafts: true,
|
includeDrafts: true,
|
||||||
userId: impersonationUserId || this.request.user.id
|
userId: impersonationUserId || this.request.user.id,
|
||||||
|
withExcludedAccounts: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -189,13 +189,15 @@ export class OrderService {
|
|||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
types,
|
types,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId,
|
||||||
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
types?: TypeOfOrder[];
|
types?: TypeOfOrder[];
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<Activity[]> {
|
}): Promise<Activity[]> {
|
||||||
const where: Prisma.OrderWhereInput = { userId };
|
const where: Prisma.OrderWhereInput = { userId };
|
||||||
|
|
||||||
@ -284,7 +286,11 @@ export class OrderService {
|
|||||||
},
|
},
|
||||||
orderBy: { date: 'asc' }
|
orderBy: { date: 'asc' }
|
||||||
})
|
})
|
||||||
).map((order) => {
|
)
|
||||||
|
.filter((order) => {
|
||||||
|
return withExcludedAccounts || order.Account?.isExcluded === false;
|
||||||
|
})
|
||||||
|
.map((order) => {
|
||||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
min,
|
min,
|
||||||
set
|
set
|
||||||
} from 'date-fns';
|
} 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 { CurrentRateService } from './current-rate.service';
|
||||||
import { CurrentPositions } from './interfaces/current-positions.interface';
|
import { CurrentPositions } from './interfaces/current-positions.interface';
|
||||||
@ -167,13 +167,143 @@ export class PortfolioCalculator {
|
|||||||
this.transactionPoints = transactionPoints;
|
this.transactionPoints = transactionPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
|
public async getChartData(start: Date, end = new Date(Date.now()), step = 1) {
|
||||||
if (!this.transactionPoints?.length) {
|
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 {
|
return {
|
||||||
currentValue: new Big(0),
|
currentValue: new Big(0),
|
||||||
hasErrors: false,
|
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
|
hasErrors: false,
|
||||||
netPerformance: new Big(0),
|
netPerformance: new Big(0),
|
||||||
netPerformancePercentage: new Big(0),
|
netPerformancePercentage: new Big(0),
|
||||||
positions: [],
|
positions: [],
|
||||||
@ -182,39 +312,38 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lastTransactionPoint =
|
const lastTransactionPoint =
|
||||||
this.transactionPoints[this.transactionPoints.length - 1];
|
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
|
||||||
|
|
||||||
// use Date.now() to use the mock for today
|
|
||||||
const today = new Date(Date.now());
|
|
||||||
|
|
||||||
let firstTransactionPoint: TransactionPoint = null;
|
let firstTransactionPoint: TransactionPoint = null;
|
||||||
let firstIndex = this.transactionPoints.length;
|
let firstIndex = transactionPointsBeforeEndDate.length;
|
||||||
const dates = [];
|
const dates = [];
|
||||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
const currencies: { [symbol: string]: string } = {};
|
const currencies: { [symbol: string]: string } = {};
|
||||||
|
|
||||||
dates.push(resetHours(start));
|
dates.push(resetHours(start));
|
||||||
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
||||||
dataGatheringItems.push({
|
dataGatheringItems.push({
|
||||||
dataSource: item.dataSource,
|
dataSource: item.dataSource,
|
||||||
symbol: item.symbol
|
symbol: item.symbol
|
||||||
});
|
});
|
||||||
currencies[item.symbol] = item.currency;
|
currencies[item.symbol] = item.currency;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.transactionPoints.length; i++) {
|
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
|
||||||
if (
|
if (
|
||||||
!isBefore(parseDate(this.transactionPoints[i].date), start) &&
|
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
|
||||||
firstTransactionPoint === null
|
firstTransactionPoint === null
|
||||||
) {
|
) {
|
||||||
firstTransactionPoint = this.transactionPoints[i];
|
firstTransactionPoint = transactionPointsBeforeEndDate[i];
|
||||||
firstIndex = i;
|
firstIndex = i;
|
||||||
}
|
}
|
||||||
if (firstTransactionPoint !== null) {
|
if (firstTransactionPoint !== null) {
|
||||||
dates.push(resetHours(parseDate(this.transactionPoints[i].date)));
|
dates.push(
|
||||||
|
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dates.push(resetHours(today));
|
dates.push(resetHours(end));
|
||||||
|
|
||||||
const marketSymbols = await this.currentRateService.getValues({
|
const marketSymbols = await this.currentRateService.getValues({
|
||||||
currencies,
|
currencies,
|
||||||
@ -241,7 +370,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayString = format(today, DATE_FORMAT);
|
const endDateString = format(end, DATE_FORMAT);
|
||||||
|
|
||||||
if (firstIndex > 0) {
|
if (firstIndex > 0) {
|
||||||
firstIndex--;
|
firstIndex--;
|
||||||
@ -254,7 +383,7 @@ export class PortfolioCalculator {
|
|||||||
const errors: ResponseError['errors'] = [];
|
const errors: ResponseError['errors'] = [];
|
||||||
|
|
||||||
for (const item of lastTransactionPoint.items) {
|
for (const item of lastTransactionPoint.items) {
|
||||||
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
const marketValue = marketSymbolMap[endDateString]?.[item.symbol];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
@ -264,6 +393,7 @@ export class PortfolioCalculator {
|
|||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformancePercentage
|
netPerformancePercentage
|
||||||
} = this.getSymbolMetrics({
|
} = this.getSymbolMetrics({
|
||||||
|
end,
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
start,
|
start,
|
||||||
symbol: item.symbol
|
symbol: item.symbol
|
||||||
@ -700,14 +830,20 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getSymbolMetrics({
|
private getSymbolMetrics({
|
||||||
|
end,
|
||||||
|
isChartMode = false,
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
start,
|
start,
|
||||||
|
step = 1,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
|
end: Date;
|
||||||
|
isChartMode?: boolean;
|
||||||
marketSymbolMap: {
|
marketSymbolMap: {
|
||||||
[date: string]: { [symbol: string]: Big };
|
[date: string]: { [symbol: string]: Big };
|
||||||
};
|
};
|
||||||
start: Date;
|
start: Date;
|
||||||
|
step?: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
let orders: PortfolioOrderItem[] = this.orders.filter((order) => {
|
let orders: PortfolioOrderItem[] = this.orders.filter((order) => {
|
||||||
@ -726,13 +862,12 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dateOfFirstTransaction = new Date(first(orders).date);
|
const dateOfFirstTransaction = new Date(first(orders).date);
|
||||||
const endDate = new Date(Date.now());
|
|
||||||
|
|
||||||
const unitPriceAtStartDate =
|
const unitPriceAtStartDate =
|
||||||
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
|
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
|
||||||
|
|
||||||
const unitPriceAtEndDate =
|
const unitPriceAtEndDate =
|
||||||
marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol];
|
marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!unitPriceAtEndDate ||
|
!unitPriceAtEndDate ||
|
||||||
@ -757,10 +892,12 @@ export class PortfolioCalculator {
|
|||||||
let grossPerformanceFromSells = new Big(0);
|
let grossPerformanceFromSells = new Big(0);
|
||||||
let initialValue: Big;
|
let initialValue: Big;
|
||||||
let investmentAtStartDate: Big;
|
let investmentAtStartDate: Big;
|
||||||
|
const investmentValues: { [date: string]: Big } = {};
|
||||||
let lastAveragePrice = new Big(0);
|
let lastAveragePrice = new Big(0);
|
||||||
let lastTransactionInvestment = new Big(0);
|
let lastTransactionInvestment = new Big(0);
|
||||||
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
||||||
let maxTotalInvestment = new Big(0);
|
let maxTotalInvestment = new Big(0);
|
||||||
|
const netPerformanceValues: { [date: string]: Big } = {};
|
||||||
let timeWeightedGrossPerformancePercentage = new Big(1);
|
let timeWeightedGrossPerformancePercentage = new Big(1);
|
||||||
let timeWeightedNetPerformancePercentage = new Big(1);
|
let timeWeightedNetPerformancePercentage = new Big(1);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
@ -785,7 +922,7 @@ export class PortfolioCalculator {
|
|||||||
orders.push({
|
orders.push({
|
||||||
symbol,
|
symbol,
|
||||||
currency: null,
|
currency: null,
|
||||||
date: format(endDate, DATE_FORMAT),
|
date: format(end, DATE_FORMAT),
|
||||||
dataSource: null,
|
dataSource: null,
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
itemType: 'end',
|
itemType: 'end',
|
||||||
@ -795,6 +932,41 @@ export class PortfolioCalculator {
|
|||||||
unitPrice: unitPriceAtEndDate
|
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
|
// Sort orders so that the start and end placeholder order are at the right
|
||||||
// position
|
// position
|
||||||
orders = sortBy(orders, (order) => {
|
orders = sortBy(orders, (order) => {
|
||||||
@ -957,6 +1129,18 @@ export class PortfolioCalculator {
|
|||||||
feesAtStartDate = fees;
|
feesAtStartDate = fees;
|
||||||
grossPerformanceAtStartDate = grossPerformance;
|
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 =
|
timeWeightedGrossPerformancePercentage =
|
||||||
@ -1042,7 +1226,9 @@ export class PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
initialValue,
|
initialValue,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
|
investmentValues,
|
||||||
netPerformancePercentage,
|
netPerformancePercentage,
|
||||||
|
netPerformanceValues,
|
||||||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
||||||
netPerformance: totalNetPerformance,
|
netPerformance: totalNetPerformance,
|
||||||
grossPerformance: totalGrossPerformance
|
grossPerformance: totalGrossPerformance
|
||||||
|
@ -35,11 +35,11 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors
|
UseInterceptors,
|
||||||
|
Version
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { ViewMode } from '@prisma/client';
|
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
||||||
@ -148,8 +148,17 @@ export class PortfolioController {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const { accounts, holdings, hasErrors } =
|
let portfolioSummary: PortfolioSummary;
|
||||||
await this.portfolioService.getDetails(
|
|
||||||
|
const {
|
||||||
|
accounts,
|
||||||
|
filteredValueInBaseCurrency,
|
||||||
|
filteredValueInPercentage,
|
||||||
|
hasErrors,
|
||||||
|
holdings,
|
||||||
|
summary,
|
||||||
|
totalValueInBaseCurrency
|
||||||
|
} = await this.portfolioService.getDetails(
|
||||||
impersonationId,
|
impersonationId,
|
||||||
this.request.user.id,
|
this.request.user.id,
|
||||||
range,
|
range,
|
||||||
@ -160,6 +169,8 @@ export class PortfolioController {
|
|||||||
hasError = true;
|
hasError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
portfolioSummary = summary;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId ||
|
impersonationId ||
|
||||||
this.userService.isRestrictedView(this.request.user)
|
this.userService.isRestrictedView(this.request.user)
|
||||||
@ -175,7 +186,7 @@ export class PortfolioController {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||||
portfolioPosition.currency,
|
portfolioPosition.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.settings.baseCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
@ -193,6 +204,22 @@ export class PortfolioController {
|
|||||||
accounts[name].current = current / totalValue;
|
accounts[name].current = current / totalValue;
|
||||||
accounts[name].original = original / totalInvestment;
|
accounts[name].original = original / totalInvestment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
portfolioSummary = nullifyValuesInObject(summary, [
|
||||||
|
'cash',
|
||||||
|
'committedFunds',
|
||||||
|
'currentGrossPerformance',
|
||||||
|
'currentNetPerformance',
|
||||||
|
'currentValue',
|
||||||
|
'dividend',
|
||||||
|
'emergencyFund',
|
||||||
|
'excludedAccountsAndActivities',
|
||||||
|
'fees',
|
||||||
|
'items',
|
||||||
|
'netWorth',
|
||||||
|
'totalBuy',
|
||||||
|
'totalSell'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasDetails = true;
|
let hasDetails = true;
|
||||||
@ -214,8 +241,12 @@ export class PortfolioController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
|
filteredValueInBaseCurrency,
|
||||||
|
filteredValueInPercentage,
|
||||||
hasError,
|
hasError,
|
||||||
holdings
|
holdings,
|
||||||
|
totalValueInBaseCurrency,
|
||||||
|
summary: hasDetails ? portfolioSummary : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +309,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId ||
|
impersonationId ||
|
||||||
this.request.user.Settings.viewMode === ViewMode.ZEN ||
|
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
||||||
this.userService.isRestrictedView(this.request.user)
|
this.userService.isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
performanceInformation.performance = nullifyValuesInObject(
|
performanceInformation.performance = nullifyValuesInObject(
|
||||||
@ -290,6 +321,35 @@ export class PortfolioController {
|
|||||||
return performanceInformation;
|
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')
|
@Get('positions')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
@ -358,7 +418,8 @@ export class PortfolioController {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||||
portfolioPosition.currency,
|
portfolioPosition.currency,
|
||||||
this.request.user?.Settings?.currency ?? this.baseCurrency
|
this.request.user?.Settings?.settings.baseCurrency ??
|
||||||
|
this.baseCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
@ -381,46 +442,6 @@ export class PortfolioController {
|
|||||||
return portfolioPublicDetails;
|
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')
|
@Get('position/:dataSource/:symbol')
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
@ -5,7 +5,6 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
|
|||||||
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||||
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface';
|
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
||||||
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
||||||
@ -22,6 +21,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser
|
|||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import {
|
import {
|
||||||
ASSET_SUB_CLASS_EMERGENCY_FUND,
|
ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
MAX_CHART_ITEMS,
|
||||||
UNKNOWN_KEY
|
UNKNOWN_KEY
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -35,7 +35,8 @@ import {
|
|||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition
|
TimelinePosition,
|
||||||
|
UserSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import type {
|
import type {
|
||||||
@ -49,8 +50,11 @@ import type {
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import {
|
import {
|
||||||
|
Account,
|
||||||
AssetClass,
|
AssetClass,
|
||||||
DataSource,
|
DataSource,
|
||||||
|
Order,
|
||||||
|
Platform,
|
||||||
Prisma,
|
Prisma,
|
||||||
Tag,
|
Tag,
|
||||||
Type as TypeOfOrder
|
Type as TypeOfOrder
|
||||||
@ -105,7 +109,8 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getAccounts(
|
public async getAccounts(
|
||||||
aUserId: string,
|
aUserId: string,
|
||||||
aFilters?: Filter[]
|
aFilters?: Filter[],
|
||||||
|
withExcludedAccounts = false
|
||||||
): Promise<AccountWithValue[]> {
|
): Promise<AccountWithValue[]> {
|
||||||
const where: Prisma.AccountWhereInput = { userId: aUserId };
|
const where: Prisma.AccountWhereInput = { userId: aUserId };
|
||||||
|
|
||||||
@ -119,10 +124,16 @@ export class PortfolioService {
|
|||||||
include: { Order: true, Platform: true },
|
include: { Order: true, Platform: true },
|
||||||
orderBy: { name: 'asc' }
|
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) => {
|
return accounts.map((account) => {
|
||||||
let transactionCount = 0;
|
let transactionCount = 0;
|
||||||
@ -159,9 +170,14 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getAccountsWithAggregations(
|
public async getAccountsWithAggregations(
|
||||||
aUserId: string,
|
aUserId: string,
|
||||||
aFilters?: Filter[]
|
aFilters?: Filter[],
|
||||||
|
withExcludedAccounts = false
|
||||||
): Promise<Accounts> {
|
): Promise<Accounts> {
|
||||||
const accounts = await this.getAccounts(aUserId, aFilters);
|
const accounts = await this.getAccounts(
|
||||||
|
aUserId,
|
||||||
|
aFilters,
|
||||||
|
withExcludedAccounts
|
||||||
|
);
|
||||||
let totalBalanceInBaseCurrency = new Big(0);
|
let totalBalanceInBaseCurrency = new Big(0);
|
||||||
let totalValueInBaseCurrency = new Big(0);
|
let totalValueInBaseCurrency = new Big(0);
|
||||||
let transactionCount = 0;
|
let transactionCount = 0;
|
||||||
@ -197,7 +213,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: this.request.user.Settings.currency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
@ -277,7 +293,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: this.request.user.Settings.currency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
@ -354,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(
|
public async getDetails(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aUserId: string,
|
aUserId: string,
|
||||||
aDateRange: DateRange = 'max',
|
aDateRange: DateRange = 'max',
|
||||||
aFilters?: Filter[]
|
aFilters?: Filter[],
|
||||||
|
withExcludedAccounts = false
|
||||||
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||||
const userId = await this.getUserId(aImpersonationId, aUserId);
|
const userId = await this.getUserId(aImpersonationId, aUserId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
@ -367,13 +435,14 @@ export class PortfolioService {
|
|||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
const userCurrency =
|
const userCurrency =
|
||||||
user.Settings?.currency ??
|
user.Settings?.settings.baseCurrency ??
|
||||||
this.request.user?.Settings?.currency ??
|
this.request.user?.Settings?.settings.baseCurrency ??
|
||||||
this.baseCurrency;
|
this.baseCurrency;
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { orders, portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
userId,
|
userId,
|
||||||
|
withExcludedAccounts,
|
||||||
filters: aFilters
|
filters: aFilters
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -400,12 +469,21 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const holdings: PortfolioDetails['holdings'] = {};
|
const holdings: PortfolioDetails['holdings'] = {};
|
||||||
const totalInvestment = currentPositions.totalInvestment.plus(
|
const totalInvestmentInBaseCurrency = currentPositions.totalInvestment.plus(
|
||||||
cashDetails.balanceInBaseCurrency
|
cashDetails.balanceInBaseCurrency
|
||||||
);
|
);
|
||||||
const totalValue = currentPositions.currentValue.plus(
|
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
|
cashDetails.balanceInBaseCurrency
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const dataGatheringItems = currentPositions.positions.map((position) => {
|
const dataGatheringItems = currentPositions.positions.map((position) => {
|
||||||
return {
|
return {
|
||||||
@ -466,10 +544,12 @@ export class PortfolioService {
|
|||||||
|
|
||||||
holdings[item.symbol] = {
|
holdings[item.symbol] = {
|
||||||
markets,
|
markets,
|
||||||
allocationCurrent: totalValue.eq(0)
|
allocationCurrent: filteredValueInBaseCurrency.eq(0)
|
||||||
? 0
|
? 0
|
||||||
: value.div(totalValue).toNumber(),
|
: value.div(filteredValueInBaseCurrency).toNumber(),
|
||||||
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
allocationInvestment: item.investment
|
||||||
|
.div(totalInvestmentInBaseCurrency)
|
||||||
|
.toNumber(),
|
||||||
assetClass: symbolProfile.assetClass,
|
assetClass: symbolProfile.assetClass,
|
||||||
assetSubClass: symbolProfile.assetSubClass,
|
assetSubClass: symbolProfile.assetSubClass,
|
||||||
countries: symbolProfile.countries,
|
countries: symbolProfile.countries,
|
||||||
@ -503,8 +583,8 @@ export class PortfolioService {
|
|||||||
cashDetails,
|
cashDetails,
|
||||||
emergencyFund,
|
emergencyFund,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
investment: totalInvestment,
|
investment: totalInvestmentInBaseCurrency,
|
||||||
value: totalValue
|
value: filteredValueInBaseCurrency
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const symbol of Object.keys(cashPositions)) {
|
for (const symbol of Object.keys(cashPositions)) {
|
||||||
@ -517,10 +597,23 @@ export class PortfolioService {
|
|||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
|
withExcludedAccounts,
|
||||||
filters: aFilters
|
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(
|
public async getPosition(
|
||||||
@ -528,11 +621,15 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioPositionDetail> {
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const orders = (
|
const orders = (
|
||||||
await this.orderService.getOrders({ userCurrency, userId })
|
await this.orderService.getOrders({
|
||||||
|
userCurrency,
|
||||||
|
userId,
|
||||||
|
withExcludedAccounts: true
|
||||||
|
})
|
||||||
).filter(({ SymbolProfile }) => {
|
).filter(({ SymbolProfile }) => {
|
||||||
return (
|
return (
|
||||||
SymbolProfile.dataSource === aDataSource &&
|
SymbolProfile.dataSource === aDataSource &&
|
||||||
@ -781,7 +878,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: this.request.user.Settings.currency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
@ -857,7 +954,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: this.request.user.Settings.currency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
@ -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> {
|
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
||||||
const currency = this.request.user.Settings.currency;
|
const currency = this.request.user.Settings.settings.baseCurrency;
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { orders, portfolioOrders, transactionPoints } =
|
||||||
@ -971,7 +1167,7 @@ export class PortfolioService {
|
|||||||
accounts
|
accounts
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{ baseCurrency: currency }
|
<UserSettings>this.request.user.Settings.settings
|
||||||
),
|
),
|
||||||
currencyClusterRisk: await this.rulesService.evaluate(
|
currencyClusterRisk: await this.rulesService.evaluate(
|
||||||
[
|
[
|
||||||
@ -992,7 +1188,7 @@ export class PortfolioService {
|
|||||||
currentPositions
|
currentPositions
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{ baseCurrency: currency }
|
<UserSettings>this.request.user.Settings.settings
|
||||||
),
|
),
|
||||||
fees: await this.rulesService.evaluate(
|
fees: await this.rulesService.evaluate(
|
||||||
[
|
[
|
||||||
@ -1002,80 +1198,12 @@ export class PortfolioService {
|
|||||||
this.getFees(orders).toNumber()
|
this.getFees(orders).toNumber()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{ baseCurrency: currency }
|
<UserSettings>this.request.user.Settings.settings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
|
||||||
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({
|
private async getCashPositions({
|
||||||
cashDetails,
|
cashDetails,
|
||||||
emergencyFund,
|
emergencyFund,
|
||||||
@ -1183,7 +1311,7 @@ export class PortfolioService {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
order.SymbolProfile.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.settings.baseCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce(
|
.reduce(
|
||||||
@ -1202,7 +1330,7 @@ export class PortfolioService {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
order.fee,
|
order.fee,
|
||||||
order.SymbolProfile.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.settings.baseCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce(
|
.reduce(
|
||||||
@ -1224,7 +1352,7 @@ export class PortfolioService {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
order.SymbolProfile.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.settings.baseCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce(
|
.reduce(
|
||||||
@ -1251,27 +1379,131 @@ export class PortfolioService {
|
|||||||
return portfolioStart;
|
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({
|
private async getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
userId
|
userId,
|
||||||
|
withExcludedAccounts
|
||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
transactionPoints: TransactionPoint[];
|
transactionPoints: TransactionPoint[];
|
||||||
orders: OrderWithAccount[];
|
orders: OrderWithAccount[];
|
||||||
portfolioOrders: PortfolioOrder[];
|
portfolioOrders: PortfolioOrder[];
|
||||||
}> {
|
}> {
|
||||||
const userCurrency =
|
const userCurrency =
|
||||||
this.request.user?.Settings?.currency ?? this.baseCurrency;
|
this.request.user?.Settings?.settings.baseCurrency ?? this.baseCurrency;
|
||||||
|
|
||||||
const orders = await this.orderService.getOrders({
|
const orders = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts,
|
includeDrafts,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
|
withExcludedAccounts,
|
||||||
types: ['BUY', 'SELL']
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1323,17 +1555,22 @@ export class PortfolioService {
|
|||||||
orders,
|
orders,
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId,
|
||||||
|
withExcludedAccounts
|
||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
orders: OrderWithAccount[];
|
orders: OrderWithAccount[];
|
||||||
portfolioItemsNow: { [p: string]: TimelinePosition };
|
portfolioItemsNow: { [p: string]: TimelinePosition };
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const accounts: PortfolioDetails['accounts'] = {};
|
const accounts: PortfolioDetails['accounts'] = {};
|
||||||
|
|
||||||
let currentAccounts = [];
|
let currentAccounts: (Account & {
|
||||||
|
Order?: Order[];
|
||||||
|
Platform?: Platform;
|
||||||
|
})[] = [];
|
||||||
|
|
||||||
if (filters.length === 0) {
|
if (filters.length === 0) {
|
||||||
currentAccounts = await this.accountService.getAccounts(userId);
|
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) {
|
for (const account of currentAccounts) {
|
||||||
const ordersByAccount = orders.filter(({ accountId }) => {
|
const ordersByAccount = orders.filter(({ accountId }) => {
|
||||||
return accountId === account.id;
|
return accountId === account.id;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { Rule } from '@ghostfolio/api/models/rule';
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -8,7 +9,7 @@ export class RulesService {
|
|||||||
|
|
||||||
public async evaluate<T extends RuleSettings>(
|
public async evaluate<T extends RuleSettings>(
|
||||||
aRules: Rule<T>[],
|
aRules: Rule<T>[],
|
||||||
aUserSettings: { baseCurrency: string }
|
aUserSettings: UserSettings
|
||||||
) {
|
) {
|
||||||
return aRules
|
return aRules
|
||||||
.filter((rule) => {
|
.filter((rule) => {
|
||||||
|
@ -12,7 +12,10 @@ import { RedisCacheService } from './redis-cache.service';
|
|||||||
inject: [ConfigurationService],
|
inject: [ConfigurationService],
|
||||||
useFactory: async (configurationService: ConfigurationService) => {
|
useFactory: async (configurationService: ConfigurationService) => {
|
||||||
return <CacheManagerOptions>{
|
return <CacheManagerOptions>{
|
||||||
|
host: configurationService.get('REDIS_HOST'),
|
||||||
max: configurationService.get('MAX_ITEM_IN_CACHE'),
|
max: configurationService.get('MAX_ITEM_IN_CACHE'),
|
||||||
|
password: configurationService.get('REDIS_PASSWORD'),
|
||||||
|
port: configurationService.get('REDIS_PORT'),
|
||||||
store: redisStore,
|
store: redisStore,
|
||||||
ttl: configurationService.get('CACHE_TTL')
|
ttl: configurationService.get('CACHE_TTL')
|
||||||
};
|
};
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { ViewMode } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface UserSettingsParams {
|
|
||||||
currency?: string;
|
|
||||||
userId: string;
|
|
||||||
viewMode?: ViewMode;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export interface UserSettings {
|
|
||||||
emergencyFund?: number;
|
|
||||||
locale?: string;
|
|
||||||
isRestrictedView?: boolean;
|
|
||||||
}
|
|
@ -1,10 +1,33 @@
|
|||||||
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
|
import type { DateRange, ViewMode } from '@ghostfolio/common/types';
|
||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsIn,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserSettingDto {
|
export class UpdateUserSettingDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
baseCurrency?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
benchmark?: string;
|
||||||
|
|
||||||
|
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
|
||||||
|
@IsOptional()
|
||||||
|
dateRange?: DateRange;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
emergencyFund?: number;
|
emergencyFund?: number;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isExperimentalFeatures?: boolean;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
@ -20,4 +43,8 @@ export class UpdateUserSettingDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
savingsRate?: number;
|
savingsRate?: number;
|
||||||
|
|
||||||
|
@IsIn(<ViewMode[]>['DEFAULT', 'ZEN'])
|
||||||
|
@IsOptional()
|
||||||
|
viewMode?: ViewMode;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { ViewMode } from '@prisma/client';
|
|
||||||
import { IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class UpdateUserSettingsDto {
|
|
||||||
@IsString()
|
|
||||||
baseCurrency: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
viewMode: ViewMode;
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config';
|
import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
@ -22,12 +22,10 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { User as UserModel } from '@prisma/client';
|
import { User as UserModel } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
import { size } from 'lodash';
|
||||||
|
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
|
||||||
import { UserSettings } from './interfaces/user-settings.interface';
|
|
||||||
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
||||||
import { UpdateUserSettingsDto } from './update-user-settings.dto';
|
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
@Controller('user')
|
@Controller('user')
|
||||||
@ -103,6 +101,12 @@ export class UserController {
|
|||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
||||||
if (
|
if (
|
||||||
|
size(data) === 1 &&
|
||||||
|
(data.benchmark || data.dateRange) &&
|
||||||
|
this.request.user.role === 'DEMO'
|
||||||
|
) {
|
||||||
|
// Allow benchmark or date range change for demo user
|
||||||
|
} else if (
|
||||||
!hasPermission(
|
!hasPermission(
|
||||||
this.request.user.permissions,
|
this.request.user.permissions,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
@ -130,33 +134,4 @@ export class UserController {
|
|||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('settings')
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {
|
|
||||||
if (
|
|
||||||
!hasPermission(
|
|
||||||
this.request.user.permissions,
|
|
||||||
permissions.updateUserSettings
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new HttpException(
|
|
||||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
|
||||||
StatusCodes.FORBIDDEN
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userSettings: UserSettingsParams = {
|
|
||||||
currency: data.baseCurrency,
|
|
||||||
userId: this.request.user.id
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasPermission(this.request.user.permissions, permissions.updateViewMode)
|
|
||||||
) {
|
|
||||||
userSettings.viewMode = data.viewMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.userService.updateUserSettings(userSettings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,19 +4,20 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||||
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
|
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
|
||||||
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
User as IUser,
|
||||||
|
UserSettings,
|
||||||
|
UserWithSettings
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import {
|
||||||
getPermissions,
|
getPermissions,
|
||||||
hasRole,
|
hasRole,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma, Role, User, ViewMode } from '@prisma/client';
|
import { Prisma, Role, User } from '@prisma/client';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
|
|
||||||
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
|
||||||
import { UserSettings } from './interfaces/user-settings.interface';
|
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -69,9 +70,7 @@ export class UserService {
|
|||||||
accounts: Account,
|
accounts: Account,
|
||||||
settings: {
|
settings: {
|
||||||
...(<UserSettings>Settings.settings),
|
...(<UserSettings>Settings.settings),
|
||||||
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale
|
||||||
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale,
|
|
||||||
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -89,7 +88,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isRestrictedView(aUser: UserWithSettings) {
|
public isRestrictedView(aUser: UserWithSettings) {
|
||||||
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false;
|
return aUser.Settings.settings.isRestrictedView ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async user(
|
public async user(
|
||||||
@ -126,21 +125,35 @@ export class UserService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (user?.Settings) {
|
if (user?.Settings) {
|
||||||
if (!user.Settings.currency) {
|
if (!user.Settings.settings) {
|
||||||
// Set default currency if needed
|
user.Settings.settings = {};
|
||||||
user.Settings.currency = UserService.DEFAULT_CURRENCY;
|
|
||||||
}
|
}
|
||||||
} else if (user) {
|
} else if (user) {
|
||||||
// Set default settings if needed
|
// Set default settings if needed
|
||||||
user.Settings = {
|
user.Settings = {
|
||||||
currency: UserService.DEFAULT_CURRENCY,
|
settings: {},
|
||||||
settings: null,
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
userId: user?.id,
|
userId: user?.id
|
||||||
viewMode: ViewMode.DEFAULT
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default value for base currency
|
||||||
|
if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
|
||||||
|
(user.Settings.settings as UserSettings).baseCurrency =
|
||||||
|
UserService.DEFAULT_CURRENCY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default value for date range
|
||||||
|
(user.Settings.settings as UserSettings).dateRange =
|
||||||
|
(user.Settings.settings as UserSettings).viewMode === 'ZEN'
|
||||||
|
? 'max'
|
||||||
|
: (user.Settings.settings as UserSettings)?.dateRange ?? 'max';
|
||||||
|
|
||||||
|
// Set default value for view mode
|
||||||
|
if (!(user.Settings.settings as UserSettings).viewMode) {
|
||||||
|
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
|
||||||
|
}
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
user.subscription =
|
user.subscription =
|
||||||
this.subscriptionService.getSubscription(Subscription);
|
this.subscriptionService.getSubscription(Subscription);
|
||||||
@ -221,10 +234,12 @@ export class UserService {
|
|||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
create: {
|
create: {
|
||||||
|
settings: {
|
||||||
currency: this.baseCurrency
|
currency: this.baseCurrency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.provider === 'ANONYMOUS') {
|
if (data.provider === 'ANONYMOUS') {
|
||||||
@ -295,7 +310,7 @@ export class UserService {
|
|||||||
userId: string;
|
userId: string;
|
||||||
userSettings: UserSettings;
|
userSettings: UserSettings;
|
||||||
}) {
|
}) {
|
||||||
const settings = userSettings as Prisma.JsonObject;
|
const settings = userSettings as unknown as Prisma.JsonObject;
|
||||||
|
|
||||||
await this.prismaService.settings.upsert({
|
await this.prismaService.settings.upsert({
|
||||||
create: {
|
create: {
|
||||||
@ -317,33 +332,6 @@ export class UserService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateUserSettings({
|
|
||||||
currency,
|
|
||||||
userId,
|
|
||||||
viewMode
|
|
||||||
}: UserSettingsParams) {
|
|
||||||
await this.prismaService.settings.upsert({
|
|
||||||
create: {
|
|
||||||
currency,
|
|
||||||
User: {
|
|
||||||
connect: {
|
|
||||||
id: userId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewMode
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
currency,
|
|
||||||
viewMode
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
userId: userId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRandomString(length: number) {
|
private getRandomString(length: number) {
|
||||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
const result = [];
|
const result = [];
|
||||||
|
@ -41,6 +41,14 @@ export class RedactValuesInResponseInterceptor<T>
|
|||||||
return activity;
|
return activity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.filteredValueInBaseCurrency) {
|
||||||
|
data.filteredValueInBaseCurrency = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.totalValueInBaseCurrency) {
|
||||||
|
data.totalValueInBaseCurrency = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
NestInterceptor
|
NestInterceptor
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -36,6 +37,13 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isArray(data.benchmarks)) {
|
||||||
|
data.benchmarks.map((benchmark) => {
|
||||||
|
benchmark.dataSource = encodeDataSource(benchmark.dataSource);
|
||||||
|
return benchmark;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.dataSource) {
|
if (data.dataSource) {
|
||||||
data.dataSource = encodeDataSource(data.dataSource);
|
data.dataSource = encodeDataSource(data.dataSource);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { EvaluationResult } from './evaluation-result.interface';
|
import { EvaluationResult } from './evaluation-result.interface';
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export interface UserSettings {
|
|
||||||
baseCurrency: string;
|
|
||||||
}
|
|
@ -1,8 +1,7 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { groupBy } from '@ghostfolio/common/helper';
|
import { groupBy } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
||||||
import { RuleInterface } from './interfaces/rule.interface';
|
import { RuleInterface } from './interfaces/rule.interface';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPosition
|
PortfolioPosition,
|
||||||
|
UserSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPosition
|
PortfolioPosition,
|
||||||
|
UserSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PortfolioDetails } from '@ghostfolio/common/interfaces';
|
import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -39,9 +39,9 @@ export class ConfigurationService {
|
|||||||
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
||||||
PORT: port({ default: 3333 }),
|
PORT: port({ default: 3333 }),
|
||||||
RAKUTEN_RAPID_API_KEY: str({ default: '' }),
|
RAKUTEN_RAPID_API_KEY: str({ default: '' }),
|
||||||
REDIS_HOST: host(),
|
REDIS_HOST: host({ default: 'localhost' }),
|
||||||
REDIS_PASSWORD: str({ default: '' }),
|
REDIS_PASSWORD: str({ default: '' }),
|
||||||
REDIS_PORT: port(),
|
REDIS_PORT: port({ default: 6379 }),
|
||||||
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
||||||
STRIPE_PUBLIC_KEY: str({ default: '' }),
|
STRIPE_PUBLIC_KEY: str({ default: '' }),
|
||||||
STRIPE_SECRET_KEY: str({ default: '' }),
|
STRIPE_SECRET_KEY: str({ default: '' }),
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@ -90,7 +91,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||||
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
||||||
modules: ['price', 'summaryProfile']
|
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||||
});
|
});
|
||||||
|
|
||||||
const { assetClass, assetSubClass } = this.parseAssetClass(
|
const { assetClass, assetSubClass } = this.parseAssetClass(
|
||||||
@ -109,7 +110,16 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
});
|
});
|
||||||
response.symbol = aSymbol;
|
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 &&
|
assetSubClass === AssetSubClass.STOCK &&
|
||||||
assetProfile.summaryProfile?.country
|
assetProfile.summaryProfile?.country
|
||||||
) {
|
) {
|
||||||
@ -183,10 +193,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
for (const historicalItem of historicalResult) {
|
for (const historicalItem of historicalResult) {
|
||||||
let marketPrice = historicalItem.close;
|
let marketPrice = historicalItem.close;
|
||||||
|
|
||||||
if (symbol === 'USDGBp') {
|
if (symbol === `${this.baseCurrency}GBp`) {
|
||||||
// Convert GPB to GBp (pence)
|
// Convert GPB to GBp (pence)
|
||||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||||
} else if (symbol === 'USDILA') {
|
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||||
// Convert ILS to ILA
|
// Convert ILS to ILA
|
||||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||||
}
|
}
|
||||||
@ -246,9 +256,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
marketPrice: quote.regularMarketPrice || 0
|
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)
|
// Convert GPB to GBp (pence)
|
||||||
response['USDGBp'] = {
|
response[`${this.baseCurrency}GBp`] = {
|
||||||
...response[symbol],
|
...response[symbol],
|
||||||
currency: 'GBp',
|
currency: 'GBp',
|
||||||
marketPrice: new Big(response[symbol].marketPrice)
|
marketPrice: new Big(response[symbol].marketPrice)
|
||||||
@ -256,11 +269,11 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
.toNumber()
|
.toNumber()
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
symbol === 'USDILS' &&
|
symbol === `${this.baseCurrency}ILS` &&
|
||||||
yahooFinanceSymbols.includes('USDILA=X')
|
yahooFinanceSymbols.includes(`${this.baseCurrency}ILA=X`)
|
||||||
) {
|
) {
|
||||||
// Convert ILS to ILA
|
// Convert ILS to ILA
|
||||||
response['USDILA'] = {
|
response[`${this.baseCurrency}ILA`] = {
|
||||||
...response[symbol],
|
...response[symbol],
|
||||||
currency: 'ILA',
|
currency: 'ILA',
|
||||||
marketPrice: new Big(response[symbol].marketPrice)
|
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)
|
// Convert USD to USX (cent)
|
||||||
response['USDUSX'] = {
|
response[`${this.baseCurrency}USX`] = {
|
||||||
currency: 'USX',
|
currency: 'USX',
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
marketPrice: new Big(1).mul(100).toNumber(),
|
marketPrice: new Big(1).mul(100).toNumber(),
|
||||||
@ -434,4 +447,46 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
return { assetClass, assetSubClass };
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,10 +99,12 @@ export class ExchangeRateDataService {
|
|||||||
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
||||||
|
|
||||||
if (!this.exchangeRates[symbol]) {
|
if (!this.exchangeRates[symbol]) {
|
||||||
// Not found, calculate indirectly via USD
|
// Not found, calculate indirectly via base currency
|
||||||
this.exchangeRates[symbol] =
|
this.exchangeRates[symbol] =
|
||||||
resultExtended[`${currency1}${'USD'}`]?.[date]?.marketPrice *
|
resultExtended[`${currency1}${this.baseCurrency}`]?.[date]
|
||||||
resultExtended[`${'USD'}${currency2}`]?.[date]?.marketPrice;
|
?.marketPrice *
|
||||||
|
resultExtended[`${this.baseCurrency}${currency2}`]?.[date]
|
||||||
|
?.marketPrice;
|
||||||
|
|
||||||
// Calculate the opposite direction
|
// Calculate the opposite direction
|
||||||
this.exchangeRates[`${currency2}${currency1}`] =
|
this.exchangeRates[`${currency2}${currency1}`] =
|
||||||
@ -126,9 +128,11 @@ export class ExchangeRateDataService {
|
|||||||
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
||||||
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
||||||
} else {
|
} else {
|
||||||
// Calculate indirectly via USD
|
// Calculate indirectly via base currency
|
||||||
const factor1 = this.exchangeRates[`${aFromCurrency}${'USD'}`];
|
const factor1 =
|
||||||
const factor2 = this.exchangeRates[`${'USD'}${aToCurrency}`];
|
this.exchangeRates[`${aFromCurrency}${this.baseCurrency}`];
|
||||||
|
const factor2 =
|
||||||
|
this.exchangeRates[`${this.baseCurrency}${aToCurrency}`];
|
||||||
|
|
||||||
factor = factor1 * factor2;
|
factor = factor1 * factor2;
|
||||||
|
|
||||||
@ -166,21 +170,6 @@ export class ExchangeRateDataService {
|
|||||||
currencies.push(account.currency);
|
currencies.push(account.currency);
|
||||||
});
|
});
|
||||||
|
|
||||||
(
|
|
||||||
await this.prismaService.settings.findMany({
|
|
||||||
distinct: ['currency'],
|
|
||||||
orderBy: [{ currency: 'asc' }],
|
|
||||||
select: { currency: true },
|
|
||||||
where: {
|
|
||||||
currency: {
|
|
||||||
not: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).forEach((userSettings) => {
|
|
||||||
currencies.push(userSettings.currency);
|
|
||||||
});
|
|
||||||
|
|
||||||
(
|
(
|
||||||
await this.prismaService.symbolProfile.findMany({
|
await this.prismaService.symbolProfile.findMany({
|
||||||
distinct: ['currency'],
|
distinct: ['currency'],
|
||||||
|
@ -64,6 +64,23 @@ export class SymbolProfileService {
|
|||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.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
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
|
||||||
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
|
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
||||||
this.accountType = accountType;
|
this.accountType = accountType;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.platformName = Platform?.name;
|
this.platformName = Platform?.name ?? '-';
|
||||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
@ -21,10 +21,12 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 mb-3">
|
<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>
|
||||||
<div class="col-6 mb-3">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
<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-select formControlName="status">
|
||||||
<mat-option></mat-option>
|
<mat-option></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
@ -13,7 +16,7 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button
|
<button
|
||||||
class="ml-1"
|
class="mt-1"
|
||||||
color="warn"
|
color="warn"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="onDeleteJobs()"
|
(click)="onDeleteJobs()"
|
||||||
|
@ -14,8 +14,7 @@ import {
|
|||||||
getDateFormatString,
|
getDateFormatString,
|
||||||
getLocale
|
getLocale
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { LineChartItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
|
@ -92,7 +92,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
if (
|
if (
|
||||||
params['assetProfileDialog'] &&
|
params['assetProfileDialog'] &&
|
||||||
params['dataSource'] &&
|
params['dataSource'] &&
|
||||||
params['dateOfFirstActivity'] &&
|
|
||||||
params['symbol']
|
params['symbol']
|
||||||
) {
|
) {
|
||||||
this.openAssetProfileDialog({
|
this.openAssetProfileDialog({
|
||||||
@ -170,12 +169,16 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
dateOfFirstActivity,
|
dateOfFirstActivity,
|
||||||
symbol
|
symbol
|
||||||
}: UniqueAsset & { dateOfFirstActivity: string }) {
|
}: UniqueAsset & { dateOfFirstActivity: string }) {
|
||||||
|
try {
|
||||||
|
dateOfFirstActivity = format(parseISO(dateOfFirstActivity), DATE_FORMAT);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
dateOfFirstActivity,
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
assetProfileDialog: true,
|
assetProfileDialog: true
|
||||||
dateOfFirstActivity: format(parseISO(dateOfFirstActivity), DATE_FORMAT)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -162,8 +162,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<form #couponForm="ngForm">
|
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||||
<mat-form-field appearance="outline" class="mr-2">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline mr-2 without-hint"
|
||||||
|
>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="duration"
|
name="duration"
|
||||||
[value]="couponDuration"
|
[value]="couponDuration"
|
||||||
@ -176,6 +179,7 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button
|
<button
|
||||||
|
class="mt-1"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="onAddCoupon()"
|
(click)="onAddCoupon()"
|
||||||
|
@ -14,8 +14,8 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
|||||||
declarations: [AdminOverviewComponent],
|
declarations: [AdminOverviewComponent],
|
||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
@ -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>
|
@ -0,0 +1,11 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
|
||||||
|
ngx-skeleton-loader {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [BenchmarkComparatorComponent],
|
||||||
|
exports: [BenchmarkComparatorComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
GfToggleModule,
|
||||||
|
MatSelectModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class GfBenchmarkComparatorModule {}
|
@ -5,10 +5,6 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos
|
|||||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import {
|
|
||||||
RANGE,
|
|
||||||
SettingsStorageService
|
|
||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -26,7 +22,6 @@ import { PositionDetailDialogParams } from '../position/position-detail-dialog/i
|
|||||||
templateUrl: './home-holdings.html'
|
templateUrl: './home-holdings.html'
|
||||||
})
|
})
|
||||||
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||||
public dateRange: DateRange;
|
|
||||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
@ -44,7 +39,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private settingsStorageService: SettingsStorageService,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.route.queryParams
|
this.route.queryParams
|
||||||
@ -73,7 +67,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
permissions.createOrder
|
permissions.createOrder
|
||||||
);
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -88,18 +82,25 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dateRange =
|
|
||||||
this.user.settings.viewMode === 'ZEN'
|
|
||||||
? 'max'
|
|
||||||
: <DateRange>this.settingsStorageService.getSetting(RANGE) ?? 'max';
|
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeDateRange(aDateRange: DateRange) {
|
public onChangeDateRange(dateRange: DateRange) {
|
||||||
this.dateRange = aDateRange;
|
this.dataService
|
||||||
this.settingsStorageService.setSetting(RANGE, this.dateRange);
|
.putUserSetting({ dateRange })
|
||||||
this.update();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
@ -151,7 +152,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
this.positions = undefined;
|
this.positions = undefined;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositions({ range: this.dateRange })
|
.fetchPositions({ range: this.user?.settings?.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.positions = response.positions;
|
this.positions = response.positions;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container justify-content-center p-3">
|
<div class="container justify-content-center p-3">
|
||||||
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
|
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="dateRange"
|
[defaultValue]="user?.settings?.dateRange"
|
||||||
[isLoading]="positions === undefined"
|
[isLoading]="positions === undefined"
|
||||||
[options]="dateRangeOptions"
|
[options]="dateRangeOptions"
|
||||||
(change)="onChangeDateRange($event.value)"
|
(change)="onChangeDateRange($event.value)"
|
||||||
@ -17,7 +17,7 @@
|
|||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="positions"
|
[positions]="positions"
|
||||||
[range]="dateRange"
|
[range]="user?.settings?.dateRange"
|
||||||
></gf-positions>
|
></gf-positions>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -2,19 +2,15 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import {
|
|
||||||
RANGE,
|
|
||||||
SettingsStorageService
|
|
||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import {
|
import {
|
||||||
|
LineChartItem,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
UniqueAsset,
|
UniqueAsset,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -25,7 +21,6 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './home-overview.html'
|
templateUrl: './home-overview.html'
|
||||||
})
|
})
|
||||||
export class HomeOverviewComponent implements OnDestroy, OnInit {
|
export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||||
public dateRange: DateRange;
|
|
||||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public errors: UniqueAsset[];
|
public errors: UniqueAsset[];
|
||||||
@ -47,7 +42,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private settingsStorageService: SettingsStorageService,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -61,7 +55,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
permissions.createOrder
|
permissions.createOrder
|
||||||
);
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -78,23 +72,28 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dateRange =
|
|
||||||
this.user.settings.viewMode === 'ZEN'
|
|
||||||
? 'max'
|
|
||||||
: <DateRange>this.settingsStorageService.getSetting(RANGE) ?? 'max';
|
|
||||||
|
|
||||||
this.showDetails =
|
this.showDetails =
|
||||||
!this.hasImpersonationId &&
|
!this.hasImpersonationId &&
|
||||||
!this.user.settings.isRestrictedView &&
|
!this.user.settings.isRestrictedView &&
|
||||||
this.user.settings.viewMode !== 'ZEN';
|
this.user.settings.viewMode !== 'ZEN';
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeDateRange(aDateRange: DateRange) {
|
public onChangeDateRange(dateRange: DateRange) {
|
||||||
this.dateRange = aDateRange;
|
this.dataService
|
||||||
this.settingsStorageService.setSetting(RANGE, this.dateRange);
|
.putUserSetting({ dateRange })
|
||||||
this.update();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
@ -103,26 +102,14 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
|
this.historicalDataItems = null;
|
||||||
this.isLoadingPerformance = true;
|
this.isLoadingPerformance = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({ range: this.dateRange })
|
.fetchPortfolioPerformance({
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
range: this.user?.settings?.dateRange,
|
||||||
.subscribe((chartData) => {
|
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
|
||||||
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 })
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.errors = response.errors;
|
this.errors = response.errors;
|
||||||
@ -130,6 +117,36 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
this.performance = response.performance;
|
this.performance = response.performance;
|
||||||
this.isLoadingPerformance = false;
|
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();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="position-absolute"
|
class="position-absolute"
|
||||||
symbol="Performance"
|
symbol="Performance"
|
||||||
[currency]="user?.settings?.baseCurrency"
|
[currency]="user?.settings?.isExperimentalFeatures ? undefined : user?.settings?.baseCurrency"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
[hidden]="historicalDataItems?.length === 0"
|
[hidden]="historicalDataItems?.length === 0"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -24,6 +24,7 @@
|
|||||||
[showLoader]="false"
|
[showLoader]="false"
|
||||||
[showXAxis]="false"
|
[showXAxis]="false"
|
||||||
[showYAxis]="false"
|
[showYAxis]="false"
|
||||||
|
[unit]="user?.settings?.isExperimentalFeatures ? '%' : undefined"
|
||||||
></gf-line-chart>
|
></gf-line-chart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -45,7 +46,7 @@
|
|||||||
></gf-portfolio-performance>
|
></gf-portfolio-performance>
|
||||||
<div *ngIf="showDetails" class="text-center">
|
<div *ngIf="showDetails" class="text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="dateRange"
|
[defaultValue]="user?.settings?.dateRange"
|
||||||
[isLoading]="isLoadingPerformance"
|
[isLoading]="isLoadingPerformance"
|
||||||
[options]="dateRangeOptions"
|
[options]="dateRangeOptions"
|
||||||
(change)="onChangeDateRange($event.value)"
|
(change)="onChangeDateRange($event.value)"
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
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 { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
InfoItem,
|
||||||
|
PortfolioSummary,
|
||||||
|
User
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public info: InfoItem;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||||
public summary: PortfolioSummary;
|
public summary: PortfolioSummary;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private router: Router,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
|
this.info = this.dataService.fetchInfo();
|
||||||
|
|
||||||
|
this.hasPermissionForSubscription = hasPermission(
|
||||||
|
this.info?.globalPermissions,
|
||||||
|
permissions.enableSubscription
|
||||||
|
);
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -38,7 +60,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
);
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeEmergencyFund(emergencyFund: number) {
|
public onChangeEmergencyFund(emergencyFund: number) {
|
||||||
@ -59,7 +79,16 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
.putUserSetting({ emergencyFund })
|
.putUserSetting({ emergencyFund })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.update();
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioSummary()
|
.fetchPortfolioDetails({})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe(({ summary }) => {
|
||||||
this.summary = response;
|
this.summary = summary;
|
||||||
this.isLoading = false;
|
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();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
public chart: Chart;
|
public chart: Chart;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
|
||||||
|
private data: InvestmentItem[];
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
Chart.register(
|
Chart.register(
|
||||||
annotationPlugin,
|
annotationPlugin,
|
||||||
@ -87,10 +89,13 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
if (!this.groupBy && this.investments?.length > 0) {
|
// Create a clone
|
||||||
|
this.data = this.investments.map((a) => Object.assign({}, a));
|
||||||
|
|
||||||
|
if (!this.groupBy && this.data?.length > 0) {
|
||||||
// Extend chart by 5% of days in market (before)
|
// Extend chart by 5% of days in market (before)
|
||||||
const firstItem = this.investments[0];
|
const firstItem = this.data[0];
|
||||||
this.investments.unshift({
|
this.data.unshift({
|
||||||
...firstItem,
|
...firstItem,
|
||||||
date: subDays(
|
date: subDays(
|
||||||
parseISO(firstItem.date),
|
parseISO(firstItem.date),
|
||||||
@ -100,8 +105,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Extend chart by 5% of days in market (after)
|
// Extend chart by 5% of days in market (after)
|
||||||
const lastItem = this.investments[this.investments.length - 1];
|
const lastItem = this.data[this.data.length - 1];
|
||||||
this.investments.push({
|
this.data.push({
|
||||||
...lastItem,
|
...lastItem,
|
||||||
date: addDays(
|
date: addDays(
|
||||||
parseDate(lastItem.date),
|
parseDate(lastItem.date),
|
||||||
@ -111,7 +116,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: this.investments.map((investmentItem) => {
|
labels: this.data.map((investmentItem) => {
|
||||||
return investmentItem.date;
|
return investmentItem.date;
|
||||||
}),
|
}),
|
||||||
datasets: [
|
datasets: [
|
||||||
@ -119,8 +124,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
borderWidth: this.groupBy ? 0 : 2,
|
borderWidth: this.groupBy ? 0 : 2,
|
||||||
data: this.investments.map((position) => {
|
data: this.data.map((position) => {
|
||||||
return position.investment;
|
return this.isInPercent
|
||||||
|
? position.investment * 100
|
||||||
|
: position.investment;
|
||||||
}),
|
}),
|
||||||
label: $localize`Deposit`,
|
label: $localize`Deposit`,
|
||||||
segment: {
|
segment: {
|
||||||
@ -249,10 +256,11 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
private getTooltipPluginConfiguration() {
|
private getTooltipPluginConfiguration() {
|
||||||
return {
|
return {
|
||||||
...getTooltipOptions(
|
...getTooltipOptions({
|
||||||
this.isInPercent ? undefined : this.currency,
|
currency: this.isInPercent ? undefined : this.currency,
|
||||||
this.isInPercent ? undefined : this.locale
|
locale: this.isInPercent ? undefined : this.locale,
|
||||||
),
|
unit: this.isInPercent ? '%' : undefined
|
||||||
|
}),
|
||||||
mode: 'index',
|
mode: 'index',
|
||||||
position: <unknown>'top',
|
position: <unknown>'top',
|
||||||
xAlign: 'center',
|
xAlign: 'center',
|
||||||
|
@ -172,6 +172,17 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</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="row">
|
||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,9 +9,11 @@ import {
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
EnhancedSymbolProfile,
|
||||||
|
LineChartItem
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
|
||||||
import { Tag } from '@prisma/client';
|
import { Tag } from '@prisma/client';
|
||||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { ViewMode } from '@prisma/client';
|
|
||||||
import { EMPTY } from 'rxjs';
|
import { EMPTY } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -80,13 +79,13 @@ export class AuthGuard implements CanActivate {
|
|||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
state.url.startsWith('/home') &&
|
state.url.startsWith('/home') &&
|
||||||
user.settings.viewMode === ViewMode.ZEN
|
user.settings.viewMode === 'ZEN'
|
||||||
) {
|
) {
|
||||||
this.router.navigate(['/zen']);
|
this.router.navigate(['/zen']);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else if (state.url.startsWith('/start')) {
|
} else if (state.url.startsWith('/start')) {
|
||||||
if (user.settings.viewMode === ViewMode.ZEN) {
|
if (user.settings.viewMode === 'ZEN') {
|
||||||
this.router.navigate(['/zen']);
|
this.router.navigate(['/zen']);
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate(['/home']);
|
this.router.navigate(['/home']);
|
||||||
@ -96,7 +95,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
state.url.startsWith('/zen') &&
|
state.url.startsWith('/zen') &&
|
||||||
user.settings.viewMode === ViewMode.DEFAULT
|
user.settings.viewMode === 'DEFAULT'
|
||||||
) {
|
) {
|
||||||
this.router.navigate(['/home']);
|
this.router.navigate(['/home']);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
|
@ -108,7 +108,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 24 hours)"
|
subLabel="(Last 24 hours)"
|
||||||
[value]="statistics?.activeUsers1d ?? '-'"
|
[value]="statistics?.activeUsers1d ?? '-'"
|
||||||
@ -117,7 +116,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 30 days)"
|
subLabel="(Last 30 days)"
|
||||||
[value]="statistics?.newUsers30d ?? '-'"
|
[value]="statistics?.newUsers30d ?? '-'"
|
||||||
@ -126,7 +124,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 30 days)"
|
subLabel="(Last 30 days)"
|
||||||
[value]="statistics?.activeUsers30d ?? '-'"
|
[value]="statistics?.activeUsers30d ?? '-'"
|
||||||
@ -139,7 +136,6 @@
|
|||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.slackCommunityUsers ?? '-'"
|
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||||
>Users in Slack community</gf-value
|
>Users in Slack community</gf-value
|
||||||
@ -152,7 +148,6 @@
|
|||||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.gitHubContributors ?? '-'"
|
[value]="statistics?.gitHubContributors ?? '-'"
|
||||||
>Contributors on GitHub</gf-value
|
>Contributors on GitHub</gf-value
|
||||||
@ -165,7 +160,6 @@
|
|||||||
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.gitHubStargazers ?? '-'"
|
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||||
>Stars on GitHub</gf-value
|
>Stars on GitHub</gf-value
|
||||||
|
@ -54,7 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToUpdateViewMode: boolean;
|
public hasPermissionToUpdateViewMode: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
public language = document.documentElement.lang;
|
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 price: number;
|
||||||
public priceId: string;
|
public priceId: string;
|
||||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
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() {
|
public onCheckout() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId })
|
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId })
|
||||||
@ -226,6 +203,24 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) {
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onRedeemCoupon() {
|
public onRedeemCoupon() {
|
||||||
let couponCode = prompt($localize`Please enter your coupon code:`);
|
let couponCode = prompt($localize`Please enter your coupon code:`);
|
||||||
couponCode = couponCode?.trim();
|
couponCode = couponCode?.trim();
|
||||||
@ -249,7 +244,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.snackBarRef = this.snackBar.open(
|
this.snackBarRef = this.snackBar.open(
|
||||||
'✅' + $localize`Coupon code has been redeemed`,
|
'✅ ' + $localize`Coupon code has been redeemed`,
|
||||||
$localize`Reload`,
|
$localize`Reload`,
|
||||||
{
|
{
|
||||||
duration: 3000
|
duration: 3000
|
||||||
|
@ -94,12 +94,15 @@
|
|||||||
<ng-container i18n>Base Currency</ng-container>
|
<ng-container i18n>Base Currency</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<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
|
<mat-select
|
||||||
name="baseCurrency"
|
name="baseCurrency"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
[value]="user.settings.baseCurrency"
|
[value]="user.settings.baseCurrency"
|
||||||
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)"
|
(selectionChange)="onChangeUserSetting('baseCurrency', $event.value)"
|
||||||
>
|
>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let currency of currencies"
|
*ngFor="let currency of currencies"
|
||||||
@ -116,7 +119,10 @@
|
|||||||
<div class="hint-text text-muted" i18n>Beta</div>
|
<div class="hint-text text-muted" i18n>Beta</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<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
|
<mat-select
|
||||||
name="language"
|
name="language"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
@ -126,6 +132,9 @@
|
|||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option value="de">Deutsch</mat-option>
|
<mat-option value="de">Deutsch</mat-option>
|
||||||
<mat-option value="en">English</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-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -138,7 +147,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<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
|
<mat-select
|
||||||
name="locale"
|
name="locale"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
@ -161,12 +173,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<div class="align-items-center d-flex overflow-hidden">
|
<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
|
<mat-select
|
||||||
name="viewMode"
|
name="viewMode"
|
||||||
[disabled]="!hasPermissionToUpdateViewMode"
|
[disabled]="!hasPermissionToUpdateViewMode"
|
||||||
[value]="user.settings.viewMode"
|
[value]="user.settings.viewMode"
|
||||||
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
(selectionChange)="onChangeUserSetting('viewMode', $event.value)"
|
||||||
>
|
>
|
||||||
<mat-option value="DEFAULT">Default</mat-option>
|
<mat-option value="DEFAULT">Default</mat-option>
|
||||||
<mat-option value="ZEN">Zen</mat-option>
|
<mat-option value="ZEN">Zen</mat-option>
|
||||||
@ -188,6 +203,22 @@
|
|||||||
></mat-slide-toggle>
|
></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
|
||||||
|
class="align-items-center d-flex mt-4 py-1"
|
||||||
|
>
|
||||||
|
<div class="pr-1 w-50">
|
||||||
|
<div i18n>Experimental Features</div>
|
||||||
|
</div>
|
||||||
|
<div class="pl-1 w-50">
|
||||||
|
<mat-slide-toggle
|
||||||
|
color="primary"
|
||||||
|
[checked]="user.settings.isExperimentalFeatures"
|
||||||
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
|
(change)="onExperimentalFeaturesChange($event)"
|
||||||
|
></mat-slide-toggle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="align-items-center d-flex mt-4 py-1">
|
<div class="align-items-center d-flex mt-4 py-1">
|
||||||
<div class="pr-1 w-50" i18n>User ID</div>
|
<div class="pr-1 w-50" i18n>User ID</div>
|
||||||
<div class="pl-1 w-50">{{ user?.id }}</div>
|
<div class="pl-1 w-50">{{ user?.id }}</div>
|
||||||
|
@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.openCreateAccountDialog();
|
this.openCreateAccountDialog();
|
||||||
} else if (params['editDialog']) {
|
} else if (params['editDialog']) {
|
||||||
if (this.accounts) {
|
if (this.accounts) {
|
||||||
const account = this.accounts.find((account) => {
|
const account = this.accounts.find(({ id }) => {
|
||||||
return account.id === params['accountId'];
|
return id === params['accountId'];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.openUpdateAccountDialog(account);
|
this.openUpdateAccountDialog(account);
|
||||||
@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
balance,
|
balance,
|
||||||
currency,
|
currency,
|
||||||
id,
|
id,
|
||||||
|
isExcluded,
|
||||||
name,
|
name,
|
||||||
platformId
|
platformId
|
||||||
}: AccountModel): void {
|
}: AccountModel): void {
|
||||||
@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
balance,
|
balance,
|
||||||
currency,
|
currency,
|
||||||
id,
|
id,
|
||||||
|
isExcluded,
|
||||||
name,
|
name,
|
||||||
platformId
|
platformId
|
||||||
}
|
}
|
||||||
@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
accountType: AccountType.SECURITIES,
|
accountType: AccountType.SECURITIES,
|
||||||
balance: 0,
|
balance: 0,
|
||||||
currency: this.user?.settings?.baseCurrency,
|
currency: this.user?.settings?.baseCurrency,
|
||||||
|
isExcluded: false,
|
||||||
name: null,
|
name: null,
|
||||||
platformId: null
|
platformId: null
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,14 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</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">
|
<div *ngIf="data.account.id">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Account ID</mat-label>
|
<mat-label i18n>Account ID</mat-label>
|
||||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
|
@ -192,6 +192,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</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">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
|
@ -55,6 +55,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="row my-5">
|
||||||
<div class="col-md-6 offset-md-3">
|
<div class="col-md-6 offset-md-3">
|
||||||
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
|
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
|
||||||
@ -133,19 +155,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row my-5">
|
<div class="row my-3">
|
||||||
<div class="col-md-6 offset-md-3">
|
<div class="col-12">
|
||||||
<h2 class="h4 mb-1 text-center">
|
<h2 class="h4 mb-1 text-center">
|
||||||
How does <strong>Ghostfolio</strong> work?
|
How does <strong>Ghostfolio</strong> work?
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
|
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
|
||||||
<ol class="m-0 pl-3">
|
</div>
|
||||||
<li class="mb-2">
|
<div class="col-md-4 my-2">
|
||||||
Sign up anonymously<br />(no e-mail address nor credit card required)
|
<mat-card class="d-flex flex-row h-100">
|
||||||
</li>
|
<div class="flex-grow-1">
|
||||||
<li class="mb-2">Add any of your historical transactions</li>
|
<div class="font-weight-bold">Sign up anonymously*</div>
|
||||||
<li>Get valuable insights of your portfolio composition</li>
|
<div class="text-muted">
|
||||||
</ol>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ import { LandingPageComponent } from './landing-page.component';
|
|||||||
GfLogoModule,
|
GfLogoModule,
|
||||||
LandingPageRoutingModule,
|
LandingPageRoutingModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@ -10,6 +10,30 @@
|
|||||||
></gf-activities-filter>
|
></gf-activities-filter>
|
||||||
</div>
|
</div>
|
||||||
</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="proportion-charts row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||||
@ -22,7 +23,8 @@ import { AllocationsPageComponent } from './allocations-page.component';
|
|||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
GfWorldMapChartModule,
|
GfWorldMapChartModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatCardModule
|
MatCardModule,
|
||||||
|
MatProgressBarModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-card-header-text {
|
.mat-card-header-text {
|
||||||
flex: 1 1 auto;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,14 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
HistoricalDataItem,
|
||||||
|
Position,
|
||||||
|
User
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -18,17 +23,22 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './analysis-page.html'
|
templateUrl: './analysis-page.html'
|
||||||
})
|
})
|
||||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||||
|
public benchmarkDataItems: HistoricalDataItem[] = [];
|
||||||
|
public benchmarks: Partial<SymbolProfile>[];
|
||||||
public bottom3: Position[];
|
public bottom3: Position[];
|
||||||
public daysInMarket: number;
|
public daysInMarket: number;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public firstOrderDate: Date;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public investments: InvestmentItem[];
|
public investments: InvestmentItem[];
|
||||||
public investmentsByMonth: InvestmentItem[];
|
public investmentsByMonth: InvestmentItem[];
|
||||||
|
public isLoadingBenchmarkComparator: boolean;
|
||||||
public mode: GroupBy;
|
public mode: GroupBy;
|
||||||
public modeOptions: ToggleOption[] = [
|
public modeOptions: ToggleOption[] = [
|
||||||
{ label: $localize`Monthly`, value: 'month' },
|
{ label: $localize`Monthly`, value: 'month' },
|
||||||
{ label: $localize`Accumulating`, value: undefined }
|
{ label: $localize`Accumulating`, value: undefined }
|
||||||
];
|
];
|
||||||
|
public performanceDataItems: HistoricalDataItem[];
|
||||||
public top3: Position[];
|
public top3: Position[];
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@ -40,7 +50,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {
|
||||||
|
const { benchmarks } = this.dataService.fetchInfo();
|
||||||
|
this.benchmarks = benchmarks;
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
@ -52,6 +65,82 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChangeBenchmark(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
|
this.dataService
|
||||||
.fetchInvestments()
|
.fetchInvestments()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -91,23 +180,37 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((state) => {
|
|
||||||
if (state?.user) {
|
|
||||||
this.user = state.user;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
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
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeGroupBy(aMode: GroupBy) {
|
this.isLoadingBenchmarkComparator = false;
|
||||||
this.mode = aMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
this.changeDetectorRef.markForCheck();
|
||||||
this.unsubscribeSubject.next();
|
});
|
||||||
this.unsubscribeSubject.complete();
|
} else {
|
||||||
|
this.isLoadingBenchmarkComparator = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,24 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg">
|
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
||||||
<div class="mb-4">
|
<div *ngIf="user?.settings?.isExperimentalFeatures" class="mb-5 row">
|
||||||
<div class="align-items-center d-flex mb-4">
|
<div class="col-lg">
|
||||||
<div
|
<gf-benchmark-comparator
|
||||||
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"
|
class="h-100"
|
||||||
[currency]="user?.settings?.baseCurrency"
|
[benchmark]="user?.settings?.benchmark"
|
||||||
|
[benchmarkDataItems]="benchmarkDataItems"
|
||||||
|
[benchmarks]="benchmarks"
|
||||||
[daysInMarket]="daysInMarket"
|
[daysInMarket]="daysInMarket"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isLoading]="isLoadingBenchmarkComparator"
|
||||||
[investments]="investments"
|
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[ngClass]="{ 'd-none': mode }"
|
[performanceDataItems]="performanceDataItems"
|
||||||
></gf-investment-chart>
|
[user]="user"
|
||||||
<gf-investment-chart
|
(benchmarkChanged)="onChangeBenchmark($event)"
|
||||||
class="h-100"
|
(dateRangeChanged)="onChangeDateRange($event)"
|
||||||
groupBy="month"
|
></gf-benchmark-comparator>
|
||||||
[currency]="user?.settings?.baseCurrency"
|
|
||||||
[daysInMarket]="daysInMarket"
|
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
|
||||||
[investments]="investmentsByMonth"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[ngClass]="{ 'd-none': !mode }"
|
|
||||||
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
|
||||||
></gf-investment-chart>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="mb-5 row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
@ -124,4 +95,49 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg">
|
||||||
|
<div class="align-items-center d-flex mb-4">
|
||||||
|
<div
|
||||||
|
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
|
||||||
|
>
|
||||||
|
<span i18n>Investment Timeline</span>
|
||||||
|
<gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</div>
|
||||||
|
<gf-toggle
|
||||||
|
class="d-none d-lg-block"
|
||||||
|
[defaultValue]="mode"
|
||||||
|
[isLoading]="false"
|
||||||
|
[options]="modeOptions"
|
||||||
|
(change)="onChangeGroupBy($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<gf-investment-chart
|
||||||
|
class="h-100"
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[daysInMarket]="daysInMarket"
|
||||||
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
|
[investments]="investments"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[ngClass]="{ 'd-none': mode }"
|
||||||
|
></gf-investment-chart>
|
||||||
|
<gf-investment-chart
|
||||||
|
class="h-100"
|
||||||
|
groupBy="month"
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[daysInMarket]="daysInMarket"
|
||||||
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
|
[investments]="investmentsByMonth"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[ngClass]="{ 'd-none': !mode }"
|
||||||
|
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
||||||
|
></gf-investment-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { GfBenchmarkComparatorModule } from '@ghostfolio/client/components/benchmark-comparator/benchmark-comparator.module';
|
||||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
@ -15,6 +16,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
AnalysisPageRoutingModule,
|
AnalysisPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfBenchmarkComparatorModule,
|
||||||
GfInvestmentChartModule,
|
GfInvestmentChartModule,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
|
@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioSummary()
|
.fetchPortfolioDetails({})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ cash, currentValue }) => {
|
.subscribe(({ summary }) => {
|
||||||
if (cash === null || currentValue === null) {
|
if (summary.cash === null || summary.currentValue === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fireWealth = new Big(currentValue);
|
this.fireWealth = new Big(summary.currentValue);
|
||||||
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
||||||
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
||||||
|
|
||||||
@ -73,7 +73,18 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ savingsRate })
|
.putUserSetting({ savingsRate })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {});
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -10,6 +10,15 @@
|
|||||||
get started. Due to the time it saves, this will be the best option
|
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.
|
for most people. The revenue is used for covering the hosting costs.
|
||||||
</p>
|
</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>
|
<p>
|
||||||
If you prefer to run Ghostfolio on your own infrastructure, please
|
If you prefer to run Ghostfolio on your own infrastructure, please
|
||||||
find the source code and further instructions on
|
find the source code and further instructions on
|
||||||
|
@ -4,9 +4,8 @@ import { Router } from '@angular/router';
|
|||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
|
||||||
import { Role } from '@prisma/client';
|
import { Role } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
|
@ -12,13 +12,14 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in
|
|||||||
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
||||||
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||||
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||||
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
|
||||||
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
||||||
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
Access,
|
Access,
|
||||||
Accounts,
|
Accounts,
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
|
BenchmarkMarketDataDetails,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
Export,
|
Export,
|
||||||
Filter,
|
Filter,
|
||||||
@ -30,13 +31,13 @@ import {
|
|||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
UniqueAsset,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||||
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { cloneDeep, groupBy } from 'lodash';
|
import { cloneDeep, groupBy } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
@ -181,12 +182,27 @@ export class DataService {
|
|||||||
return this.http.get<Access[]>('/api/v1/access');
|
return this.http.get<Access[]>('/api/v1/access');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchBenchmarkBySymbol({
|
||||||
|
dataSource,
|
||||||
|
startDate,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
startDate: Date;
|
||||||
|
} & UniqueAsset): Observable<BenchmarkMarketDataDetails> {
|
||||||
|
return this.http.get<BenchmarkMarketDataDetails>(
|
||||||
|
`/api/v1/benchmark/${dataSource}/${symbol}/${format(
|
||||||
|
startDate,
|
||||||
|
DATE_FORMAT
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public fetchBenchmarks() {
|
public fetchBenchmarks() {
|
||||||
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchChart({ range }: { range: DateRange }) {
|
public fetchChart({ range, version }: { range: DateRange; version: number }) {
|
||||||
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
|
return this.http.get<PortfolioChart>(`/api/v${version}/portfolio/chart`, {
|
||||||
params: { range }
|
params: { range }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -285,7 +301,11 @@ export class DataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
|
public fetchPortfolioDetails({
|
||||||
|
filters
|
||||||
|
}: {
|
||||||
|
filters?: Filter[];
|
||||||
|
}): Observable<PortfolioDetails> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|
||||||
if (filters?.length > 0) {
|
if (filters?.length > 0) {
|
||||||
@ -331,17 +351,32 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
|
return this.http
|
||||||
|
.get<any>('/api/v1/portfolio/details', {
|
||||||
params
|
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>(
|
return this.http.get<PortfolioPerformanceResponse>(
|
||||||
'/api/v1/portfolio/performance',
|
`/api/v${version}/portfolio/performance`,
|
||||||
{
|
{ params: { range } }
|
||||||
params
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,18 +390,6 @@ export class DataService {
|
|||||||
return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
|
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({
|
public fetchPositionDetail({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
@ -430,10 +453,6 @@ export class DataService {
|
|||||||
return this.http.put<User>(`/api/v1/user/setting`, aData);
|
return this.http.put<User>(`/api/v1/user/setting`, aData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public putUserSettings(aData: UpdateUserSettingsDto) {
|
|
||||||
return this.http.put<User>(`/api/v1/user/settings`, aData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public redeemCoupon(couponCode: string) {
|
public redeemCoupon(couponCode: string) {
|
||||||
return this.http.post('/api/v1/subscription/redeem-coupon', {
|
return this.http.post('/api/v1/subscription/redeem-coupon', {
|
||||||
couponCode
|
couponCode
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ccb2a809018b32a96c813ae69126ce05976109ce" datatype="html">
|
<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>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/app.component.html</context>
|
<context context-type="sourcefile">apps/client/src/app/app.component.html</context>
|
||||||
<context context-type="linenumber">55,56</context>
|
<context context-type="linenumber">55,56</context>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<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>
|
<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>
|
<target state="translated">Aktivitäten</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
<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>
|
<target state="translated">Jobs löschen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
|
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
|
||||||
@ -218,7 +218,7 @@
|
|||||||
<target state="translated">Symbol</target>
|
<target state="translated">Symbol</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
|
<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>
|
<target state="translated">Datenquelle</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
|
<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>
|
<target state="translated">Versuche</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
|
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
|
||||||
@ -262,7 +262,7 @@
|
|||||||
<target state="translated">Erstellt</target>
|
<target state="translated">Erstellt</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
|
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
|
||||||
@ -270,7 +270,7 @@
|
|||||||
<target state="translated">Abgeschlossen</target>
|
<target state="translated">Abgeschlossen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
|
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
|
||||||
@ -278,7 +278,7 @@
|
|||||||
<target state="translated">Status</target>
|
<target state="translated">Status</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
|
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
|
||||||
@ -286,7 +286,7 @@
|
|||||||
<target state="translated">Anlageprofil</target>
|
<target state="translated">Anlageprofil</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
|
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
|
||||||
@ -294,7 +294,7 @@
|
|||||||
<target state="translated">Historische Marktdaten</target>
|
<target state="translated">Historische Marktdaten</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
|
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
|
||||||
@ -302,7 +302,7 @@
|
|||||||
<target state="translated">Daten anzeigen</target>
|
<target state="translated">Daten anzeigen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
|
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
|
||||||
@ -310,7 +310,7 @@
|
|||||||
<target state="translated">Stacktrace anzeigen</target>
|
<target state="translated">Stacktrace anzeigen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
|
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
|
||||||
@ -318,7 +318,7 @@
|
|||||||
<target state="translated">Job löschen</target>
|
<target state="translated">Job löschen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
|
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
|
||||||
@ -370,7 +370,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<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="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>
|
||||||
<context-group purpose="location">
|
<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>
|
<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>
|
||||||
<context-group purpose="location">
|
<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="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>
|
||||||
<context-group purpose="location">
|
<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>
|
<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>
|
<target state="translated">Hinzufügen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
|
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
|
||||||
@ -582,7 +582,7 @@
|
|||||||
<target state="translated">Verwaltung</target>
|
<target state="translated">Verwaltung</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
|
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
|
||||||
@ -590,7 +590,7 @@
|
|||||||
<target state="translated">Cache leeren</target>
|
<target state="translated">Cache leeren</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2817099043823177227" datatype="html">
|
<trans-unit id="2817099043823177227" datatype="html">
|
||||||
@ -642,7 +642,7 @@
|
|||||||
<target state="translated">Aktuelle Marktstimmung</target>
|
<target state="translated">Aktuelle Marktstimmung</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
|
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
|
||||||
<context context-type="linenumber">11</context>
|
<context context-type="linenumber">12</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
||||||
@ -1031,10 +1031,10 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="67251f04518ae452230c68a748b3fa2838b4db74" datatype="html">
|
<trans-unit id="67251f04518ae452230c68a748b3fa2838b4db74" datatype="html">
|
||||||
<source>Net Worth</source>
|
<source>Net Worth</source>
|
||||||
<target state="translated">Reinvermögen</target>
|
<target state="translated">Gesamtvermögen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
|
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
|
||||||
@ -1042,7 +1042,7 @@
|
|||||||
<target state="translated">Performance pro Jahr</target>
|
<target state="translated">Performance pro Jahr</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
|
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
|
||||||
@ -1050,7 +1050,7 @@
|
|||||||
<target state="translated">Dividenden</target>
|
<target state="translated">Dividenden</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6785405835169448749" datatype="html">
|
<trans-unit id="6785405835169448749" datatype="html">
|
||||||
@ -1058,7 +1058,7 @@
|
|||||||
<target state="translated">Bitte gib den Betrag deines Notfallfonds ein:</target>
|
<target state="translated">Bitte gib den Betrag deines Notfallfonds ein:</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
|
||||||
<context context-type="linenumber">48</context>
|
<context context-type="linenumber">52</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
||||||
@ -1172,6 +1172,10 @@
|
|||||||
<trans-unit id="3041670542776846470" datatype="html">
|
<trans-unit id="3041670542776846470" datatype="html">
|
||||||
<source>This feature requires a subscription.</source>
|
<source>This feature requires a subscription.</source>
|
||||||
<target state="translated">Diese Funktion erfordert ein Abonnement.</target>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||||
<context context-type="linenumber">67</context>
|
<context context-type="linenumber">67</context>
|
||||||
@ -1180,6 +1184,10 @@
|
|||||||
<trans-unit id="5499742151525073097" datatype="html">
|
<trans-unit id="5499742151525073097" datatype="html">
|
||||||
<source>Upgrade Plan</source>
|
<source>Upgrade Plan</source>
|
||||||
<target state="translated">Abonnement abschliessen</target>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||||
<context context-type="linenumber">69</context>
|
<context context-type="linenumber">69</context>
|
||||||
@ -1262,7 +1270,7 @@
|
|||||||
<target state="translated">Bitte gebe deinen Gutscheincode ein:</target>
|
<target state="translated">Bitte gebe deinen Gutscheincode ein:</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">230</context>
|
<context context-type="linenumber">225</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4420880039966769543" datatype="html">
|
<trans-unit id="4420880039966769543" datatype="html">
|
||||||
@ -1270,7 +1278,7 @@
|
|||||||
<target state="translated">Gutscheincode konnte nicht eingelöst werden</target>
|
<target state="translated">Gutscheincode konnte nicht eingelöst werden</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">240</context>
|
<context context-type="linenumber">235</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4819099731531004979" datatype="html">
|
<trans-unit id="4819099731531004979" datatype="html">
|
||||||
@ -1278,7 +1286,7 @@
|
|||||||
<target state="translated">Gutscheincode wurde eingelöst</target>
|
<target state="translated">Gutscheincode wurde eingelöst</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">252</context>
|
<context context-type="linenumber">247</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7967484035994732534" datatype="html">
|
<trans-unit id="7967484035994732534" datatype="html">
|
||||||
@ -1286,7 +1294,7 @@
|
|||||||
<target state="translated">Neu laden</target>
|
<target state="translated">Neu laden</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">253</context>
|
<context context-type="linenumber">248</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7963559562180316948" datatype="html">
|
<trans-unit id="7963559562180316948" datatype="html">
|
||||||
@ -1294,7 +1302,7 @@
|
|||||||
<target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target>
|
<target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">299</context>
|
<context context-type="linenumber">294</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
||||||
@ -1323,7 +1331,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="f147d0f7f965cccee2e77294cba8e1b88021fa08" datatype="html">
|
<trans-unit id="f147d0f7f965cccee2e77294cba8e1b88021fa08" datatype="html">
|
||||||
<source>Upgrade</source>
|
<source>Upgrade</source>
|
||||||
<target state="new">Upgrade</target>
|
<target state="translated">Upgrade</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">37</context>
|
<context context-type="linenumber">37</context>
|
||||||
@ -1379,10 +1387,10 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6b939b00e8481ed8aa8a24d8add7a209d7116759" datatype="html">
|
<trans-unit id="6b939b00e8481ed8aa8a24d8add7a209d7116759" datatype="html">
|
||||||
<source>Locale</source>
|
<source>Locale</source>
|
||||||
<target state="new">Locale</target>
|
<target state="translated">Lokalität</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">134</context>
|
<context context-type="linenumber">144</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||||
@ -1390,7 +1398,7 @@
|
|||||||
<target state="translated">Datums- und Zahlenformat</target>
|
<target state="translated">Datums- und Zahlenformat</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">136</context>
|
<context context-type="linenumber">146</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||||
@ -1398,7 +1406,7 @@
|
|||||||
<target state="translated">Ansicht</target>
|
<target state="translated">Ansicht</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">159</context>
|
<context context-type="linenumber">172</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||||
@ -1406,7 +1414,7 @@
|
|||||||
<target state="translated">Einloggen mit Fingerabdruck</target>
|
<target state="translated">Einloggen mit Fingerabdruck</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">180</context>
|
<context context-type="linenumber">196</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||||
@ -1414,7 +1422,7 @@
|
|||||||
<target state="translated">Benutzer ID</target>
|
<target state="translated">Benutzer ID</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">191</context>
|
<context context-type="linenumber">223</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||||
@ -1422,7 +1430,7 @@
|
|||||||
<target state="translated">Zugangsberechtigung</target>
|
<target state="translated">Zugangsberechtigung</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">200</context>
|
<context context-type="linenumber">232</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||||
@ -1516,6 +1524,10 @@
|
|||||||
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
|
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
|
||||||
<source>Platform</source>
|
<source>Platform</source>
|
||||||
<target state="translated">Plattform</target>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
||||||
<context context-type="linenumber">35</context>
|
<context context-type="linenumber">35</context>
|
||||||
@ -1530,7 +1542,7 @@
|
|||||||
<target state="translated">Konto ID</target>
|
<target state="translated">Konto ID</target>
|
||||||
<context-group purpose="location">
|
<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="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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4979019387603946865" datatype="html">
|
<trans-unit id="4979019387603946865" datatype="html">
|
||||||
@ -1618,7 +1630,7 @@
|
|||||||
<target state="translated">Nach Konto</target>
|
<target state="translated">Nach Konto</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
|
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
|
||||||
@ -1626,7 +1638,7 @@
|
|||||||
<target state="translated">Nach Währung</target>
|
<target state="translated">Nach Währung</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
|
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
|
||||||
@ -1634,7 +1646,7 @@
|
|||||||
<target state="translated">Nach Asset Class</target>
|
<target state="translated">Nach Asset Class</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
|
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
|
||||||
@ -1642,7 +1654,7 @@
|
|||||||
<target state="translated">Nach Position</target>
|
<target state="translated">Nach Position</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
|
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
|
||||||
@ -1650,7 +1662,7 @@
|
|||||||
<target state="translated">Nach Sektor</target>
|
<target state="translated">Nach Sektor</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
|
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
|
||||||
@ -1658,7 +1670,7 @@
|
|||||||
<target state="translated">Nach Kontinent</target>
|
<target state="translated">Nach Kontinent</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
|
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
|
||||||
@ -1666,7 +1678,7 @@
|
|||||||
<target state="translated">Nach Land</target>
|
<target state="translated">Nach Land</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
|
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
|
||||||
@ -1674,7 +1686,7 @@
|
|||||||
<target state="translated">Regionen</target>
|
<target state="translated">Regionen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||||
@ -1694,7 +1706,7 @@
|
|||||||
<target state="translated">Analyse</target>
|
<target state="translated">Analyse</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">2</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
|
||||||
@ -1706,7 +1718,7 @@
|
|||||||
<target state="translated">Zeitstrahl der Investitionen</target>
|
<target state="translated">Zeitstrahl der Investitionen</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">10</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
|
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
|
||||||
@ -1714,7 +1726,7 @@
|
|||||||
<target state="translated">Gewinner</target>
|
<target state="translated">Gewinner</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">55</context>
|
<context context-type="linenumber">26</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
|
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
|
||||||
@ -1722,7 +1734,7 @@
|
|||||||
<target state="translated">Verlierer</target>
|
<target state="translated">Verlierer</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">91</context>
|
<context context-type="linenumber">62</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5857197365507636437" datatype="html">
|
<trans-unit id="5857197365507636437" datatype="html">
|
||||||
@ -2036,6 +2048,10 @@
|
|||||||
<trans-unit id="9201103587777813545" datatype="html">
|
<trans-unit id="9201103587777813545" datatype="html">
|
||||||
<source>Portfolio</source>
|
<source>Portfolio</source>
|
||||||
<target state="translated">Portfolio</target>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
|
||||||
<context context-type="linenumber">12</context>
|
<context context-type="linenumber">12</context>
|
||||||
@ -2059,7 +2075,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="a3d148b40a389fda0665eb583c9e434ec5ee1ced" datatype="html">
|
<trans-unit id="a3d148b40a389fda0665eb583c9e434ec5ee1ced" datatype="html">
|
||||||
<source> Ghostfolio empowers you to keep track of your wealth. </source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||||
<context context-type="linenumber">132,134</context>
|
<context context-type="linenumber">132,134</context>
|
||||||
@ -2268,9 +2284,13 @@
|
|||||||
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
|
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
|
||||||
<source>Beta</source>
|
<source>Beta</source>
|
||||||
<target state="translated">Beta</target>
|
<target state="translated">Beta</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
|
||||||
|
<context context-type="linenumber">5</context>
|
||||||
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">116</context>
|
<context context-type="linenumber">119</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
|
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
|
||||||
@ -2286,7 +2306,7 @@
|
|||||||
<target state="translated">Sprache</target>
|
<target state="translated">Sprache</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">115</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
|
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
|
||||||
@ -2410,7 +2430,7 @@
|
|||||||
<target state="translated">Entwickelte Länder</target>
|
<target state="translated">Entwickelte Länder</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||||
@ -2422,7 +2442,7 @@
|
|||||||
<target state="translated">Schwellenländer</target>
|
<target state="translated">Schwellenländer</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<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>
|
<target state="translated">Andere Länder</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<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 context-type="linenumber">136</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</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">
|
<trans-unit id="e34e2478d2d30c9d01758d01b7212411171b9bd5" datatype="html">
|
||||||
<source>Projected Total Amount</source>
|
<source>Projected Total Amount</source>
|
||||||
<target state="translated">Projizierter Gesamtbetrag</target>
|
<target state="translated">Projizierter Gesamtbetrag</target>
|
||||||
@ -2503,7 +2479,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2937311350146031865" datatype="html">
|
<trans-unit id="2937311350146031865" datatype="html">
|
||||||
<source>Initial</source>
|
<source>Initial</source>
|
||||||
<target state="new">Beginn</target>
|
<target state="translated">Beginn</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts</context>
|
||||||
<context context-type="linenumber">57</context>
|
<context context-type="linenumber">57</context>
|
||||||
@ -2522,7 +2498,7 @@
|
|||||||
<target state="translated">Monatlich</target>
|
<target state="translated">Monatlich</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
||||||
<context context-type="linenumber">29</context>
|
<context context-type="linenumber">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1975246224413290232" datatype="html">
|
<trans-unit id="1975246224413290232" datatype="html">
|
||||||
@ -2530,7 +2506,7 @@
|
|||||||
<target state="translated">Aufsummiert</target>
|
<target state="translated">Aufsummiert</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
||||||
<context context-type="linenumber">30</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5213771062241898526" datatype="html">
|
<trans-unit id="5213771062241898526" datatype="html">
|
||||||
@ -2538,7 +2514,7 @@
|
|||||||
<target state="translated">Einlage</target>
|
<target state="translated">Einlage</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
|
||||||
<context context-type="linenumber">125</context>
|
<context context-type="linenumber">132</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
|
||||||
@ -2555,7 +2531,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1054498214311181686" datatype="html">
|
<trans-unit id="1054498214311181686" datatype="html">
|
||||||
<source>Savings</source>
|
<source>Savings</source>
|
||||||
<target state="new">Ersparnisse</target>
|
<target state="translated">Ersparnisse</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
|
||||||
<context context-type="linenumber">296</context>
|
<context context-type="linenumber">296</context>
|
||||||
@ -2598,7 +2574,7 @@
|
|||||||
<target state="translated">Filtern nach...</target>
|
<target state="translated">Filtern nach...</target>
|
||||||
<context-group purpose="location">
|
<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="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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2078421919111943467" datatype="html">
|
<trans-unit id="2078421919111943467" datatype="html">
|
||||||
@ -2641,6 +2617,62 @@
|
|||||||
<context context-type="linenumber">4,7</context>
|
<context context-type="linenumber">4,7</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
|
||||||
|
<source>Experimental Features</source>
|
||||||
|
<target state="translated">Experimentelle Funktionen</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
|
<context context-type="linenumber">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>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
2679
apps/client/src/locales/messages.es.xlf
Normal file
2679
apps/client/src/locales/messages.es.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2679
apps/client/src/locales/messages.it.xlf
Normal file
2679
apps/client/src/locales/messages.it.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2678
apps/client/src/locales/messages.nl.xlf
Normal file
2678
apps/client/src/locales/messages.nl.xlf
Normal file
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<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>
|
<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>
|
<source>Activities</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
||||||
@ -196,14 +196,14 @@
|
|||||||
<source>Delete Jobs</source>
|
<source>Delete Jobs</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
|
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
|
||||||
<source>Symbol</source>
|
<source>Symbol</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
|
<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>
|
<source>Data Source</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
|
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
|
||||||
@ -237,63 +237,63 @@
|
|||||||
<source>Attempts</source>
|
<source>Attempts</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
|
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
|
||||||
<source>Created</source>
|
<source>Created</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
|
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
|
||||||
<source>Finished</source>
|
<source>Finished</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
|
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
|
||||||
<source>Status</source>
|
<source>Status</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
|
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
|
||||||
<source>Asset Profile</source>
|
<source>Asset Profile</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
|
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
|
||||||
<source>Historical Market Data</source>
|
<source>Historical Market Data</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
|
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
|
||||||
<source>View Data</source>
|
<source>View Data</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
|
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
|
||||||
<source>View Stacktrace</source>
|
<source>View Stacktrace</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
|
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
|
||||||
<source>Delete Job</source>
|
<source>Delete Job</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
|
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
|
||||||
@ -341,7 +341,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<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="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>
|
||||||
<context-group purpose="location">
|
<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>
|
<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>
|
||||||
<context-group purpose="location">
|
<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="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>
|
||||||
<context-group purpose="location">
|
<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>
|
<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>
|
<source>Add</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
|
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
|
||||||
<source>Housekeeping</source>
|
<source>Housekeeping</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
|
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
|
||||||
<source>Flush Cache</source>
|
<source>Flush Cache</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2817099043823177227" datatype="html">
|
<trans-unit id="2817099043823177227" datatype="html">
|
||||||
@ -583,7 +583,7 @@
|
|||||||
<source>Current Market Mood</source>
|
<source>Current Market Mood</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
|
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
|
||||||
<context context-type="linenumber">11</context>
|
<context context-type="linenumber">12</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
||||||
@ -936,28 +936,28 @@
|
|||||||
<source>Net Worth</source>
|
<source>Net Worth</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
|
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
|
||||||
<source>Annualized Performance</source>
|
<source>Annualized Performance</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
|
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
|
||||||
<source>Dividend</source>
|
<source>Dividend</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6785405835169448749" datatype="html">
|
<trans-unit id="6785405835169448749" datatype="html">
|
||||||
<source>Please enter the amount of your emergency fund:</source>
|
<source>Please enter the amount of your emergency fund:</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
|
||||||
<context context-type="linenumber">48</context>
|
<context context-type="linenumber">52</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
||||||
@ -1058,6 +1058,10 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3041670542776846470" datatype="html">
|
<trans-unit id="3041670542776846470" datatype="html">
|
||||||
<source>This feature requires a subscription.</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||||
<context context-type="linenumber">67</context>
|
<context context-type="linenumber">67</context>
|
||||||
@ -1065,6 +1069,10 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5499742151525073097" datatype="html">
|
<trans-unit id="5499742151525073097" datatype="html">
|
||||||
<source>Upgrade Plan</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||||
<context context-type="linenumber">69</context>
|
<context context-type="linenumber">69</context>
|
||||||
@ -1137,35 +1145,35 @@
|
|||||||
<source>Please enter your coupon code:</source>
|
<source>Please enter your coupon code:</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">230</context>
|
<context context-type="linenumber">225</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4420880039966769543" datatype="html">
|
<trans-unit id="4420880039966769543" datatype="html">
|
||||||
<source>Could not redeem coupon code</source>
|
<source>Could not redeem coupon code</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">240</context>
|
<context context-type="linenumber">235</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4819099731531004979" datatype="html">
|
<trans-unit id="4819099731531004979" datatype="html">
|
||||||
<source>Coupon code has been redeemed</source>
|
<source>Coupon code has been redeemed</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">252</context>
|
<context context-type="linenumber">247</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7967484035994732534" datatype="html">
|
<trans-unit id="7967484035994732534" datatype="html">
|
||||||
<source>Reload</source>
|
<source>Reload</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">253</context>
|
<context context-type="linenumber">248</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7963559562180316948" datatype="html">
|
<trans-unit id="7963559562180316948" datatype="html">
|
||||||
<source>Do you really want to remove this sign in method?</source>
|
<source>Do you really want to remove this sign in method?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||||
<context context-type="linenumber">299</context>
|
<context context-type="linenumber">294</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
||||||
@ -1243,42 +1251,42 @@
|
|||||||
<source>Locale</source>
|
<source>Locale</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">134</context>
|
<context context-type="linenumber">144</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||||
<source>Date and number format</source>
|
<source>Date and number format</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">136</context>
|
<context context-type="linenumber">146</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||||
<source>View Mode</source>
|
<source>View Mode</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">159</context>
|
<context context-type="linenumber">172</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||||
<source>Sign in with fingerprint</source>
|
<source>Sign in with fingerprint</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">180</context>
|
<context context-type="linenumber">196</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||||
<source>User ID</source>
|
<source>User ID</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">191</context>
|
<context context-type="linenumber">223</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||||
<source>Granted Access</source>
|
<source>Granted Access</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">200</context>
|
<context context-type="linenumber">232</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||||
@ -1362,6 +1370,10 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
|
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
|
||||||
<source>Platform</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
||||||
<context context-type="linenumber">35</context>
|
<context context-type="linenumber">35</context>
|
||||||
@ -1375,7 +1387,7 @@
|
|||||||
<source>Account ID</source>
|
<source>Account ID</source>
|
||||||
<context-group purpose="location">
|
<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="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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4979019387603946865" datatype="html">
|
<trans-unit id="4979019387603946865" datatype="html">
|
||||||
@ -1453,56 +1465,56 @@
|
|||||||
<source>By Account</source>
|
<source>By Account</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
|
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
|
||||||
<source>By Currency</source>
|
<source>By Currency</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
|
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
|
||||||
<source>By Asset Class</source>
|
<source>By Asset Class</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
|
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
|
||||||
<source>By Holding</source>
|
<source>By Holding</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
|
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
|
||||||
<source>By Sector</source>
|
<source>By Sector</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
|
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
|
||||||
<source>By Continent</source>
|
<source>By Continent</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
|
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
|
||||||
<source>By Country</source>
|
<source>By Country</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
|
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
|
||||||
<source>Regions</source>
|
<source>Regions</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||||
@ -1520,7 +1532,7 @@
|
|||||||
<source>Analysis</source>
|
<source>Analysis</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">2</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
|
||||||
@ -1531,21 +1543,21 @@
|
|||||||
<source>Investment Timeline</source>
|
<source>Investment Timeline</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">10</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
|
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
|
||||||
<source>Top</source>
|
<source>Top</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">55</context>
|
<context context-type="linenumber">26</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
|
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
|
||||||
<source>Bottom</source>
|
<source>Bottom</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
|
||||||
<context context-type="linenumber">91</context>
|
<context context-type="linenumber">62</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5857197365507636437" datatype="html">
|
<trans-unit id="5857197365507636437" datatype="html">
|
||||||
@ -1824,6 +1836,10 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9201103587777813545" datatype="html">
|
<trans-unit id="9201103587777813545" datatype="html">
|
||||||
<source>Portfolio</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
|
||||||
<context context-type="linenumber">12</context>
|
<context context-type="linenumber">12</context>
|
||||||
@ -2027,9 +2043,13 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
|
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
|
||||||
<source>Beta</source>
|
<source>Beta</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
|
||||||
|
<context context-type="linenumber">5</context>
|
||||||
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">116</context>
|
<context context-type="linenumber">119</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
|
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
|
||||||
@ -2043,7 +2063,7 @@
|
|||||||
<source>Language</source>
|
<source>Language</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
<context context-type="linenumber">115</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
|
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
|
||||||
@ -2096,7 +2116,7 @@
|
|||||||
<source>Developed Markets</source>
|
<source>Developed Markets</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||||
@ -2136,7 +2156,7 @@
|
|||||||
<source>Other Markets</source>
|
<source>Other Markets</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||||
@ -2147,7 +2167,7 @@
|
|||||||
<source>Emerging Markets</source>
|
<source>Emerging Markets</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
|
<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>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
<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 context-type="linenumber">136</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</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">
|
<trans-unit id="e34e2478d2d30c9d01758d01b7212411171b9bd5" datatype="html">
|
||||||
<source>Projected Total Amount</source>
|
<source>Projected Total Amount</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -2246,7 +2227,7 @@
|
|||||||
<source>Accumulating</source>
|
<source>Accumulating</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
||||||
<context context-type="linenumber">30</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2937311350146031865" datatype="html">
|
<trans-unit id="2937311350146031865" datatype="html">
|
||||||
@ -2267,7 +2248,7 @@
|
|||||||
<source>Deposit</source>
|
<source>Deposit</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
|
||||||
<context context-type="linenumber">125</context>
|
<context context-type="linenumber">132</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
|
||||||
@ -2285,7 +2266,7 @@
|
|||||||
<source>Monthly</source>
|
<source>Monthly</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
|
||||||
<context context-type="linenumber">29</context>
|
<context context-type="linenumber">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html">
|
<trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html">
|
||||||
@ -2331,7 +2312,7 @@
|
|||||||
<source>Filter by...</source>
|
<source>Filter by...</source>
|
||||||
<context-group purpose="location">
|
<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="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>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="303469635941752458" datatype="html">
|
<trans-unit id="303469635941752458" datatype="html">
|
||||||
@ -2359,6 +2340,55 @@
|
|||||||
<context context-type="linenumber">6</context>
|
<context context-type="linenumber">6</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
|
||||||
|
<source>Experimental Features</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||||
|
<context context-type="linenumber">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>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
@ -146,13 +146,6 @@ ngx-skeleton-loader {
|
|||||||
@include gf-table;
|
@include gf-table;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-fab,
|
|
||||||
.mat-flat-button {
|
|
||||||
&.mat-primary {
|
|
||||||
color: rgba(var(--light-primary-text)) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-card {
|
.mat-card {
|
||||||
&:not([class*='mat-elevation-z']) {
|
&:not([class*='mat-elevation-z']) {
|
||||||
border: 1px solid rgba(var(--dark-dividers));
|
border: 1px solid rgba(var(--dark-dividers));
|
||||||
@ -164,6 +157,49 @@ ngx-skeleton-loader {
|
|||||||
margin: 0 !important;
|
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 {
|
.no-min-width {
|
||||||
min-width: unset !important;
|
min-width: unset !important;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ services:
|
|||||||
- ../.env
|
- ../.env
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=prefer
|
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=prefer
|
||||||
|
NODE_ENV: production
|
||||||
REDIS_HOST: 'redis'
|
REDIS_HOST: 'redis'
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
|
@ -6,6 +6,7 @@ services:
|
|||||||
- ../.env
|
- ../.env
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=prefer
|
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=prefer
|
||||||
|
NODE_ENV: production
|
||||||
REDIS_HOST: 'redis'
|
REDIS_HOST: 'redis'
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
|
@ -2,7 +2,15 @@ import { Chart, TooltipPosition } from 'chart.js';
|
|||||||
|
|
||||||
import { getBackgroundColor, getTextColor } from './helper';
|
import { getBackgroundColor, getTextColor } from './helper';
|
||||||
|
|
||||||
export function getTooltipOptions(currency = '', locale = '') {
|
export function getTooltipOptions({
|
||||||
|
currency = '',
|
||||||
|
locale = '',
|
||||||
|
unit = ''
|
||||||
|
}: {
|
||||||
|
currency?: string;
|
||||||
|
locale?: string;
|
||||||
|
unit?: string;
|
||||||
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
backgroundColor: getBackgroundColor(),
|
backgroundColor: getBackgroundColor(),
|
||||||
bodyColor: `rgb(${getTextColor()})`,
|
bodyColor: `rgb(${getTextColor()})`,
|
||||||
@ -20,6 +28,8 @@ export function getTooltipOptions(currency = '', locale = '') {
|
|||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
minimumFractionDigits: 2
|
minimumFractionDigits: 2
|
||||||
})} ${currency}`;
|
})} ${currency}`;
|
||||||
|
} else if (unit) {
|
||||||
|
label += `${context.parsed.y.toFixed(2)} ${unit}`;
|
||||||
} else {
|
} else {
|
||||||
label += context.parsed.y.toFixed(2);
|
label += context.parsed.y.toFixed(2);
|
||||||
}
|
}
|
||||||
|
@ -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_BENCHMARKS = 'BENCHMARKS';
|
||||||
export const PROPERTY_COUPONS = 'COUPONS';
|
export const PROPERTY_COUPONS = 'COUPONS';
|
||||||
export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as currencies from '@dinero.js/currencies';
|
import * as currencies from '@dinero.js/currencies';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
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 { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||||
import { Benchmark } from './interfaces';
|
import { Benchmark } from './interfaces';
|
||||||
@ -42,7 +42,11 @@ export function downloadAsFile({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function encodeDataSource(aDataSource: DataSource) {
|
export function encodeDataSource(aDataSource: DataSource) {
|
||||||
|
if (aDataSource) {
|
||||||
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractNumberFromString(aString: string): number {
|
export function extractNumberFromString(aString: string): number {
|
||||||
@ -71,6 +75,12 @@ export function getCssVariable(aCssVariable: string) {
|
|||||||
export function getDateFnsLocale(aLanguageCode: string) {
|
export function getDateFnsLocale(aLanguageCode: string) {
|
||||||
if (aLanguageCode === 'de') {
|
if (aLanguageCode === 'de') {
|
||||||
return de;
|
return de;
|
||||||
|
} else if (aLanguageCode === 'es') {
|
||||||
|
return es;
|
||||||
|
} else if (aLanguageCode === 'it') {
|
||||||
|
return it;
|
||||||
|
} else if (aLanguageCode === 'nl') {
|
||||||
|
return nl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { LineChartItem } from './line-chart-item.interface';
|
||||||
|
|
||||||
|
export interface BenchmarkMarketDataDetails {
|
||||||
|
marketData: LineChartItem[];
|
||||||
|
}
|
@ -2,5 +2,7 @@ export interface HistoricalDataItem {
|
|||||||
averagePrice?: number;
|
averagePrice?: number;
|
||||||
date: string;
|
date: string;
|
||||||
grossPerformancePercent?: number;
|
grossPerformancePercent?: number;
|
||||||
|
netPerformance?: number;
|
||||||
|
netPerformanceInPercentage?: number;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataItem
|
AdminMarketDataItem
|
||||||
} from './admin-market-data.interface';
|
} from './admin-market-data.interface';
|
||||||
|
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
|
||||||
import { Benchmark } from './benchmark.interface';
|
import { Benchmark } from './benchmark.interface';
|
||||||
import { Coupon } from './coupon.interface';
|
import { Coupon } from './coupon.interface';
|
||||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||||
@ -15,6 +16,7 @@ import { FilterGroup } from './filter-group.interface';
|
|||||||
import { Filter } from './filter.interface';
|
import { Filter } from './filter.interface';
|
||||||
import { HistoricalDataItem } from './historical-data-item.interface';
|
import { HistoricalDataItem } from './historical-data-item.interface';
|
||||||
import { InfoItem } from './info-item.interface';
|
import { InfoItem } from './info-item.interface';
|
||||||
|
import { LineChartItem } from './line-chart-item.interface';
|
||||||
import { PortfolioChart } from './portfolio-chart.interface';
|
import { PortfolioChart } from './portfolio-chart.interface';
|
||||||
import { PortfolioDetails } from './portfolio-details.interface';
|
import { PortfolioDetails } from './portfolio-details.interface';
|
||||||
import { PortfolioInvestments } from './portfolio-investments.interface';
|
import { PortfolioInvestments } from './portfolio-investments.interface';
|
||||||
@ -47,6 +49,7 @@ export {
|
|||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
AdminMarketDataItem,
|
AdminMarketDataItem,
|
||||||
Benchmark,
|
Benchmark,
|
||||||
|
BenchmarkMarketDataDetails,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
Coupon,
|
Coupon,
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
@ -55,6 +58,7 @@ export {
|
|||||||
FilterGroup,
|
FilterGroup,
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
|
LineChartItem,
|
||||||
OAuthResponse,
|
OAuthResponse,
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user