Compare commits

..

41 Commits

Author SHA1 Message Date
7a733ae49b Release 1.189.0 (#1237) 2022-09-08 21:06:37 +02:00
376ce88492 Bugfix/fix benchmark chart (#1236)
* Fix benchmark chart

* Distinguish between currency and unit in tooltip

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

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

* Update changelog
2022-09-03 21:41:06 +02:00
0cc42ffd7c Add end date parameter (#1224)
* Add end date parameter
2022-09-03 21:28:57 +02:00
3ccb812ac3 Release 1.186.2 (#1223) 2022-09-03 11:37:14 +02:00
0a8549db3e Release 1.186.1 (#1222) 2022-09-03 11:19:31 +02:00
c95e90ff31 Release 1.186.0 (#1221) 2022-09-03 10:28:20 +02:00
b59af0d864 Feature/upgrade nx to version 14.6.4 (#1220)
* Upgrade nx and angular

* Update changelog
2022-09-03 10:26:56 +02:00
408bdbd187 Bugfix/fix GitHub contributors count (#1219)
* Fix GitHub contributors count

* Update changelog
2022-09-03 10:06:16 +02:00
a3bfa46fb0 Feature/remove alias from user (#1218)
* Remove alias

* Update changelog
2022-09-03 09:47:18 +02:00
8cb1b3f925 Feature/decrease rate limiter duration (#1217)
* Decrease rate limiter duration

* Update changelog
2022-09-03 09:09:57 +02:00
15c650f951 Bugfix/fix blog post link (#1216)
* Fix link

* Update sitemap.xml
2022-09-03 09:09:37 +02:00
c198bd78da Add architectures (#1205) 2022-09-03 08:32:01 +02:00
35963580bc Bugfix/improve error handling in portfolio calculations (#1215)
* Improve error handling

* Update changelog
2022-09-03 08:31:47 +02:00
cf2c5bad02 Bugfix/change environment variables redis host and port to mandatory (#1211)
* Change REDIS_HOST and REDIS_PORT to mandatory

* Update changelog
2022-09-01 15:35:37 +02:00
f332aea9b4 Release 1.185.0 (#1207) 2022-08-30 20:53:26 +02:00
7a9fd18407 Feature/improve markets overview (#1206)
* Improve markets overview

* Update changelog
2022-08-30 20:52:13 +02:00
ca08d3154a Bugfix/disable language selector for demo user (#1204)
* Disable language selector for demo user

* Update changelog
2022-08-28 21:11:27 +02:00
01d4ae8757 Feature/move build pipeline from travis to GitHub actions (#1203)
* Remove travis configurations

* Update changelog
2022-08-28 21:10:25 +02:00
43ce2786c1 Fetch all history for all tags and branches (#1202) 2022-08-28 11:01:54 +02:00
de2092c4d2 Release 1.184.2 (#1201) 2022-08-28 10:09:59 +02:00
435a180e54 Release 1.184.1 (#1200) 2022-08-28 09:44:09 +02:00
0ad30ffabe Add version to docker tag (#1199) 2022-08-28 09:43:06 +02:00
0cc5e558f1 Release 1.184.0 (#1198) 2022-08-28 09:12:03 +02:00
63b183cc6f Feature/finalize GitHub action to create arm64 docker image (#1197)
* Change order and refactor secrets

* Update changelog
2022-08-28 09:10:18 +02:00
10bae24c5c Build arm64 docker image and use github actions (#1134)
* Setup build pipeline for arm64 docker images using GitHub Actions
2022-08-27 14:37:22 +02:00
0e29278e96 Feature/add alias to access (#1193)
* Add alias to access

* Update changelog
2022-08-27 11:29:09 +02:00
2db46e5bbf Feature/support localization in date fns (#1195)
* Add locale to date-fns (formatDistanceToNow)

* Update changelog
2022-08-27 10:54:59 +02:00
e757e90e5a Improve translation (#1196) 2022-08-27 10:42:54 +02:00
184ddc6209 Bugfix/fix missing assets in local development (#1194)
* Fix missing assets in local development

* Update changelog
2022-08-27 10:29:50 +02:00
e3662a143c Improve localization (#1191)
* Improve localization

* Update changelog
2022-08-26 20:10:59 +02:00
25afd7e07b Remove database:baseline (#1192) 2022-08-26 18:08:05 +02:00
7fceaa1350 Add language to urls (#1187) 2022-08-26 17:52:42 +02:00
84 changed files with 3197 additions and 1805 deletions

36
.github/workflows/build-code.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Build code
on:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node_version:
- 16
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node_version }}
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Check formatting
run: yarn format:check
- name: Execute tests
run: yarn test
- name: Build application
run: yarn build:all

49
.github/workflows/docker-image.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: Docker image CD
on:
push:
tags:
- '*.*.*'
pull_request:
branches:
- 'main'
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ghostfolio/ghostfolio
tags: |
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.output.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,30 +0,0 @@
language: node_js
git:
depth: false
node_js:
- 16
services:
- docker
cache: yarn
if: (type = pull_request) OR (tag IS present)
jobs:
include:
- stage: Install dependencies
if: type = pull_request
script: yarn --frozen-lockfile
- stage: Check formatting
if: type = pull_request
script: yarn format:check
- stage: Execute tests
if: type = pull_request
script: yarn test
- stage: Build application
if: type = pull_request
script: yarn build:all
- stage: Build and publish docker image
if: tag IS present
script: ./publish-docker-image.sh

View File

@ -5,6 +5,79 @@ 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.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
- Decreased the rate limiter duration of queue jobs from 5 to 4 seconds
- Removed the alias from the `User` database schema
- Upgraded `angular` from version `14.1.0` to `14.2.0`
- Upgraded `Nx` from version `14.5.1` to `14.6.4`
### Fixed
- Fixed the environment variables `REDIS_HOST`, `REDIS_PASSWORD` and `REDIS_PORT` in the Redis configuration
- Handled errors in the portfolio calculation if there is no internet connection
- Fixed the _GitHub_ contributors count on the about page
## 1.185.0 - 30.08.2022
### Added
- Added a skeleton loader to the market mood component in the markets overview
### Changed
- Moved the build pipeline from _Travis_ to _GitHub Actions_
- Increased the caching of the benchmarks
### Fixed
- Disabled the language selector for the demo user
## 1.184.2 - 28.08.2022
### Added
- Added the alias to the `Access` database schema
- Added support for translated time distances
- Added a _GitHub Action_ to create an `arm64` docker image
### Changed
- Improved the language localization for German (`de`)
### Fixed
- Fixed the missing assets during the local development
## 1.183.0 - 24.08.2022 ## 1.183.0 - 24.08.2022
### Added ### Added

View File

@ -1,7 +1,6 @@
FROM node:16-alpine as builder FROM --platform=$BUILDPLATFORM node:16-slim as builder
# Build application and add additional files # Build application and add additional files
WORKDIR /ghostfolio WORKDIR /ghostfolio
# Only add basic files without the application itself to avoid rebuilding # Only add basic files without the application itself to avoid rebuilding
@ -10,9 +9,16 @@ COPY ./CHANGELOG.md CHANGELOG.md
COPY ./LICENSE LICENSE COPY ./LICENSE LICENSE
COPY ./package.json package.json COPY ./package.json package.json
COPY ./yarn.lock yarn.lock COPY ./yarn.lock yarn.lock
COPY ./.yarnrc .yarnrc
COPY ./prisma/schema.prisma prisma/schema.prisma COPY ./prisma/schema.prisma prisma/schema.prisma
RUN apk add --no-cache python3 g++ make openssl git RUN apt update && apt install -y \
git \
g++ \
make \
openssl \
python3 \
&& rm -rf /var/lib/apt/lists/*
RUN yarn install RUN yarn install
# See https://github.com/nrwl/nx/issues/6586 for further details # See https://github.com/nrwl/nx/issues/6586 for further details
@ -45,7 +51,11 @@ COPY package.json /ghostfolio/dist/apps/api
RUN yarn database:generate-typings RUN yarn database:generate-typings
# Image to run, copy everything needed from builder # Image to run, copy everything needed from builder
FROM node:16-alpine FROM node:16-slim
RUN apt update && apt install -y \
openssl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
WORKDIR /ghostfolio/apps/api WORKDIR /ghostfolio/apps/api
EXPOSE 3333 EXPOSE 3333

View File

@ -12,13 +12,11 @@
<strong>Open Source Wealth Management Software</strong> <strong>Open Source Wealth Management Software</strong>
</p> </p>
<p> <p>
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/faq"><strong>FAQ</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a> <a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/en/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/en/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/faq"><strong>FAQ</strong></a> | <a href="https://ghostfol.io/en/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
</p> </p>
<p> <p>
<a href="#contributing"> <a href="#contributing">
<img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/></a> <img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/></a>
<a href="https://travis-ci.com/github/ghostfolio/ghostfolio" rel="nofollow">
<img src="https://travis-ci.com/ghostfolio/ghostfolio.svg?branch=main" alt="Build Status"/></a>
<a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow"> <a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow">
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3"/></a> <img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3"/></a>
</p> </p>
@ -33,7 +31,7 @@
## Ghostfolio Premium ## Ghostfolio Premium
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to 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. Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to 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.
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section. If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
@ -81,6 +79,8 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
## Self-hosting ## Self-hosting
We provide official container images hosted on [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio) for `linux/amd64` and `linux/arm64`.
### Supported Environment Variables ### Supported Environment Variables
| Name | Default Value | Description | | Name | Default Value | Description |
@ -94,9 +94,9 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database | | `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database | | `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database | | `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
| `REDIS_HOST` | `localhost` | The host where _Redis_ is running | | `REDIS_HOST` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | | The password of _Redis_ | | `REDIS_PASSWORD` | | The password of _Redis_ |
| `REDIS_PORT` | `6379` | The port where _Redis_ is running | | `REDIS_PORT` | | The port where _Redis_ is running |
### Run with Docker Compose ### Run with Docker Compose
@ -259,7 +259,7 @@ Ghostfolio is **100% free** and **open source**. We encourage and support an act
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you. Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
If you like to support this project, get **[Ghostfolio Premium](https://ghostfol.io/pricing)** or **[Buy me a coffee](https://www.buymeacoffee.com/ghostfolio)**. If you like to support this project, get **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** or **[Buy me a coffee](https://www.buymeacoffee.com/ghostfolio)**.
## License ## License

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default { export default {
displayName: 'api', displayName: 'api',

View File

@ -42,14 +42,16 @@ export class AccessController {
return accessesWithGranteeUser.map((access) => { return accessesWithGranteeUser.map((access) => {
if (access.GranteeUser) { if (access.GranteeUser) {
return { return {
granteeAlias: access.GranteeUser?.alias, alias: access.alias,
grantee: access.GranteeUser?.id,
id: access.id, id: access.id,
type: 'RESTRICTED_VIEW' type: 'RESTRICTED_VIEW'
}; };
} }
return { return {
granteeAlias: 'Public', alias: access.alias,
grantee: 'Public',
id: access.id, id: access.id,
type: 'PUBLIC' type: 'PUBLIC'
}; };
@ -71,6 +73,10 @@ export class AccessController {
} }
return this.accessService.createAccess({ return this.accessService.createAccess({
alias: data.alias || undefined,
GranteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } }
: undefined,
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}); });
} }

View File

@ -1 +1,11 @@
export class CreateAccessDto {} import { IsOptional, IsString } from 'class-validator';
export class CreateAccessDto {
@IsOptional()
@IsString()
alias?: string;
@IsOptional()
@IsString()
granteeUserId?: string;
}

View File

@ -54,7 +54,7 @@ export class WebAuthService {
rpName: 'Ghostfolio', rpName: 'Ghostfolio',
rpID: this.rpID, rpID: this.rpID,
userID: user.id, userID: user.id,
userName: user.alias, userName: '',
timeout: 60000, timeout: 60000,
attestationType: 'indirect', attestationType: 'indirect',
authenticatorSelection: { authenticatorSelection: {

View File

@ -1,30 +1,48 @@
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 { PropertyService } from '@ghostfolio/api/services/property/property.service'; import {
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config'; BenchmarkMarketDataDetails,
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; BenchmarkResponse
import { Controller, Get, UseInterceptors } from '@nestjs/common'; } 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';
@Controller('benchmark') @Controller('benchmark')
export class BenchmarkController { export class BenchmarkController {
public constructor( public constructor(private readonly benchmarkService: BenchmarkService) {}
private readonly benchmarkService: BenchmarkService,
private readonly propertyService: PropertyService
) {}
@Get() @Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> { public async getBenchmark(): Promise<BenchmarkResponse> {
const benchmarkAssets: UniqueAsset[] =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as UniqueAsset[]) ?? [];
return { return {
benchmarks: await this.benchmarkService.getBenchmarks(benchmarkAssets) benchmarks: await this.benchmarkService.getBenchmarks()
}; };
} }
@Get(':dataSource/:symbol/:startDateString')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseGuards(AuthGuard('jwt'))
public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string
): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString);
return this.benchmarkService.getMarketDataBySymbol({
dataSource,
startDate,
symbol
});
}
} }

View File

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

View File

@ -1,10 +1,19 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; import { 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 Big from 'big.js'; import Big from 'big.js';
import { format } from 'date-fns';
import ms from 'ms';
@Injectable() @Injectable()
export class BenchmarkService { export class BenchmarkService {
@ -13,25 +22,36 @@ export class BenchmarkService {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService, private readonly redisCacheService: RedisCacheService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public async getBenchmarks( public calculateChangeInPercentage(baseValue: number, currentValue: number) {
benchmarkAssets: UniqueAsset[] return new Big(currentValue).div(baseValue).minus(1).toNumber();
): Promise<BenchmarkResponse['benchmarks']> { }
public async getBenchmarks({ useCache = true } = {}): Promise<
BenchmarkResponse['benchmarks']
> {
let benchmarks: BenchmarkResponse['benchmarks']; let benchmarks: BenchmarkResponse['benchmarks'];
try { if (useCache) {
benchmarks = JSON.parse( try {
await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS) benchmarks = JSON.parse(
); await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS)
);
if (benchmarks) { if (benchmarks) {
return benchmarks; return benchmarks;
} }
} catch {} } catch {}
}
const benchmarkAssets: UniqueAsset[] =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as UniqueAsset[]) ?? [];
const promises: Promise<number>[] = []; const promises: Promise<number>[] = [];
const [quotes, assetProfiles] = await Promise.all([ const [quotes, assetProfiles] = await Promise.all([
@ -46,14 +66,15 @@ export class BenchmarkService {
const allTimeHighs = await Promise.all(promises); const allTimeHighs = await Promise.all(promises);
benchmarks = allTimeHighs.map((allTimeHigh, index) => { benchmarks = allTimeHighs.map((allTimeHigh, index) => {
const { marketPrice } = quotes[benchmarkAssets[index].symbol]; const { marketPrice } = quotes[benchmarkAssets[index].symbol] ?? {};
let performancePercentFromAllTimeHigh = new Big(0); let performancePercentFromAllTimeHigh = 0;
if (allTimeHigh) { if (allTimeHigh && marketPrice) {
performancePercentFromAllTimeHigh = new Big(marketPrice) performancePercentFromAllTimeHigh = this.calculateChangeInPercentage(
.div(allTimeHigh) allTimeHigh,
.minus(1); marketPrice
);
} }
return { return {
@ -68,7 +89,7 @@ export class BenchmarkService {
})?.name, })?.name,
performances: { performances: {
allTimeHigh: { allTimeHigh: {
performancePercent: performancePercentFromAllTimeHigh.toNumber() performancePercent: performancePercentFromAllTimeHigh
} }
} }
}; };
@ -76,13 +97,67 @@ export class BenchmarkService {
await this.redisCacheService.set( await this.redisCacheService.set(
this.CACHE_KEY_BENCHMARKS, this.CACHE_KEY_BENCHMARKS,
JSON.stringify(benchmarks) JSON.stringify(benchmarks),
ms('4 hours') / 1000
); );
return benchmarks; return benchmarks;
} }
private getMarketCondition(aPerformanceInPercent: Big) { public async getBenchmarkAssetProfiles(): Promise<UniqueAsset[]> {
return aPerformanceInPercent.lte(-0.2) ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; const benchmarkAssets: UniqueAsset[] =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as UniqueAsset[]) ?? [];
const assetProfiles = await this.symbolProfileService.getSymbolProfiles(
benchmarkAssets
);
return assetProfiles.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
});
}
public async getMarketDataBySymbol({
dataSource,
startDate,
symbol
}: { startDate: Date } & UniqueAsset): Promise<BenchmarkMarketDataDetails> {
const marketDataItems = await this.marketDataService.marketDataItems({
orderBy: {
date: 'asc'
},
where: {
dataSource,
symbol,
date: {
gte: startDate
}
}
});
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
return {
marketData: marketDataItems.map((marketDataItem) => {
return {
date: format(marketDataItem.date, DATE_FORMAT),
value:
marketPriceAtStartDate === 0
? 0
: this.calculateChangeInPercentage(
marketPriceAtStartDate,
marketDataItem.marketPrice
) * 100
};
})
};
}
private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
} }
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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 { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; 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';
@ -13,7 +13,10 @@ import {
PROPERTY_SYSTEM_MESSAGE, PROPERTY_SYSTEM_MESSAGE,
ghostfolioFearAndGreedIndexDataSource ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { encodeDataSource } from '@ghostfolio/common/helper'; import {
encodeDataSource,
extractNumberFromString
} from '@ghostfolio/common/helper';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface'; import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
@ -21,6 +24,7 @@ import { permissions } from '@ghostfolio/common/permissions';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as bent from 'bent'; import * as bent from 'bent';
import * as cheerio from 'cheerio';
import { subDays } from 'date-fns'; import { subDays } from 'date-fns';
@Injectable() @Injectable()
@ -28,9 +32,9 @@ 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 dataGatheringService: DataGatheringService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
@ -106,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(),
@ -143,17 +148,21 @@ export class InfoService {
private async countGitHubContributors(): Promise<number> { private async countGitHubContributors(): Promise<number> {
try { try {
const get = bent( const get = bent(
`https://api.github.com/repos/ghostfolio/ghostfolio/contributors`, 'https://github.com/ghostfolio/ghostfolio',
'GET', 'GET',
'json', 'string',
200, 200,
{ {}
'User-Agent': 'request'
}
); );
const contributors = await get(); const html = await get();
return contributors?.length; const $ = cheerio.load(html);
return extractNumberFromString(
$(
`a[href="/ghostfolio/ghostfolio/graphs/contributors"] .Counter`
).text()
);
} catch (error) { } catch (error) {
Logger.error(error, 'InfoService'); Logger.error(error, 'InfoService');

View File

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

View File

@ -35,7 +35,8 @@ 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';
@ -110,6 +111,26 @@ export class PortfolioController {
}; };
} }
@Get('chart')
@UseGuards(AuthGuard('jwt'))
@Version('2')
public async getChartV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') range
): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioService.getChartV2(
impersonationId,
range
);
return {
chart: historicalDataContainer.items,
hasError: false,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
@Get('details') @Get('details')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@ -349,6 +370,7 @@ export class PortfolioController {
const portfolioPublicDetails: PortfolioPublicDetails = { const portfolioPublicDetails: PortfolioPublicDetails = {
hasDetails, hasDetails,
alias: access.alias,
holdings: {} holdings: {}
}; };

View File

@ -57,6 +57,7 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
addDays,
differenceInDays, differenceInDays,
endOfToday, endOfToday,
format, format,
@ -71,7 +72,7 @@ import {
subDays, subDays,
subYears subYears
} from 'date-fns'; } from 'date-fns';
import { isEmpty, sortBy, uniq, uniqBy } from 'lodash'; import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash';
import { import {
HistoricalDataContainer, HistoricalDataContainer,
@ -85,6 +86,7 @@ const emergingMarkets = require('../../assets/countries/emerging-markets.json');
@Injectable() @Injectable()
export class PortfolioService { export class PortfolioService {
private static readonly MAX_CHART_ITEMS = 250;
private baseCurrency: string; private baseCurrency: string;
public constructor( public constructor(
@ -327,10 +329,10 @@ export class PortfolioService {
} }
let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq( let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq(
lastItem?.netPerformance lastItem?.netPerformance ?? 0
); );
let isAllTimeLow = timelineInfo.minNetPerformance?.eq( let isAllTimeLow = timelineInfo.minNetPerformance?.eq(
lastItem?.netPerformance lastItem?.netPerformance ?? 0
); );
if (isAllTimeHigh && isAllTimeLow) { if (isAllTimeHigh && isAllTimeLow) {
isAllTimeHigh = false; isAllTimeHigh = false;
@ -354,6 +356,78 @@ export class PortfolioService {
}; };
} }
public async getChartV2(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: []
};
}
const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
daysInMarket / Math.min(daysInMarket, PortfolioService.MAX_CHART_ITEMS)
);
const items: HistoricalDataItem[] = [];
let currentEndDate = startDate;
while (isBefore(currentEndDate, endDate)) {
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate,
currentEndDate
);
items.push({
date: format(currentEndDate, DATE_FORMAT),
value: currentPositions.netPerformancePercentage.toNumber() * 100
});
currentEndDate = addDays(currentEndDate, step);
}
const today = new Date();
if (last(items)?.date !== format(today, DATE_FORMAT)) {
// Add today
const { netPerformancePercentage } =
await portfolioCalculator.getCurrentPositions(startDate, today);
items.push({
date: format(today, DATE_FORMAT),
value: netPerformancePercentage.toNumber() * 100
});
}
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: items
};
}
public async getDetails( public async getDetails(
aImpersonationId: string, aImpersonationId: string,
aUserId: string, aUserId: string,
@ -466,7 +540,9 @@ export class PortfolioService {
holdings[item.symbol] = { holdings[item.symbol] = {
markets, markets,
allocationCurrent: value.div(totalValue).toNumber(), allocationCurrent: totalValue.eq(0)
? 0
: value.div(totalValue).toNumber(),
allocationInvestment: item.investment.div(totalInvestment).toNumber(), allocationInvestment: item.investment.div(totalInvestment).toNumber(),
assetClass: symbolProfile.assetClass, assetClass: symbolProfile.assetClass,
assetSubClass: symbolProfile.assetSubClass, assetSubClass: symbolProfile.assetSubClass,
@ -478,7 +554,7 @@ export class PortfolioService {
item.grossPerformancePercentage?.toNumber() ?? 0, item.grossPerformancePercentage?.toNumber() ?? 0,
investment: item.investment.toNumber(), investment: item.investment.toNumber(),
marketPrice: item.marketPrice, marketPrice: item.marketPrice,
marketState: dataProviderResponse.marketState, marketState: dataProviderResponse?.marketState ?? 'delayed',
name: symbolProfile.name, name: symbolProfile.name,
netPerformance: item.netPerformance?.toNumber() ?? 0, netPerformance: item.netPerformance?.toNumber() ?? 0,
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0, netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,

View File

@ -1,7 +1,6 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { CacheModule, Module } from '@nestjs/common'; import { CacheManagerOptions, CacheModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as redisStore from 'cache-manager-redis-store'; import * as redisStore from 'cache-manager-redis-store';
import { RedisCacheService } from './redis-cache.service'; import { RedisCacheService } from './redis-cache.service';
@ -9,16 +8,18 @@ import { RedisCacheService } from './redis-cache.service';
@Module({ @Module({
imports: [ imports: [
CacheModule.registerAsync({ CacheModule.registerAsync({
imports: [ConfigModule], imports: [ConfigurationModule],
inject: [ConfigService], inject: [ConfigurationService],
useFactory: async (configurationService: ConfigurationService) => ({ useFactory: async (configurationService: ConfigurationService) => {
host: configurationService.get('REDIS_HOST'), return <CacheManagerOptions>{
max: configurationService.get('MAX_ITEM_IN_CACHE'), host: configurationService.get('REDIS_HOST'),
password: configurationService.get('REDIS_PASSWORD'), max: configurationService.get('MAX_ITEM_IN_CACHE'),
port: configurationService.get('REDIS_PORT'), password: configurationService.get('REDIS_PASSWORD'),
store: redisStore, port: configurationService.get('REDIS_PORT'),
ttl: configurationService.get('CACHE_TTL') store: redisStore,
}) ttl: configurationService.get('CACHE_TTL')
};
}
}), }),
ConfigurationModule ConfigurationModule
], ],

View File

@ -5,6 +5,10 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
emergencyFund?: number; emergencyFund?: number;
@IsBoolean()
@IsOptional()
isExperimentalFeatures?: boolean;
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
isRestrictedView?: boolean; isRestrictedView?: boolean;

View File

@ -43,7 +43,7 @@ export class UserService {
include: { include: {
User: true User: true
}, },
orderBy: { User: { alias: 'asc' } }, orderBy: { alias: 'asc' },
where: { GranteeUser: { id } } where: { GranteeUser: { id } }
}); });
let tags = await this.tagService.getByUser(id); let tags = await this.tagService.getByUser(id);
@ -62,7 +62,7 @@ export class UserService {
tags, tags,
access: access.map((accessItem) => { access: access.map((accessItem) => {
return { return {
alias: accessItem.User.alias, alias: accessItem.alias,
id: accessItem.id id: accessItem.id
}; };
}), }),
@ -98,7 +98,6 @@ export class UserService {
const { const {
accessToken, accessToken,
Account, Account,
alias,
authChallenge, authChallenge,
createdAt, createdAt,
id, id,
@ -116,7 +115,6 @@ export class UserService {
const user: UserWithSettings = { const user: UserWithSettings = {
accessToken, accessToken,
Account, Account,
alias,
authChallenge, authChallenge,
createdAt, createdAt,
id, id,

View File

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

View File

@ -17,7 +17,7 @@ import { SymbolProfileModule } from './symbol-profile.module';
imports: [ imports: [
BullModule.registerQueue({ BullModule.registerQueue({
limiter: { limiter: {
duration: ms('5 seconds'), duration: ms('4 seconds'),
max: 1 max: 1
}, },
name: DATA_GATHERING_QUEUE name: DATA_GATHERING_QUEUE

View File

@ -6,7 +6,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import {
DATE_FORMAT,
extractNumberFromString,
getYesterday
} 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';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
@ -16,8 +20,6 @@ import { addDays, format, isBefore } from 'date-fns';
@Injectable() @Injectable()
export class GhostfolioScraperApiService implements DataProviderInterface { export class GhostfolioScraperApiService implements DataProviderInterface {
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
public constructor( public constructor(
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
@ -77,7 +79,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
const html = await get(); const html = await get();
const $ = cheerio.load(html); const $ = cheerio.load(html);
const value = this.extractNumberFromString($(selector).text()); const value = extractNumberFromString($(selector).text());
return { return {
[symbol]: { [symbol]: {
@ -175,15 +177,4 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return { items }; return { items };
} }
private extractNumberFromString(aString: string): number {
try {
const [numberString] = aString.match(
GhostfolioScraperApiService.NUMERIC_REGEXP
);
return parseFloat(numberString.trim());
} catch {
return undefined;
}
}
} }

View File

@ -1,13 +1,12 @@
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';
@Module({ @Module({
exports: [TwitterBotService], exports: [TwitterBotService],
imports: [BenchmarkModule, ConfigurationModule, PropertyModule, SymbolModule], imports: [BenchmarkModule, ConfigurationModule, SymbolModule],
providers: [TwitterBotService] providers: [TwitterBotService]
}) })
export class TwitterBotModule {} export class TwitterBotModule {}

View File

@ -1,9 +1,7 @@
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service'; import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
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 { import {
PROPERTY_BENCHMARKS,
ghostfolioFearAndGreedIndexDataSource, ghostfolioFearAndGreedIndexDataSource,
ghostfolioFearAndGreedIndexSymbol ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -11,7 +9,6 @@ import {
resolveFearAndGreedIndex, resolveFearAndGreedIndex,
resolveMarketCondition resolveMarketCondition
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { isWeekend } from 'date-fns'; import { isWeekend } from 'date-fns';
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2'; import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
@ -23,7 +20,6 @@ export class TwitterBotService {
public constructor( public constructor(
private readonly benchmarkService: BenchmarkService, private readonly benchmarkService: BenchmarkService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly propertyService: PropertyService,
private readonly symbolService: SymbolService private readonly symbolService: SymbolService
) { ) {
this.twitterClient = new TwitterApi({ this.twitterClient = new TwitterApi({
@ -82,14 +78,9 @@ export class TwitterBotService {
} }
private async getBenchmarkListing(aMax: number) { private async getBenchmarkListing(aMax: number) {
const benchmarkAssets: UniqueAsset[] = const benchmarks = await this.benchmarkService.getBenchmarks({
((await this.propertyService.getByKey( useCache: false
PROPERTY_BENCHMARKS });
)) as UniqueAsset[]) ?? [];
const benchmarks = await this.benchmarkService.getBenchmarks(
benchmarkAssets
);
const benchmarkListing: string[] = []; const benchmarkListing: string[] = [];

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default { export default {
displayName: 'client', displayName: 'client',

View File

@ -2,5 +2,13 @@
"/api": { "/api": {
"target": "http://localhost:3333", "target": "http://localhost:3333",
"secure": false "secure": false
},
"/assets": {
"target": "http://localhost:3333",
"secure": false
},
"/ionicons": {
"target": "http://localhost:3333",
"secure": false
} }
} }

View File

@ -1,8 +1,15 @@
<table class="gf-table w-100" mat-table [dataSource]="dataSource"> <table class="gf-table w-100" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="granteeAlias"> <ng-container matColumnDef="alias">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ element.alias }}
</td>
</ng-container>
<ng-container matColumnDef="grantee">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
{{ element.granteeAlias }} {{ element.grantee }}
</td> </td>
</ng-container> </ng-container>

View File

@ -33,7 +33,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
public ngOnInit() {} public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
this.displayedColumns = ['granteeAlias', 'type', 'details']; this.displayedColumns = ['alias', 'grantee', 'type', 'details'];
if (this.showActions) { if (this.showActions) {
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');

View File

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

View File

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

View File

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

View File

@ -0,0 +1,39 @@
<div class="align-items-center 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>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Compare with...</mat-label>
<mat-select
name="benchmark"
[value]="benchmark"
(selectionChange)="onChangeBenchmark($event.value)"
>
<mat-option *ngFor="let benchmark of benchmarks" [value]="benchmark">{{
benchmark.symbol
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="chart-container">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '100%',
width: '100%'
}"
></ngx-skeleton-loader>
<canvas
#chartCanvas
class="h-100"
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
></canvas>
</div>

View File

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

View File

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

View File

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

View File

@ -1,13 +1,23 @@
<div class="align-items-center d-flex flex-row"> <div class="position-relative">
<div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div> <div class="align-items-center d-flex flex-row" [hidden]="!fearAndGreedIndex">
<div> <div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
<div class="h4 mb-0"> <div>
<span class="mr-2">{{ fearAndGreedIndexText }}</span> <div class="h4 mb-0">
<small class="text-muted" <span class="mr-2">{{ fearAndGreedIndexText }}</span>
><strong>{{ fearAndGreedIndex }}</strong <small class="text-muted"
>/100</small ><strong>{{ fearAndGreedIndex }}</strong
> >/100</small
>
</div>
<small class="d-block" i18n>Current Market Mood</small>
</div> </div>
<small class="d-block" i18n>Current Market Mood</small>
</div> </div>
<ngx-skeleton-loader
*ngIf="!fearAndGreedIndex"
animation="pulse"
class="position-absolute w-100"
[theme]="{
height: '100%'
}"
></ngx-skeleton-loader>
</div> </div>

View File

@ -1,3 +1,8 @@
:host { :host {
display: block; display: block;
ngx-skeleton-loader {
bottom: 0;
top: 0;
}
} }

View File

@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { FearAndGreedIndexComponent } from './fear-and-greed-index.component'; import { FearAndGreedIndexComponent } from './fear-and-greed-index.component';
@NgModule({ @NgModule({
declarations: [FearAndGreedIndexComponent], declarations: [FearAndGreedIndexComponent],
exports: [FearAndGreedIndexComponent], exports: [FearAndGreedIndexComponent],
imports: [CommonModule] imports: [CommonModule, NgxSkeletonLoaderModule]
}) })
export class GfFearAndGreedIndexModule {} export class GfFearAndGreedIndexModule {}

View File

@ -20,7 +20,6 @@
<gf-fear-and-greed-index <gf-fear-and-greed-index
class="d-flex justify-content-center" class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex" [fearAndGreedIndex]="fearAndGreedIndex"
[hidden]="isLoading"
></gf-fear-and-greed-index> ></gf-fear-and-greed-index>
</div> </div>
</div> </div>

View File

@ -8,13 +8,13 @@ import {
} from '@ghostfolio/client/services/settings-storage.service'; } 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';
@ -106,7 +106,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.isLoadingPerformance = true; this.isLoadingPerformance = true;
this.dataService this.dataService
.fetchChart({ range: this.dateRange }) .fetchChart({
range: this.dateRange,
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => { .subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => { this.historicalDataItems = chartData.chart.map((chartDataItem) => {

View File

@ -15,7 +15,7 @@
<gf-line-chart <gf-line-chart
class="position-absolute" class="position-absolute"
symbol="Performance" symbol="Performance"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.isExperimentalFeatures ? undefined : user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems" [historicalDataItems]="historicalDataItems"
[hidden]="historicalDataItems?.length === 0" [hidden]="historicalDataItems?.length === 0"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -24,6 +24,7 @@
[showLoader]="false" [showLoader]="false"
[showXAxis]="false" [showXAxis]="false"
[showYAxis]="false" [showYAxis]="false"
[unit]="user?.settings?.isExperimentalFeatures ? '%' : undefined"
></gf-line-chart> ></gf-line-chart>
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings" [hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
[isLoading]="isLoading" [isLoading]="isLoading"
[language]="user?.settings?.language"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[summary]="summary" [summary]="summary"
(emergencyFundChanged)="onChangeEmergencyFund($event)" (emergencyFundChanged)="onChangeEmergencyFund($event)"

View File

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

View File

@ -7,6 +7,7 @@ import {
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { getDateFnsLocale } from '@ghostfolio/common/helper';
import { PortfolioSummary } from '@ghostfolio/common/interfaces'; import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
@ -20,6 +21,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() hasPermissionToUpdateUserSettings: boolean; @Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean; @Input() isLoading: boolean;
@Input() language: string;
@Input() locale: string; @Input() locale: string;
@Input() summary: PortfolioSummary; @Input() summary: PortfolioSummary;
@ -34,7 +36,9 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
public ngOnChanges() { public ngOnChanges() {
if (this.summary) { if (this.summary) {
if (this.summary.firstOrderDate) { if (this.summary.firstOrderDate) {
this.timeInMarket = formatDistanceToNow(this.summary.firstOrderDate); this.timeInMarket = formatDistanceToNow(this.summary.firstOrderDate, {
locale: getDateFnsLocale(this.language)
});
} else { } else {
this.timeInMarket = '-'; this.timeInMarket = '-';
} }

View File

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

View File

@ -226,6 +226,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();
@ -316,6 +334,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, { const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
data: { data: {
access: { access: {
alias: '',
type: 'PUBLIC' type: 'PUBLIC'
} }
}, },
@ -331,7 +350,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
if (access) { if (access) {
this.dataService this.dataService
.postAccess({}) .postAccess({ alias: access.alias })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe({ .subscribe({
next: () => { next: () => {

View File

@ -119,6 +119,7 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-select <mat-select
name="language" name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="language" [value]="language"
(selectionChange)="onChangeUserSetting('language', $event.value)" (selectionChange)="onChangeUserSetting('language', $event.value)"
> >
@ -187,6 +188,22 @@
></mat-slide-toggle> ></mat-slide-toggle>
</div> </div>
</div> </div>
<div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="align-items-center d-flex mt-4 py-1"
>
<div class="pr-1 w-50">
<div i18n>Experimental Features</div>
</div>
<div class="pl-1 w-50">
<mat-slide-toggle
color="primary"
[checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)"
></mat-slide-toggle>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1"> <div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>User ID</div> <div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 w-50">{{ user?.id }}</div> <div class="pl-1 w-50">{{ user?.id }}</div>

View File

@ -1,6 +1,17 @@
<form #addAccessForm="ngForm" class="d-flex flex-column h-100"> <form #addAccessForm="ngForm" class="d-flex flex-column h-100">
<h1 i18n mat-dialog-title>Grant access</h1> <h1 i18n mat-dialog-title>Grant access</h1>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Alias</mat-label>
<input
matInput
name="alias"
type="text"
[(ngModel)]="data.access.alias"
/>
</mat-form-field>
</div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label> <mat-label i18n>Type</mat-label>

View File

@ -4,6 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
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 { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component'; import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component';
@ -16,6 +17,7 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule,
MatSelectModule, MatSelectModule,
ReactiveFormsModule ReactiveFormsModule
] ]

View File

@ -84,7 +84,7 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex w-100" class="d-flex w-100"
href="'../en/blog/2022/01/ghostfolio-first-months-in-open-source" href="../en/blog/2022/01/ghostfolio-first-months-in-open-source"
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate"> <div class="h6 m-0 text-truncate">

View File

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

View File

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

View File

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

View File

@ -38,7 +38,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public user: User; public user: User;
private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...';
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@ -84,7 +83,9 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
this.isLoading = true; this.isLoading = true;
this.activeFilters = filters; this.activeFilters = filters;
this.placeholder = this.placeholder =
this.activeFilters.length <= 0 ? this.SEARCH_PLACEHOLDER : ''; this.activeFilters.length <= 0
? $localize`Filter by account or tag...`
: '';
return this.dataService.fetchPortfolioDetails({ return this.dataService.fetchPortfolioDetails({
filters: this.activeFilters filters: this.activeFilters

View File

@ -2,7 +2,8 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="h4 mb-3 text-center" i18n> <h3 class="h4 mb-3 text-center" i18n>
Hello, someone has shared a <strong>Portfolio</strong> with you! Hello, {{ portfolioPublicDetails?.alias ?? 'someone' }} has shared a
<strong>Portfolio</strong> with you!
</h3> </h3>
</div> </div>
</div> </div>

View File

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

View File

@ -14,11 +14,13 @@ import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interfac
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 { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
Access, Access,
Accounts, Accounts,
AdminData, AdminData,
AdminMarketData, AdminMarketData,
BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
Export, Export,
Filter, Filter,
@ -31,12 +33,13 @@ import {
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { AccountWithValue, DateRange } from '@ghostfolio/common/types'; import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
import { DataSource, Order as OrderModel } from '@prisma/client'; import { DataSource, Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { cloneDeep, groupBy } from 'lodash'; import { cloneDeep, groupBy } from 'lodash';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -181,12 +184,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 }
}); });
} }

View File

@ -6,70 +6,70 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url> <url>
<loc>https://ghostfol.io</loc> <loc>https://ghostfol.io</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc> <loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/about</loc> <loc>https://ghostfol.io/en/about</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/about/changelog</loc> <loc>https://ghostfol.io/en/about/changelog</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog</loc> <loc>https://ghostfol.io/en/blog</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc> <loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc> <loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc> <loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc> <loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc> <loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/demo</loc> <loc>https://ghostfol.io/en/demo</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/faq</loc> <loc>https://ghostfol.io/en/faq</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/features</loc> <loc>https://ghostfol.io/en/features</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/markets</loc> <loc>https://ghostfol.io/en/markets</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/pricing</loc> <loc>https://ghostfol.io/en/pricing</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/register</loc> <loc>https://ghostfol.io/en/register</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/resources</loc> <loc>https://ghostfol.io/en/resources</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod> <lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url> </url>
</urlset> </urlset>

View File

@ -26,7 +26,7 @@
<target state="translated">Empfänger</target> <target state="translated">Empfänger</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">3</context> <context context-type="linenumber">10</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html"> <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
@ -34,7 +34,7 @@
<target state="translated">Typ</target> <target state="translated">Typ</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">17</context>
</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>
@ -42,7 +42,7 @@
</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>
<context context-type="linenumber">6</context> <context context-type="linenumber">17</context>
</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>
@ -62,7 +62,7 @@
<target state="translated">Details</target> <target state="translated">Details</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">27</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="53ea2772322e7d4d21515bb0c6dead283f8e18a5" datatype="html"> <trans-unit id="53ea2772322e7d4d21515bb0c6dead283f8e18a5" datatype="html">
@ -70,7 +70,7 @@
<target state="translated">Widerrufen</target> <target state="translated">Widerrufen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">47</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8264698726451826067" datatype="html"> <trans-unit id="8264698726451826067" datatype="html">
@ -131,7 +131,7 @@
</trans-unit> </trans-unit>
<trans-unit id="d04d5b5d13ac9acf9750f1807f0227eeee98b247" datatype="html"> <trans-unit id="d04d5b5d13ac9acf9750f1807f0227eeee98b247" datatype="html">
<source>Total</source> <source>Total</source>
<target state="translated">Total</target> <target state="translated">Gesamt</target>
<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">18</context> <context context-type="linenumber">18</context>
@ -366,7 +366,7 @@
</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>
<context context-type="linenumber">14</context> <context context-type="linenumber">25</context>
</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>
@ -390,7 +390,7 @@
</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>
<context context-type="linenumber">21</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/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>
@ -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">
@ -798,7 +798,7 @@
<target state="translated">Registrieren</target> <target state="translated">Registrieren</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">136</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5207635742003539443" datatype="html"> <trans-unit id="5207635742003539443" 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">
@ -1070,7 +1070,7 @@
</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>
<context context-type="linenumber">44</context> <context context-type="linenumber">45</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="aee7d0de4e30405c1289745e4264e622b613632a" datatype="html"> <trans-unit id="aee7d0de4e30405c1289745e4264e622b613632a" datatype="html">
@ -1262,7 +1262,7 @@
<target state="translated">Bitte gebe deinen Gutscheincode ein:</target> <target state="translated">Bitte gebe deinen Gutscheincode ein:</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">248</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4420880039966769543" datatype="html"> <trans-unit id="4420880039966769543" datatype="html">
@ -1270,7 +1270,7 @@
<target state="translated">Gutscheincode konnte nicht eingelöst werden</target> <target state="translated">Gutscheincode konnte nicht eingelöst werden</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">240</context> <context context-type="linenumber">258</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4819099731531004979" datatype="html"> <trans-unit id="4819099731531004979" datatype="html">
@ -1278,7 +1278,7 @@
<target state="translated">Gutscheincode wurde eingelöst</target> <target state="translated">Gutscheincode wurde eingelöst</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">252</context> <context context-type="linenumber">270</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7967484035994732534" datatype="html"> <trans-unit id="7967484035994732534" datatype="html">
@ -1286,7 +1286,7 @@
<target state="translated">Neu laden</target> <target state="translated">Neu laden</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">253</context> <context context-type="linenumber">271</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7963559562180316948" datatype="html"> <trans-unit id="7963559562180316948" datatype="html">
@ -1294,7 +1294,7 @@
<target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target> <target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">299</context> <context context-type="linenumber">317</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html"> <trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1382,7 +1382,7 @@
<target state="new">Locale</target> <target state="new">Locale</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">134</context> <context context-type="linenumber">135</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html"> <trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
@ -1390,7 +1390,7 @@
<target state="translated">Datums- und Zahlenformat</target> <target state="translated">Datums- und Zahlenformat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html"> <trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
@ -1398,7 +1398,7 @@
<target state="translated">Ansicht</target> <target state="translated">Ansicht</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">160</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html"> <trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
@ -1406,7 +1406,7 @@
<target state="translated">Einloggen mit Fingerabdruck</target> <target state="translated">Einloggen mit Fingerabdruck</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">181</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html"> <trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
@ -1414,7 +1414,7 @@
<target state="translated">Benutzer ID</target> <target state="translated">Benutzer ID</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">191</context> <context context-type="linenumber">208</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html"> <trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
@ -1422,7 +1422,7 @@
<target state="translated">Zugangsberechtigung</target> <target state="translated">Zugangsberechtigung</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">200</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html"> <trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1438,7 +1438,7 @@
<target state="translated">Öffentlich</target> <target state="translated">Öffentlich</target>
<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>
<context context-type="linenumber">8</context> <context context-type="linenumber">19</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5016419499983434110" datatype="html"> <trans-unit id="5016419499983434110" datatype="html">
@ -1678,7 +1678,7 @@
</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>
<context context-type="linenumber">75</context> <context context-type="linenumber">76</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2948175671993825247" datatype="html"> <trans-unit id="2948175671993825247" datatype="html">
@ -1694,7 +1694,7 @@
<target state="translated">Analyse</target> <target state="translated">Analyse</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">2</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
@ -1706,7 +1706,7 @@
<target state="translated">Zeitstrahl der Investitionen</target> <target state="translated">Zeitstrahl der Investitionen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">102</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html"> <trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
@ -1714,7 +1714,7 @@
<target state="translated">Gewinner</target> <target state="translated">Gewinner</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html"> <trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
@ -1722,7 +1722,7 @@
<target state="translated">Verlierer</target> <target state="translated">Verlierer</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">59</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5857197365507636437" datatype="html"> <trans-unit id="5857197365507636437" datatype="html">
@ -1782,7 +1782,7 @@
</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>
<context context-type="linenumber">13</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="888f6842631dc20550ad7e9a9c45ef55bf45852b" datatype="html"> <trans-unit id="888f6842631dc20550ad7e9a9c45ef55bf45852b" datatype="html">
@ -2041,20 +2041,12 @@
<context context-type="linenumber">12</context> <context context-type="linenumber">12</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7081e73933b030c02e11519f76884ef7cb5d99fc" datatype="html">
<source> Hello, someone has shared a <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> with you! </source>
<target state="new"/>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
<context context-type="linenumber">4,6</context>
</context-group>
</trans-unit>
<trans-unit id="4056a76e7a710ab32285892a58d66f2d1a927796" datatype="html"> <trans-unit id="4056a76e7a710ab32285892a58d66f2d1a927796" datatype="html">
<source>Currencies</source> <source>Currencies</source>
<target state="translated">Währungen</target> <target state="translated">Währungen</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">29</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3c3163d370916438f3b52ea17720bfb2a68a1709" datatype="html"> <trans-unit id="3c3163d370916438f3b52ea17720bfb2a68a1709" datatype="html">
@ -2062,7 +2054,7 @@
<target state="translated">Kontinente</target> <target state="translated">Kontinente</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">59</context> <context context-type="linenumber">60</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="a3d148b40a389fda0665eb583c9e434ec5ee1ced" datatype="html"> <trans-unit id="a3d148b40a389fda0665eb583c9e434ec5ee1ced" datatype="html">
@ -2070,7 +2062,7 @@
<target state="new"/> <target state="new"/>
<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">131,133</context> <context context-type="linenumber">132,134</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8298333184054476827" datatype="html"> <trans-unit id="8298333184054476827" datatype="html">
@ -2206,7 +2198,7 @@
<target state="translated">Möchtest du diese Aktivität wirklich löschen?</target> <target state="translated">Möchtest du diese Aktivität wirklich löschen?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">134</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -2276,6 +2268,10 @@
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html"> <trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
<source>Beta</source> <source>Beta</source>
<target state="translated">Beta</target> <target state="translated">Beta</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">116</context>
@ -2422,7 +2418,7 @@
</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>
<context context-type="linenumber">89</context> <context context-type="linenumber">90</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="81eb53c18dfd116d6e54877444847b3091d92ab0" datatype="html"> <trans-unit id="81eb53c18dfd116d6e54877444847b3091d92ab0" datatype="html">
@ -2434,7 +2430,7 @@
</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>
<context context-type="linenumber">98</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7233cd3a1ef8913fa5c6db7a29c88044646ceacc" datatype="html"> <trans-unit id="7233cd3a1ef8913fa5c6db7a29c88044646ceacc" datatype="html">
@ -2446,7 +2442,7 @@
</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>
<context context-type="linenumber">107</context> <context context-type="linenumber">108</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="add4cd82e3e38a3110fe67b3c7df56e9602644ee" datatype="html"> <trans-unit id="add4cd82e3e38a3110fe67b3c7df56e9602644ee" datatype="html">
@ -2503,7 +2499,7 @@
</trans-unit> </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">Geschätzter Gesamtbetrag</target> <target state="translated">Projizierter Gesamtbetrag</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">44</context>
@ -2530,7 +2526,7 @@
<target state="translated">Monatlich</target> <target state="translated">Monatlich</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1975246224413290232" datatype="html"> <trans-unit id="1975246224413290232" datatype="html">
@ -2538,15 +2534,19 @@
<target state="translated">Aufsummiert</target> <target state="translated">Aufsummiert</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5213771062241898526" datatype="html"> <trans-unit id="5213771062241898526" datatype="html">
<source>Deposit</source> <source>Deposit</source>
<target state="translated">Einlage</target> <target state="translated">Einlage</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">131</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
<context context-type="linenumber">125</context> <context context-type="linenumber">130</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
@ -2616,6 +2616,70 @@
<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">136</context> <context context-type="linenumber">136</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts</context>
<context context-type="linenumber">87</context>
</context-group>
</trans-unit>
<trans-unit id="303469635941752458" datatype="html">
<source>Filter by account, currency, symbol or type...</source>
<target state="translated">Filtern nach Konto, Währung, Symbol oder Typ...</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">291</context>
</context-group>
</trans-unit>
<trans-unit id="fbaaeb297e70b9a800acf841b9d26c19d60651ef" datatype="html">
<source>Alias</source>
<target state="translated">Alias</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
<context context-type="linenumber">6</context>
</context-group>
</trans-unit>
<trans-unit id="3d14940af7de691ac27efb67bef3e974cbe3281c" datatype="html">
<source> Hello, <x id="INTERPOLATION" equiv-text="{{ portfolioPublicDetails?.alias ?? &apos;someone&apos; }}"/> has shared a <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> with you! </source>
<target state="translated"> Hallo, <x id="INTERPOLATION" equiv-text="{{ portfolioPublicDetails?.alias ?? &apos;jemand&apos; }}"/> hat ein <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> mit dir geteilt! </target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
<context context-type="linenumber">4,7</context>
</context-group>
</trans-unit>
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
<source>Experimental Features</source>
<target state="translated">Experimentelle Funktionen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context>
</context-group>
</trans-unit>
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
<source>Benchmarks</source>
<target state="translated">Benchmarks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="44fcf77e86dc038202ebad6b46d1d833d60d781b" datatype="html">
<source>Compare with...</source>
<target state="translated">Vergleichen mit...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
<trans-unit id="1931353503905413384" datatype="html">
<source>Benchmark</source>
<target state="translated">Benchmark</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -24,14 +24,14 @@
<source>Grantee</source> <source>Grantee</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">3</context> <context context-type="linenumber">10</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html"> <trans-unit id="f61c6867295f3b53d23557021f2f4e0aa1d0b8fc" datatype="html">
<source>Type</source> <source>Type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">17</context>
</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>
@ -39,7 +39,7 @@
</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>
<context context-type="linenumber">6</context> <context context-type="linenumber">17</context>
</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>
@ -58,14 +58,14 @@
<source>Details</source> <source>Details</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">27</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="53ea2772322e7d4d21515bb0c6dead283f8e18a5" datatype="html"> <trans-unit id="53ea2772322e7d4d21515bb0c6dead283f8e18a5" datatype="html">
<source>Revoke</source> <source>Revoke</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">47</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8264698726451826067" datatype="html"> <trans-unit id="8264698726451826067" datatype="html">
@ -337,7 +337,7 @@
</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>
<context context-type="linenumber">14</context> <context context-type="linenumber">25</context>
</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>
@ -360,7 +360,7 @@
</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>
<context context-type="linenumber">21</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/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>
@ -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">
@ -726,7 +726,7 @@
<source>Get Started</source> <source>Get Started</source>
<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">136</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5207635742003539443" datatype="html"> <trans-unit id="5207635742003539443" datatype="html">
@ -957,7 +957,7 @@
<source>Please enter the amount of your emergency fund:</source> <source>Please enter the amount of your emergency fund:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html"> <trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
@ -968,7 +968,7 @@
</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>
<context context-type="linenumber">44</context> <context context-type="linenumber">45</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="aee7d0de4e30405c1289745e4264e622b613632a" datatype="html"> <trans-unit id="aee7d0de4e30405c1289745e4264e622b613632a" datatype="html">
@ -1137,35 +1137,35 @@
<source>Please enter your coupon code:</source> <source>Please enter your coupon code:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">248</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4420880039966769543" datatype="html"> <trans-unit id="4420880039966769543" datatype="html">
<source>Could not redeem coupon code</source> <source>Could not redeem coupon code</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">240</context> <context context-type="linenumber">258</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4819099731531004979" datatype="html"> <trans-unit id="4819099731531004979" datatype="html">
<source>Coupon code has been redeemed</source> <source>Coupon code has been redeemed</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">252</context> <context context-type="linenumber">270</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7967484035994732534" datatype="html"> <trans-unit id="7967484035994732534" datatype="html">
<source>Reload</source> <source>Reload</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">253</context> <context context-type="linenumber">271</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7963559562180316948" datatype="html"> <trans-unit id="7963559562180316948" datatype="html">
<source>Do you really want to remove this sign in method?</source> <source>Do you really want to remove this sign in method?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">299</context> <context context-type="linenumber">317</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html"> <trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1243,42 +1243,42 @@
<source>Locale</source> <source>Locale</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">134</context> <context context-type="linenumber">135</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html"> <trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
<source>Date and number format</source> <source>Date and number format</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html"> <trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
<source>View Mode</source> <source>View Mode</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">160</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html"> <trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
<source>Sign in with fingerprint</source> <source>Sign in with fingerprint</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">181</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html"> <trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
<source>User ID</source> <source>User ID</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">191</context> <context context-type="linenumber">208</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html"> <trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
<source>Granted Access</source> <source>Granted Access</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">200</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html"> <trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1292,7 +1292,7 @@
<source>Public</source> <source>Public</source>
<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>
<context context-type="linenumber">8</context> <context context-type="linenumber">19</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5016419499983434110" datatype="html"> <trans-unit id="5016419499983434110" datatype="html">
@ -1506,7 +1506,7 @@
</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>
<context context-type="linenumber">75</context> <context context-type="linenumber">76</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2948175671993825247" datatype="html"> <trans-unit id="2948175671993825247" datatype="html">
@ -1520,7 +1520,7 @@
<source>Analysis</source> <source>Analysis</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">2</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/portfolio-page.html</context>
@ -1531,21 +1531,21 @@
<source>Investment Timeline</source> <source>Investment Timeline</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">102</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html"> <trans-unit id="6ae1c94f6bad274424f97e9bc8766242c1577447" datatype="html">
<source>Top</source> <source>Top</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html"> <trans-unit id="6723d5c967329a3ac75524cf0c1af5ced022b9a3" datatype="html">
<source>Bottom</source> <source>Bottom</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">59</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5857197365507636437" datatype="html"> <trans-unit id="5857197365507636437" datatype="html">
@ -1599,7 +1599,7 @@
</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>
<context context-type="linenumber">13</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="888f6842631dc20550ad7e9a9c45ef55bf45852b" datatype="html"> <trans-unit id="888f6842631dc20550ad7e9a9c45ef55bf45852b" datatype="html">
@ -1829,32 +1829,25 @@
<context context-type="linenumber">12</context> <context context-type="linenumber">12</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7081e73933b030c02e11519f76884ef7cb5d99fc" datatype="html">
<source> Hello, someone has shared a <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> with you! </source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
<context context-type="linenumber">4,6</context>
</context-group>
</trans-unit>
<trans-unit id="4056a76e7a710ab32285892a58d66f2d1a927796" datatype="html"> <trans-unit id="4056a76e7a710ab32285892a58d66f2d1a927796" datatype="html">
<source>Currencies</source> <source>Currencies</source>
<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">29</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3c3163d370916438f3b52ea17720bfb2a68a1709" datatype="html"> <trans-unit id="3c3163d370916438f3b52ea17720bfb2a68a1709" datatype="html">
<source>Continents</source> <source>Continents</source>
<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">59</context> <context context-type="linenumber">60</context>
</context-group> </context-group>
</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>
<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">131,133</context> <context context-type="linenumber">132,134</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8298333184054476827" datatype="html"> <trans-unit id="8298333184054476827" datatype="html">
@ -1973,7 +1966,7 @@
<source>Do you really want to delete this activity?</source> <source>Do you really want to delete this activity?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">134</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -2034,6 +2027,10 @@
</trans-unit> </trans-unit>
<trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html"> <trans-unit id="313fcf0f8dac5ff5800a3e6bd67cb1955089ccca" datatype="html">
<source>Beta</source> <source>Beta</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">116</context>
@ -2107,7 +2104,7 @@
</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>
<context context-type="linenumber">89</context> <context context-type="linenumber">90</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="27fe3d097c64eaec7ff564358f80fb7ba795f484" datatype="html"> <trans-unit id="27fe3d097c64eaec7ff564358f80fb7ba795f484" datatype="html">
@ -2147,7 +2144,7 @@
</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>
<context context-type="linenumber">107</context> <context context-type="linenumber">108</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="81eb53c18dfd116d6e54877444847b3091d92ab0" datatype="html"> <trans-unit id="81eb53c18dfd116d6e54877444847b3091d92ab0" datatype="html">
@ -2158,7 +2155,7 @@
</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>
<context context-type="linenumber">98</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="936788a5ab949fe0d70098ba051ac7a44999ff08" datatype="html"> <trans-unit id="936788a5ab949fe0d70098ba051ac7a44999ff08" datatype="html">
@ -2253,7 +2250,7 @@
<source>Accumulating</source> <source>Accumulating</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2937311350146031865" datatype="html"> <trans-unit id="2937311350146031865" datatype="html">
@ -2272,9 +2269,13 @@
</trans-unit> </trans-unit>
<trans-unit id="5213771062241898526" datatype="html"> <trans-unit id="5213771062241898526" datatype="html">
<source>Deposit</source> <source>Deposit</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">131</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/investment-chart/investment-chart.component.ts</context>
<context context-type="linenumber">125</context> <context context-type="linenumber">130</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.ts</context>
@ -2292,7 +2293,7 @@
<source>Monthly</source> <source>Monthly</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html"> <trans-unit id="8511b16abcf065252b350d64e337ba2447db3ffb" datatype="html">
@ -2329,6 +2330,10 @@
<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">136</context> <context context-type="linenumber">136</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts</context>
<context context-type="linenumber">87</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="4550487415324294802" datatype="html"> <trans-unit id="4550487415324294802" datatype="html">
<source>Filter by...</source> <source>Filter by...</source>
@ -2337,6 +2342,59 @@
<context context-type="linenumber">129</context> <context context-type="linenumber">129</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="303469635941752458" datatype="html">
<source>Filter by account, currency, symbol or type...</source>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">291</context>
</context-group>
</trans-unit>
<trans-unit id="3d14940af7de691ac27efb67bef3e974cbe3281c" datatype="html">
<source> Hello, <x id="INTERPOLATION" equiv-text="{{ portfolioPublicDetails?.alias ?? &apos;someone&apos; }}"/> has shared a <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> with you! </source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
<context context-type="linenumber">4,7</context>
</context-group>
</trans-unit>
<trans-unit id="fbaaeb297e70b9a800acf841b9d26c19d60651ef" datatype="html">
<source>Alias</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
<context context-type="linenumber">6</context>
</context-group>
</trans-unit>
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
<source>Experimental Features</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context>
</context-group>
</trans-unit>
<trans-unit id="1931353503905413384" datatype="html">
<source>Benchmark</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
<source>Benchmarks</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="44fcf77e86dc038202ebad6b46d1d833d60d781b" datatype="html">
<source>Compare with...</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default { export default {
displayName: 'common', displayName: 'common',

View File

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

View File

@ -1,10 +1,13 @@
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 { ghostfolioScraperApiSymbolPrefix, locale } from './config'; import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark } from './interfaces'; import { Benchmark } from './interfaces';
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
export function capitalize(aString: string) { export function capitalize(aString: string) {
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
} }
@ -39,7 +42,20 @@ export function downloadAsFile({
} }
export function encodeDataSource(aDataSource: DataSource) { export function encodeDataSource(aDataSource: DataSource) {
return Buffer.from(aDataSource, 'utf-8').toString('hex'); if (aDataSource) {
return Buffer.from(aDataSource, 'utf-8').toString('hex');
}
return undefined;
}
export function extractNumberFromString(aString: string): number {
try {
const [numberString] = aString.match(NUMERIC_REGEXP);
return parseFloat(numberString.trim());
} catch {
return undefined;
}
} }
export function getBackgroundColor() { export function getBackgroundColor() {
@ -56,6 +72,14 @@ export function getCssVariable(aCssVariable: string) {
); );
} }
export function getDateFnsLocale(aLanguageCode: string) {
if (aLanguageCode === 'de') {
return de;
}
return undefined;
}
export function getDateFormatString(aLocale?: string) { export function getDateFormatString(aLocale?: string) {
const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts( const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts(
new Date() new Date()

View File

@ -1,5 +1,6 @@
export interface Access { export interface Access {
granteeAlias: string; alias?: string;
grantee: string;
id: string; id: string;
type: 'PUBLIC' | 'RESTRICTED_VIEW'; type: 'PUBLIC' | 'RESTRICTED_VIEW';
} }

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export interface PortfolioPublicDetails { export interface PortfolioPublicDetails {
alias?: string;
hasDetails: boolean; hasDetails: boolean;
holdings: { holdings: {
[symbol: string]: Pick< [symbol: string]: Pick<

View File

@ -2,6 +2,7 @@ import { ViewMode } from '@prisma/client';
export interface UserSettings { export interface UserSettings {
baseCurrency?: string; baseCurrency?: string;
isExperimentalFeatures?: boolean;
isRestrictedView?: boolean; isRestrictedView?: boolean;
language?: string; language?: string;
locale: string; locale: string;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default { export default {
displayName: 'ui', displayName: 'ui',

View File

@ -65,8 +65,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
public totalFees: number; public totalFees: number;
public totalValue: number; public totalValue: number;
private readonly SEARCH_PLACEHOLDER =
'Filter by account, currency, symbol or type...';
private readonly SEARCH_STRING_SEPARATOR = ','; private readonly SEARCH_STRING_SEPARATOR = ',';
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -289,7 +287,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
}); });
this.placeholder = this.placeholder =
lowercaseSearchKeywords.length <= 0 ? this.SEARCH_PLACEHOLDER : ''; lowercaseSearchKeywords.length <= 0
? $localize`Filter by account, currency, symbol or type...`
: '';
this.searchKeywords = filters.map((filter) => { this.searchKeywords = filters.map((filter) => {
return filter.label; return filter.label;

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.183.0", "version": "1.189.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -24,7 +24,7 @@
"database:migrate": "prisma migrate deploy", "database:migrate": "prisma migrate deploy",
"database:push": "prisma db push", "database:push": "prisma db push",
"database:seed": "prisma db seed", "database:seed": "prisma db seed",
"database:setup": "yarn database:push && yarn database:seed && yarn database:baseline", "database:setup": "yarn database:push && yarn database:seed",
"database:validate": "prisma validate", "database:validate": "prisma validate",
"dep-graph": "nx dep-graph", "dep-graph": "nx dep-graph",
"e2e": "ng e2e", "e2e": "ng e2e",
@ -53,16 +53,16 @@
"workspace-generator": "nx workspace-generator" "workspace-generator": "nx workspace-generator"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "14.1.0", "@angular/animations": "14.2.0",
"@angular/cdk": "14.1.0", "@angular/cdk": "14.2.0",
"@angular/common": "14.1.0", "@angular/common": "14.2.0",
"@angular/compiler": "14.1.0", "@angular/compiler": "14.2.0",
"@angular/core": "14.1.0", "@angular/core": "14.2.0",
"@angular/forms": "14.1.0", "@angular/forms": "14.2.0",
"@angular/material": "14.1.0", "@angular/material": "14.2.0",
"@angular/platform-browser": "14.1.0", "@angular/platform-browser": "14.2.0",
"@angular/platform-browser-dynamic": "14.1.0", "@angular/platform-browser-dynamic": "14.2.0",
"@angular/router": "14.1.0", "@angular/router": "14.2.0",
"@codewithdan/observable-store": "2.2.11", "@codewithdan/observable-store": "2.2.11",
"@dfinity/agent": "0.12.1", "@dfinity/agent": "0.12.1",
"@dfinity/auth-client": "0.12.1", "@dfinity/auth-client": "0.12.1",
@ -80,7 +80,7 @@
"@nestjs/platform-express": "9.0.7", "@nestjs/platform-express": "9.0.7",
"@nestjs/schedule": "2.1.0", "@nestjs/schedule": "2.1.0",
"@nestjs/serve-static": "3.0.0", "@nestjs/serve-static": "3.0.0",
"@nrwl/angular": "14.5.1", "@nrwl/angular": "14.6.4",
"@prisma/client": "4.1.1", "@prisma/client": "4.1.1",
"@simplewebauthn/browser": "5.2.1", "@simplewebauthn/browser": "5.2.1",
"@simplewebauthn/server": "5.2.1", "@simplewebauthn/server": "5.2.1",
@ -121,34 +121,34 @@
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"prisma": "4.1.1", "prisma": "4.1.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "7.4.0", "rxjs": "7.5.6",
"stripe": "8.199.0", "stripe": "8.199.0",
"svgmap": "2.6.0", "svgmap": "2.6.0",
"twitter-api-v2": "1.10.3", "twitter-api-v2": "1.10.3",
"uuid": "8.3.2", "uuid": "8.3.2",
"yahoo-finance2": "2.3.3", "yahoo-finance2": "2.3.3",
"zone.js": "0.11.6" "zone.js": "0.11.8"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "14.1.0", "@angular-devkit/build-angular": "14.2.1",
"@angular-eslint/eslint-plugin": "14.0.2", "@angular-eslint/eslint-plugin": "14.0.3",
"@angular-eslint/eslint-plugin-template": "14.0.2", "@angular-eslint/eslint-plugin-template": "14.0.3",
"@angular-eslint/template-parser": "14.0.2", "@angular-eslint/template-parser": "14.0.3",
"@angular/cli": "14.1.0", "@angular/cli": "14.2.1",
"@angular/compiler-cli": "14.1.0", "@angular/compiler-cli": "14.2.0",
"@angular/language-service": "14.1.0", "@angular/language-service": "14.2.0",
"@angular/localize": "14.1.0", "@angular/localize": "14.2.0",
"@nestjs/schematics": "9.0.1", "@nestjs/schematics": "9.0.1",
"@nestjs/testing": "9.0.7", "@nestjs/testing": "9.0.7",
"@nrwl/cli": "14.5.1", "@nrwl/cli": "14.6.4",
"@nrwl/cypress": "14.5.1", "@nrwl/cypress": "14.6.4",
"@nrwl/eslint-plugin-nx": "14.5.1", "@nrwl/eslint-plugin-nx": "14.6.4",
"@nrwl/jest": "14.5.1", "@nrwl/jest": "14.6.4",
"@nrwl/nest": "14.5.1", "@nrwl/nest": "14.6.4",
"@nrwl/node": "14.5.1", "@nrwl/node": "14.6.4",
"@nrwl/nx-cloud": "14.2.0", "@nrwl/nx-cloud": "14.6.1",
"@nrwl/storybook": "14.5.1", "@nrwl/storybook": "14.6.4",
"@nrwl/workspace": "14.5.1", "@nrwl/workspace": "14.6.4",
"@simplewebauthn/typescript-types": "5.2.1", "@simplewebauthn/typescript-types": "5.2.1",
"@storybook/addon-essentials": "6.5.9", "@storybook/addon-essentials": "6.5.9",
"@storybook/angular": "6.5.9", "@storybook/angular": "6.5.9",
@ -160,9 +160,9 @@
"@types/cache-manager": "3.4.2", "@types/cache-manager": "3.4.2",
"@types/color": "3.0.2", "@types/color": "3.0.2",
"@types/google-spreadsheet": "3.1.5", "@types/google-spreadsheet": "3.1.5",
"@types/jest": "27.4.1", "@types/jest": "28.1.8",
"@types/lodash": "4.14.174", "@types/lodash": "4.14.174",
"@types/node": "16.11.7", "@types/node": "18.7.1",
"@types/papaparse": "5.2.6", "@types/papaparse": "5.2.6",
"@types/passport-google-oauth20": "2.0.11", "@types/passport-google-oauth20": "2.0.11",
"@typescript-eslint/eslint-plugin": "5.4.0", "@typescript-eslint/eslint-plugin": "5.4.0",
@ -176,14 +176,15 @@
"import-sort-cli": "6.0.0", "import-sort-cli": "6.0.0",
"import-sort-parser-typescript": "6.0.0", "import-sort-parser-typescript": "6.0.0",
"import-sort-style-module": "6.0.0", "import-sort-style-module": "6.0.0",
"jest": "27.5.1", "jest": "28.1.3",
"jest-preset-angular": "11.1.2", "jest-environment-jsdom": "28.1.1",
"nx": "14.5.1", "jest-preset-angular": "12.2.2",
"nx": "14.6.4",
"prettier": "2.7.1", "prettier": "2.7.1",
"replace-in-file": "6.2.0", "replace-in-file": "6.2.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-jest": "27.1.4", "ts-jest": "28.0.8",
"ts-node": "10.8.1", "ts-node": "10.9.1",
"tslib": "2.0.0", "tslib": "2.0.0",
"typescript": "4.7.3" "typescript": "4.7.3"
}, },

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Access" ADD COLUMN "alias" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" DROP COLUMN "alias";

View File

@ -1,7 +1,7 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
previewFeatures = [] previewFeatures = []
binaryTargets = ["debian-openssl-1.1.x", "native"] binaryTargets = ["debian-openssl-1.1.x", "linux-arm64-openssl-1.1.x", "native"]
} }
datasource db { datasource db {
@ -10,6 +10,7 @@ datasource db {
} }
model Access { model Access {
alias String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
granteeUserId String? granteeUserId String?
id String @id @default(uuid()) id String @id @default(uuid())
@ -159,7 +160,6 @@ model Tag {
model User { model User {
accessToken String? accessToken String?
alias String?
authChallenge String? authChallenge String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
id String @id @default(uuid()) id String @id @default(uuid())

View File

@ -111,7 +111,6 @@ async function main() {
} }
] ]
}, },
alias: 'Demo',
id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f', id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f',
role: Role.DEMO role: Role.DEMO
}, },

View File

@ -1,5 +0,0 @@
set -xe
echo "$DOCKER_HUB_ACCESS_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG -t ghostfolio/ghostfolio:latest .
docker push ghostfolio/ghostfolio --all-tags

3080
yarn.lock

File diff suppressed because it is too large Load Diff