Compare commits
116 Commits
Author | SHA1 | Date | |
---|---|---|---|
f72d31bab3 | |||
4c893c4dcc | |||
ffb11cd10e | |||
d424b7731e | |||
6043c87481 | |||
fca0a688b6 | |||
5c6cc4fed5 | |||
64a7d38ff9 | |||
68d0d39161 | |||
233a8a8a18 | |||
190779ee35 | |||
6ef8121561 | |||
58bf57d1e6 | |||
71c5412dd5 | |||
ae85398c3d | |||
048900d01b | |||
074b09b543 | |||
f9e04022f4 | |||
8fd1fbd44a | |||
0fb33ae71c | |||
3a35d72ec2 | |||
32fe3e195f | |||
805f4b05be | |||
5b51a6840a | |||
36bd6164e6 | |||
eac52a215b | |||
9ff8cd5471 | |||
33cc7e4e7e | |||
47f84dab06 | |||
384d18b2a6 | |||
2363983bdc | |||
4af76764be | |||
a65424aafa | |||
f9cd629470 | |||
ccb8c86596 | |||
246de7aa86 | |||
a323313c71 | |||
538c8947cd | |||
1ec5fd12fe | |||
4376b8903e | |||
a8e096f9ac | |||
8e577592f6 | |||
c896bf9199 | |||
16145f18d9 | |||
5398da0dc8 | |||
2466f4ff5d | |||
8f3a9bdfbb | |||
44dfd2bd48 | |||
3fc2228f1d | |||
b018819a1f | |||
ac9311d783 | |||
e23ce0f35d | |||
f4b52aa41c | |||
655b040d4d | |||
0f637a5d0f | |||
3f85c327f5 | |||
c2df99072d | |||
e8afbcad9c | |||
e6d8de781b | |||
6e1935899f | |||
169cb85b66 | |||
fe6658d0ac | |||
1f0381228e | |||
f4b63b5de5 | |||
e45a0ad068 | |||
81c6cc021d | |||
859b24aa5b | |||
2bc325f182 | |||
a6186c23e2 | |||
cf234003ec | |||
8d3954304e | |||
9562139fa6 | |||
c857ea9a8f | |||
5c9fa71d95 | |||
fefbfa31d1 | |||
93a1fae51c | |||
3715edd9ba | |||
e3916e1ba3 | |||
76ceac4edc | |||
333b63bfe2 | |||
3006c21b12 | |||
f01a3f893d | |||
72974e888f | |||
0cee7a0b35 | |||
f3d337b044 | |||
7667af059c | |||
1095b47f45 | |||
dacd7271eb | |||
e093041184 | |||
8f2caa508a | |||
862f670ccf | |||
54bf4c7a43 | |||
c0ace51ee9 | |||
b1b5689242 | |||
b68cdaf8ea | |||
b387a80a0d | |||
6e4660295a | |||
d4c3a9d1e8 | |||
263f6b32f2 | |||
637f31ae3b | |||
547e27c7a1 | |||
f10dc176f2 | |||
0a966e46cd | |||
4f281d25e1 | |||
aaba8c35c2 | |||
7d27cb3398 | |||
91678028b5 | |||
5e3cac8ac9 | |||
33f20b6b48 | |||
e4fd255dd7 | |||
e320aa91f7 | |||
0fcfa6c1bd | |||
42d32ed652 | |||
21b4b0ef24 | |||
4f8fe83a16 | |||
980ad1028c |
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: '[BUG]'
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
The Issue tracker is **ONLY** used for reporting bugs. New features should be discussed on our [Slack channel](https://ghostfolio.slack.com) or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions).
|
||||||
|
|
||||||
|
**Bug Description**
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what the bug is. -->
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
|
||||||
|
<!-- Steps to reproduce the behavior -->
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
|
||||||
|
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
|
||||||
|
<!-- If applicable, add logs to help explain your problem. -->
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
|
||||||
|
<!-- Please complete the following information -->
|
||||||
|
|
||||||
|
- Ghostfolio Version X.Y.Z
|
||||||
|
- Browser
|
||||||
|
- OS
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
|
||||||
|
<!-- Add any other context about the problem here. -->
|
@ -1,4 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"attributeGroups": [
|
||||||
|
"$ANGULAR_ELEMENT_REF",
|
||||||
|
"$ANGULAR_STRUCTURAL_DIRECTIVE",
|
||||||
|
"$DEFAULT",
|
||||||
|
"$ANGULAR_INPUT",
|
||||||
|
"$ANGULAR_TWO_WAY_BINDING",
|
||||||
|
"$ANGULAR_OUTPUT"
|
||||||
|
],
|
||||||
|
"attributeSort": "ASC",
|
||||||
"endOfLine": "auto",
|
"endOfLine": "auto",
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
222
CHANGELOG.md
222
CHANGELOG.md
@ -5,6 +5,222 @@ 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.207.0 - 31.10.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for translated labels of asset and asset sub class
|
||||||
|
- Added support for dates in _ISO 8601_ date format (`YYYY-MM-DD`) in the activities import
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Darkened the background color of the dark mode
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the public page
|
||||||
|
- Improved the loading indicator of the portfolio evolution chart
|
||||||
|
|
||||||
|
## 1.206.2 - 20.10.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fixed the `rxjs` version to `7.5.6` (resolutions)
|
||||||
|
- Migrated the `angular.json` to `project.json` files in the `Nx` workspace
|
||||||
|
- Upgraded `nestjs` from version `9.0.7` to `9.1.4`
|
||||||
|
- Upgraded `Nx` from version `14.6.4` to `15.0.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the performance calculation including `SELL` activities with a significant performance gain
|
||||||
|
|
||||||
|
## 1.205.2 - 16.10.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Persisted the language on url change
|
||||||
|
- Improved the portfolio evolution chart
|
||||||
|
- Refactored the appearance (dark mode) in user settings (from `appearance` to `colorScheme`)
|
||||||
|
- Improved the wording on the landing page
|
||||||
|
|
||||||
|
## 1.204.1 - 15.10.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to change the appearance (dark mode) in user settings
|
||||||
|
- Added the total amount chart to the investment timeline
|
||||||
|
- Setup the `prettier` plugin `prettier-plugin-organize-attributes`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Respected the current date in the _FIRE_ calculator
|
||||||
|
- Simplified the settings management in the admin control panel
|
||||||
|
- Renamed the data source type `RAKUTEN` to `RAPID_API`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed some links in the blog posts
|
||||||
|
- Fixed the alignment of the value component on the allocations page
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Rename the environment variable from `RAKUTEN_RAPID_API_KEY` to `RAPID_API_API_KEY`
|
||||||
|
|
||||||
|
## 1.203.0 - 08.10.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Supported a progressive line animation in the line chart component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the benchmark comparator from experimental to general availability
|
||||||
|
- Improved the user interface of the benchmark comparator
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the performance and chart calculation of today
|
||||||
|
- Fixed the alignment of the value component in the admin control panel
|
||||||
|
|
||||||
|
## 1.202.0 - 07.10.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for a translated 4% rule in the _FIRE_ section
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the caching of the benchmarks in the markets overview (only cache if fetching was successful)
|
||||||
|
- Improved the wording in the twitter bot service
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the support for cryptocurrencies having a symbol with less than 3 characters (e.g. `SC-USD`)
|
||||||
|
- Fixed the text truncation in the value component
|
||||||
|
|
||||||
|
## 1.201.0 - 01.10.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a blog post: _Hacktoberfest 2022_
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usage of the value component in the admin control panel
|
||||||
|
- Improved the language localization for Español (`es`)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the usage of the value component on the allocations page
|
||||||
|
|
||||||
|
## 1.200.0 - 01.10.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a mini statistics section to the landing page including pulls on _Docker Hub_
|
||||||
|
- Added an _As seen in_ section to the landing page
|
||||||
|
- Added support for an icon in the value component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `prisma` from version `4.1.1` to `4.4.0`
|
||||||
|
|
||||||
|
## 1.199.1 - 27.09.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Set up the language localization for Español (`es`)
|
||||||
|
- Added support for sectors in mutual funds
|
||||||
|
|
||||||
|
## 1.198.0 - 25.09.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to exclude an account from analysis
|
||||||
|
- Set up the language localization for Nederlands (`nl`)
|
||||||
|
|
||||||
|
## 1.197.0 - 24.09.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the value of the active filter in percentage on the allocations page
|
||||||
|
- Extended the feature overview page by multi-language support (English, German, Italian)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Combined the performance and chart calculation
|
||||||
|
- Improved the style of various selectors (density)
|
||||||
|
|
||||||
|
## 1.196.0 - 22.09.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Set up the language localization for Italiano (`it`)
|
||||||
|
- Extended the landing page
|
||||||
|
|
||||||
|
## 1.195.0 - 20.09.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the algorithm of the performance chart calculation
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Improved the chart tooltip of the benchmark comparator
|
||||||
|
|
||||||
|
## 1.194.0 - 17.09.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `NODE_ENV: production` to the `docker-compose` files (`docker-compose.yml` and `docker-compose.build.yml`)
|
||||||
|
- Visualized the percentage of the active filter on the allocations page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Respected the end date in the performance chart calculation
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Set `NODE_ENV: production` as in [docker-compose.yml](https://github.com/ghostfolio/ghostfolio/blob/main/docker/docker-compose.yml)
|
||||||
|
|
||||||
|
## 1.193.0 - 14.09.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Sorted the benchmarks by name
|
||||||
|
- Extended the pricing page
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the calculations of the exchange rate service by changing `USD` to the base currency
|
||||||
|
- Fixed the missing assets during the local development
|
||||||
|
|
||||||
|
## 1.192.0 - 11.09.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Simplified the configuration of the benchmarks: `symbolProfileId` instead of `dataSource` and `symbol`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.3.3` to `2.3.6`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Improved the loading indicator of the benchmark comparator
|
||||||
|
- Improved the error handling in the benchmark calculation
|
||||||
|
|
||||||
|
## 1.191.0 - 10.09.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed the `currency` and `viewMode` from the `User` database schema
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Allowed the date range change for the demo user
|
||||||
|
|
||||||
## 1.190.0 - 10.09.2022
|
## 1.190.0 - 10.09.2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -132,7 +348,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
|
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
|
||||||
- Set up language localization for German (`de`)
|
- Set up the language localization for German (`de`)
|
||||||
- Resolved the feature graphic of the blog post
|
- Resolved the feature graphic of the blog post
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -794,8 +1010,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Display the value in base currency in the accounts table on mobile
|
- Displayed the value in base currency in the accounts table on mobile
|
||||||
- Display the value in base currency in the activities table on mobile
|
- Displayed the value in base currency in the activities table on mobile
|
||||||
- Renamed `orders` to `activities` in import and export functionality
|
- Renamed `orders` to `activities` in import and export functionality
|
||||||
- Harmonized the algebraic sign of `currentGrossPerformancePercent` and `currentNetPerformancePercent` with `currentGrossPerformance` and `currentNetPerformance`
|
- Harmonized the algebraic sign of `currentGrossPerformancePercent` and `currentNetPerformancePercent` with `currentGrossPerformance` and `currentNetPerformance`
|
||||||
- Improved the pricing page
|
- Improved the pricing page
|
||||||
|
@ -25,7 +25,6 @@ RUN yarn install
|
|||||||
COPY ./decorate-angular-cli.js decorate-angular-cli.js
|
COPY ./decorate-angular-cli.js decorate-angular-cli.js
|
||||||
RUN node decorate-angular-cli.js
|
RUN node decorate-angular-cli.js
|
||||||
|
|
||||||
COPY ./angular.json angular.json
|
|
||||||
COPY ./nx.json nx.json
|
COPY ./nx.json nx.json
|
||||||
COPY ./replace.build.js replace.build.js
|
COPY ./replace.build.js replace.build.js
|
||||||
COPY ./jest.preset.js jest.preset.js
|
COPY ./jest.preset.js jest.preset.js
|
||||||
|
19
README.md
19
README.md
@ -153,6 +153,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Run `yarn install`
|
1. Run `yarn install`
|
||||||
|
1. Run `yarn build:dev` to build the source code including the assets
|
||||||
1. Run `docker-compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
1. Run `docker-compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
||||||
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
||||||
1. Start the server and the client (see [_Development_](#Development))
|
1. Start the server and the client (see [_Development_](#Development))
|
||||||
@ -189,20 +190,22 @@ Run `yarn test`
|
|||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
|
|
||||||
|
### Authorization: Bearer Token
|
||||||
|
|
||||||
|
Set the header for each request as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
"Authorization": "Bearer eyJh..."
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
|
||||||
|
|
||||||
### Import Activities
|
### Import Activities
|
||||||
|
|
||||||
#### Request
|
#### Request
|
||||||
|
|
||||||
`POST http://localhost:3333/api/v1/import`
|
`POST http://localhost:3333/api/v1/import`
|
||||||
|
|
||||||
#### Authorization: Bearer Token
|
|
||||||
|
|
||||||
Set the header as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
"Authorization": "Bearer eyJh..."
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Body
|
#### Body
|
||||||
|
|
||||||
```
|
```
|
||||||
|
357
angular.json
357
angular.json
@ -1,357 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"projects": {
|
|
||||||
"api": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "apps/api",
|
|
||||||
"sourceRoot": "apps/api/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"prefix": "api",
|
|
||||||
"schematics": {},
|
|
||||||
"architect": {
|
|
||||||
"build": {
|
|
||||||
"builder": "@nrwl/node:webpack",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/apps/api",
|
|
||||||
"main": "apps/api/src/main.ts",
|
|
||||||
"tsConfig": "apps/api/tsconfig.app.json",
|
|
||||||
"assets": ["apps/api/src/assets"]
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"generatePackageJson": true,
|
|
||||||
"optimization": true,
|
|
||||||
"extractLicenses": true,
|
|
||||||
"inspect": false,
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "apps/api/src/environments/environment.ts",
|
|
||||||
"with": "apps/api/src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"outputs": ["{options.outputPath}"]
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"builder": "@nrwl/node:node",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "api:build"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["apps/api/**/*.ts"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "apps/api/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
},
|
|
||||||
"outputs": ["coverage/apps/api"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"client": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"projectType": "application",
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "apps/client",
|
|
||||||
"sourceRoot": "apps/client/src",
|
|
||||||
"prefix": "gf",
|
|
||||||
"architect": {
|
|
||||||
"build": {
|
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/apps/client",
|
|
||||||
"index": "apps/client/src/index.html",
|
|
||||||
"main": "apps/client/src/main.ts",
|
|
||||||
"polyfills": "apps/client/src/polyfills.ts",
|
|
||||||
"tsConfig": "apps/client/tsconfig.app.json",
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"glob": "assetlinks.json",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../.well-known"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "CHANGELOG.md",
|
|
||||||
"input": "",
|
|
||||||
"output": "./../assets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "LICENSE",
|
|
||||||
"input": "",
|
|
||||||
"output": "./../assets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "robots.txt",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "sitemap.xml",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "**/*",
|
|
||||||
"input": "node_modules/ionicons/dist/ionicons",
|
|
||||||
"output": "./../ionicons"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "**/*.js",
|
|
||||||
"input": "node_modules/ionicons/dist/",
|
|
||||||
"output": "./../"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "**/*",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../assets/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"styles": ["apps/client/src/styles.scss"],
|
|
||||||
"scripts": ["node_modules/marked/marked.min.js"],
|
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"buildOptimizer": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"optimization": false,
|
|
||||||
"namedChunks": true
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"development-de": {
|
|
||||||
"baseHref": "/de/",
|
|
||||||
"localize": ["de"]
|
|
||||||
},
|
|
||||||
"development-en": {
|
|
||||||
"baseHref": "/en/",
|
|
||||||
"localize": ["en"]
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "apps/client/src/environments/environment.ts",
|
|
||||||
"with": "apps/client/src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"optimization": true,
|
|
||||||
"outputHashing": "all",
|
|
||||||
"sourceMap": false,
|
|
||||||
"namedChunks": false,
|
|
||||||
"extractLicenses": true,
|
|
||||||
"vendorChunk": false,
|
|
||||||
"buildOptimizer": true,
|
|
||||||
"budgets": [
|
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "2mb",
|
|
||||||
"maximumError": "5mb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "6kb",
|
|
||||||
"maximumError": "10kb"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"defaultConfiguration": ""
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "client:build",
|
|
||||||
"proxyConfig": "apps/client/proxy.conf.json"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"development-de": {
|
|
||||||
"browserTarget": "client:build:development-de"
|
|
||||||
},
|
|
||||||
"development-en": {
|
|
||||||
"browserTarget": "client:build:development-en"
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"browserTarget": "client:build:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "client:build",
|
|
||||||
"includeContext": true,
|
|
||||||
"outputPath": "src/locales",
|
|
||||||
"targetFiles": ["messages.de.xlf"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["apps/client/**/*.ts"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "apps/client/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
},
|
|
||||||
"outputs": ["coverage/apps/client"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"i18n": {
|
|
||||||
"locales": {
|
|
||||||
"de": {
|
|
||||||
"baseHref": "/de/",
|
|
||||||
"translation": "apps/client/src/locales/messages.de.xlf"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sourceLocale": "en"
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"client-e2e": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "apps/client-e2e",
|
|
||||||
"sourceRoot": "apps/client-e2e/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"architect": {
|
|
||||||
"e2e": {
|
|
||||||
"builder": "@nrwl/cypress:cypress",
|
|
||||||
"options": {
|
|
||||||
"cypressConfig": "apps/client-e2e/cypress.json",
|
|
||||||
"tsConfig": "apps/client-e2e/tsconfig.e2e.json",
|
|
||||||
"devServerTarget": "client:serve"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"devServerTarget": "client:serve:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [],
|
|
||||||
"implicitDependencies": ["client"]
|
|
||||||
},
|
|
||||||
"common": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "libs/common",
|
|
||||||
"sourceRoot": "libs/common/src",
|
|
||||||
"projectType": "library",
|
|
||||||
"architect": {
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["libs/common/**/*.ts"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"outputs": ["coverage/libs/common"],
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "libs/common/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"projectType": "library",
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "libs/ui",
|
|
||||||
"sourceRoot": "libs/ui/src",
|
|
||||||
"prefix": "gf",
|
|
||||||
"architect": {
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"outputs": ["coverage/libs/ui"],
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "libs/ui/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["libs/ui/src/**/*.ts", "libs/ui/src/**/*.html"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storybook": {
|
|
||||||
"builder": "@storybook/angular:start-storybook",
|
|
||||||
"options": {
|
|
||||||
"port": 4400,
|
|
||||||
"configDir": "libs/ui/.storybook",
|
|
||||||
"browserTarget": "ui:build-storybook",
|
|
||||||
"compodoc": false
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"build-storybook": {
|
|
||||||
"builder": "@storybook/angular:build-storybook",
|
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"options": {
|
|
||||||
"outputDir": "dist/storybook/ui",
|
|
||||||
"configDir": "libs/ui/.storybook",
|
|
||||||
"browserTarget": "ui:build-storybook",
|
|
||||||
"compodoc": false
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"ui-e2e": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "apps/ui-e2e",
|
|
||||||
"sourceRoot": "apps/ui-e2e/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"architect": {
|
|
||||||
"e2e": {
|
|
||||||
"builder": "@nrwl/cypress:cypress",
|
|
||||||
"options": {
|
|
||||||
"cypressConfig": "apps/ui-e2e/cypress.json",
|
|
||||||
"devServerTarget": "ui:storybook",
|
|
||||||
"tsConfig": "apps/ui-e2e/tsconfig.json"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"devServerTarget": "ui:storybook:ci"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [],
|
|
||||||
"implicitDependencies": ["ui"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
56
apps/api/project.json
Normal file
56
apps/api/project.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/api/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "api",
|
||||||
|
"generators": {},
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nrwl/webpack:webpack",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/api",
|
||||||
|
"main": "apps/api/src/main.ts",
|
||||||
|
"tsConfig": "apps/api/tsconfig.app.json",
|
||||||
|
"assets": ["apps/api/src/assets"],
|
||||||
|
"target": "node",
|
||||||
|
"compiler": "tsc"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"generatePackageJson": true,
|
||||||
|
"optimization": true,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"inspect": false,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "apps/api/src/environments/environment.ts",
|
||||||
|
"with": "apps/api/src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": ["{options.outputPath}"]
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@nrwl/node:node",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "api:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["apps/api/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "apps/api/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
},
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/apps/api"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -95,9 +95,10 @@ export class AccountController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let accountsWithAggregations =
|
let accountsWithAggregations =
|
||||||
await this.portfolioService.getAccountsWithAggregations(
|
await this.portfolioService.getAccountsWithAggregations({
|
||||||
impersonationUserId || this.request.user.id
|
userId: impersonationUserId || this.request.user.id,
|
||||||
);
|
withExcludedAccounts: true
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationUserId ||
|
impersonationUserId ||
|
||||||
@ -137,10 +138,11 @@ export class AccountController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let accountsWithAggregations =
|
let accountsWithAggregations =
|
||||||
await this.portfolioService.getAccountsWithAggregations(
|
await this.portfolioService.getAccountsWithAggregations({
|
||||||
impersonationUserId || this.request.user.id,
|
filters: [{ id, type: 'ACCOUNT' }],
|
||||||
[{ id, type: 'ACCOUNT' }]
|
userId: impersonationUserId || this.request.user.id,
|
||||||
);
|
withExcludedAccounts: true
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationUserId ||
|
impersonationUserId ||
|
||||||
|
@ -107,15 +107,23 @@ export class AccountService {
|
|||||||
public async getCashDetails({
|
public async getCashDetails({
|
||||||
currency,
|
currency,
|
||||||
filters = [],
|
filters = [],
|
||||||
userId
|
userId,
|
||||||
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
currency: string;
|
currency: string;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
userId: string;
|
userId: string;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<CashDetails> {
|
}): Promise<CashDetails> {
|
||||||
let totalCashBalanceInBaseCurrency = new Big(0);
|
let totalCashBalanceInBaseCurrency = new Big(0);
|
||||||
|
|
||||||
const where: Prisma.AccountWhereInput = { userId };
|
const where: Prisma.AccountWhereInput = {
|
||||||
|
userId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (withExcludedAccounts === false) {
|
||||||
|
where.isExcluded = false;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ACCOUNT: filtersByAccount,
|
ACCOUNT: filtersByAccount,
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateIf
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class CreateAccountDto {
|
export class CreateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@ -11,6 +17,10 @@ export class CreateAccountDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isExcluded?: boolean;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateIf
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class UpdateAccountDto {
|
export class UpdateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@ -14,6 +20,10 @@ export class UpdateAccountDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isExcluded?: boolean;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ -181,10 +181,10 @@ export class AdminService {
|
|||||||
public async putSetting(key: string, value: string) {
|
public async putSetting(key: string, value: string) {
|
||||||
let response: Property;
|
let response: Property;
|
||||||
|
|
||||||
if (value === '') {
|
if (value) {
|
||||||
response = await this.propertyService.delete({ key });
|
|
||||||
} else {
|
|
||||||
response = await this.propertyService.put({ key, value });
|
response = await this.propertyService.put({ key, value });
|
||||||
|
} else {
|
||||||
|
response = await this.propertyService.delete({ key });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === PROPERTY_CURRENCIES) {
|
if (key === PROPERTY_CURRENCIES) {
|
||||||
|
@ -4,7 +4,10 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
import {
|
||||||
|
MAX_CHART_ITEMS,
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
@ -12,10 +15,10 @@ import {
|
|||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BenchmarkService {
|
export class BenchmarkService {
|
||||||
@ -31,9 +34,13 @@ export class BenchmarkService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public calculateChangeInPercentage(baseValue: number, currentValue: number) {
|
public calculateChangeInPercentage(baseValue: number, currentValue: number) {
|
||||||
|
if (baseValue && currentValue) {
|
||||||
return new Big(currentValue).div(baseValue).minus(1).toNumber();
|
return new Big(currentValue).div(baseValue).minus(1).toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
public async getBenchmarks({ useCache = true } = {}): Promise<
|
public async getBenchmarks({ useCache = true } = {}): Promise<
|
||||||
BenchmarkResponse['benchmarks']
|
BenchmarkResponse['benchmarks']
|
||||||
> {
|
> {
|
||||||
@ -51,25 +58,26 @@ export class BenchmarkService {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const benchmarkAssets: UniqueAsset[] =
|
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles();
|
||||||
((await this.propertyService.getByKey(
|
|
||||||
PROPERTY_BENCHMARKS
|
|
||||||
)) as UniqueAsset[]) ?? [];
|
|
||||||
const promises: Promise<number>[] = [];
|
const promises: Promise<number>[] = [];
|
||||||
|
|
||||||
const [quotes, assetProfiles] = await Promise.all([
|
const quotes = await this.dataProviderService.getQuotes(
|
||||||
this.dataProviderService.getQuotes(benchmarkAssets),
|
benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
||||||
this.symbolProfileService.getSymbolProfiles(benchmarkAssets)
|
return { dataSource, symbol };
|
||||||
]);
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (const benchmarkAsset of benchmarkAssets) {
|
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
||||||
promises.push(this.marketDataService.getMax(benchmarkAsset));
|
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTimeHighs = await Promise.all(promises);
|
const allTimeHighs = await Promise.all(promises);
|
||||||
|
let storeInCache = true;
|
||||||
|
|
||||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol] ?? {};
|
const { marketPrice } =
|
||||||
|
quotes[benchmarkAssetProfiles[index].symbol] ?? {};
|
||||||
|
|
||||||
let performancePercentFromAllTimeHigh = 0;
|
let performancePercentFromAllTimeHigh = 0;
|
||||||
|
|
||||||
@ -78,18 +86,15 @@ export class BenchmarkService {
|
|||||||
allTimeHigh,
|
allTimeHigh,
|
||||||
marketPrice
|
marketPrice
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
storeInCache = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketCondition: this.getMarketCondition(
|
marketCondition: this.getMarketCondition(
|
||||||
performancePercentFromAllTimeHigh
|
performancePercentFromAllTimeHigh
|
||||||
),
|
),
|
||||||
name: assetProfiles.find(({ dataSource, symbol }) => {
|
name: benchmarkAssetProfiles[index].name,
|
||||||
return (
|
|
||||||
dataSource === benchmarkAssets[index].dataSource &&
|
|
||||||
symbol === benchmarkAssets[index].symbol
|
|
||||||
);
|
|
||||||
})?.name,
|
|
||||||
performances: {
|
performances: {
|
||||||
allTimeHigh: {
|
allTimeHigh: {
|
||||||
performancePercent: performancePercentFromAllTimeHigh
|
performancePercent: performancePercentFromAllTimeHigh
|
||||||
@ -98,31 +103,39 @@ export class BenchmarkService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (storeInCache) {
|
||||||
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
|
ms('4 hours') / 1000
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return benchmarks;
|
return benchmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBenchmarkAssetProfiles(): Promise<UniqueAsset[]> {
|
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
||||||
const benchmarkAssets: UniqueAsset[] =
|
const symbolProfileIds: string[] = (
|
||||||
((await this.propertyService.getByKey(
|
((await this.propertyService.getByKey(PROPERTY_BENCHMARKS)) as {
|
||||||
PROPERTY_BENCHMARKS
|
symbolProfileId: string;
|
||||||
)) as UniqueAsset[]) ?? [];
|
}[]) ?? []
|
||||||
|
).map(({ symbolProfileId }) => {
|
||||||
|
return symbolProfileId;
|
||||||
|
});
|
||||||
|
|
||||||
const assetProfiles = await this.symbolProfileService.getSymbolProfiles(
|
const assetProfiles =
|
||||||
benchmarkAssets
|
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);
|
||||||
);
|
|
||||||
|
|
||||||
return assetProfiles.map(({ dataSource, symbol }) => {
|
return assetProfiles
|
||||||
|
.map(({ dataSource, id, name, symbol }) => {
|
||||||
return {
|
return {
|
||||||
dataSource,
|
dataSource,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
symbol
|
symbol
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMarketDataBySymbol({
|
public async getMarketDataBySymbol({
|
||||||
@ -151,16 +164,18 @@ export class BenchmarkService {
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
marketDataItems.push({
|
const step = Math.round(
|
||||||
...currentSymbolItem,
|
marketDataItems.length / Math.min(marketDataItems.length, MAX_CHART_ITEMS)
|
||||||
createdAt: new Date(),
|
);
|
||||||
date: new Date(),
|
|
||||||
id: uuidv4()
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
|
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
|
||||||
return {
|
const response = {
|
||||||
marketData: marketDataItems.map((marketDataItem) => {
|
marketData: [
|
||||||
|
...marketDataItems
|
||||||
|
.filter((marketDataItem, index) => {
|
||||||
|
return index % step === 0;
|
||||||
|
})
|
||||||
|
.map((marketDataItem) => {
|
||||||
return {
|
return {
|
||||||
date: format(marketDataItem.date, DATE_FORMAT),
|
date: format(marketDataItem.date, DATE_FORMAT),
|
||||||
value:
|
value:
|
||||||
@ -172,7 +187,21 @@ export class BenchmarkService {
|
|||||||
) * 100
|
) * 100
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (currentSymbolItem?.marketPrice) {
|
||||||
|
response.marketData.push({
|
||||||
|
date: format(new Date(), DATE_FORMAT),
|
||||||
|
value:
|
||||||
|
this.calculateChangeInPercentage(
|
||||||
|
marketPriceAtStartDate,
|
||||||
|
currentSymbolItem.marketPrice
|
||||||
|
) * 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMarketCondition(aPerformanceInPercent: number) {
|
private getMarketCondition(aPerformanceInPercent: number) {
|
||||||
|
@ -4,22 +4,51 @@ import * as path from 'path';
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FrontendMiddleware implements NestMiddleware {
|
export class FrontendMiddleware implements NestMiddleware {
|
||||||
public indexHtmlDe = fs.readFileSync(
|
public indexHtmlDe = '';
|
||||||
|
public indexHtmlEn = '';
|
||||||
|
public indexHtmlEs = '';
|
||||||
|
public indexHtmlIt = '';
|
||||||
|
public indexHtmlNl = '';
|
||||||
|
public isProduction: boolean;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly configurationService: ConfigurationService
|
||||||
|
) {
|
||||||
|
const NODE_ENV =
|
||||||
|
this.configService.get<'development' | 'production'>('NODE_ENV') ??
|
||||||
|
'development';
|
||||||
|
|
||||||
|
this.isProduction = NODE_ENV === 'production';
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.indexHtmlDe = fs.readFileSync(
|
||||||
this.getPathOfIndexHtmlFile('de'),
|
this.getPathOfIndexHtmlFile('de'),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
public indexHtmlEn = fs.readFileSync(
|
this.indexHtmlEn = fs.readFileSync(
|
||||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
|
this.indexHtmlEs = fs.readFileSync(
|
||||||
public constructor(
|
this.getPathOfIndexHtmlFile('es'),
|
||||||
private readonly configurationService: ConfigurationService
|
'utf8'
|
||||||
) {}
|
);
|
||||||
|
this.indexHtmlIt = fs.readFileSync(
|
||||||
|
this.getPathOfIndexHtmlFile('it'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
this.indexHtmlNl = fs.readFileSync(
|
||||||
|
this.getPathOfIndexHtmlFile('nl'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
public use(req: Request, res: Response, next: NextFunction) {
|
public use(req: Request, res: Response, next: NextFunction) {
|
||||||
let featureGraphicPath = 'assets/cover.png';
|
let featureGraphicPath = 'assets/cover.png';
|
||||||
@ -29,9 +58,18 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
req.path === '/en/blog/2022/08/500-stars-on-github/'
|
req.path === '/en/blog/2022/08/500-stars-on-github/'
|
||||||
) {
|
) {
|
||||||
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
|
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
|
||||||
|
} else if (
|
||||||
|
req.path === '/en/blog/2022/10/hacktoberfest-2022' ||
|
||||||
|
req.path === '/en/blog/2022/10/hacktoberfest-2022/'
|
||||||
|
) {
|
||||||
|
featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.path.startsWith('/api/') || this.isFileRequest(req.url)) {
|
if (
|
||||||
|
req.path.startsWith('/api/') ||
|
||||||
|
this.isFileRequest(req.url) ||
|
||||||
|
!this.isProduction
|
||||||
|
) {
|
||||||
// Skip
|
// Skip
|
||||||
next();
|
next();
|
||||||
} else if (req.path === '/de' || req.path.startsWith('/de/')) {
|
} else if (req.path === '/de' || req.path.startsWith('/de/')) {
|
||||||
@ -43,6 +81,33 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
rootUrl: this.configurationService.get('ROOT_URL')
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else if (req.path === '/es' || req.path.startsWith('/es/')) {
|
||||||
|
res.send(
|
||||||
|
this.interpolate(this.indexHtmlEs, {
|
||||||
|
featureGraphicPath,
|
||||||
|
languageCode: 'es',
|
||||||
|
path: req.path,
|
||||||
|
rootUrl: this.configurationService.get('ROOT_URL')
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (req.path === '/it' || req.path.startsWith('/it/')) {
|
||||||
|
res.send(
|
||||||
|
this.interpolate(this.indexHtmlIt, {
|
||||||
|
featureGraphicPath,
|
||||||
|
languageCode: 'it',
|
||||||
|
path: req.path,
|
||||||
|
rootUrl: this.configurationService.get('ROOT_URL')
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (req.path === '/nl' || req.path.startsWith('/nl/')) {
|
||||||
|
res.send(
|
||||||
|
this.interpolate(this.indexHtmlNl, {
|
||||||
|
featureGraphicPath,
|
||||||
|
languageCode: 'nl',
|
||||||
|
path: req.path,
|
||||||
|
rootUrl: this.configurationService.get('ROOT_URL')
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
res.send(
|
res.send(
|
||||||
this.interpolate(this.indexHtmlEn, {
|
this.interpolate(this.indexHtmlEn, {
|
||||||
|
@ -145,6 +145,27 @@ export class InfoService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async countDockerHubPulls(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const get = bent(
|
||||||
|
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
|
||||||
|
'GET',
|
||||||
|
'json',
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
'User-Agent': 'request'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { pull_count } = await get();
|
||||||
|
return pull_count;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error, 'InfoService');
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async countGitHubContributors(): Promise<number> {
|
private async countGitHubContributors(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const get = bent(
|
||||||
@ -245,6 +266,8 @@ export class InfoService {
|
|||||||
const activeUsers1d = await this.countActiveUsers(1);
|
const activeUsers1d = await this.countActiveUsers(1);
|
||||||
const activeUsers30d = await this.countActiveUsers(30);
|
const activeUsers30d = await this.countActiveUsers(30);
|
||||||
const newUsers30d = await this.countNewUsers(30);
|
const newUsers30d = await this.countNewUsers(30);
|
||||||
|
|
||||||
|
const dockerHubPulls = await this.countDockerHubPulls();
|
||||||
const gitHubContributors = await this.countGitHubContributors();
|
const gitHubContributors = await this.countGitHubContributors();
|
||||||
const gitHubStargazers = await this.countGitHubStargazers();
|
const gitHubStargazers = await this.countGitHubStargazers();
|
||||||
const slackCommunityUsers = await this.countSlackCommunityUsers();
|
const slackCommunityUsers = await this.countSlackCommunityUsers();
|
||||||
@ -252,6 +275,7 @@ export class InfoService {
|
|||||||
statistics = {
|
statistics = {
|
||||||
activeUsers1d,
|
activeUsers1d,
|
||||||
activeUsers30d,
|
activeUsers30d,
|
||||||
|
dockerHubPulls,
|
||||||
gitHubContributors,
|
gitHubContributors,
|
||||||
gitHubStargazers,
|
gitHubStargazers,
|
||||||
newUsers30d,
|
newUsers30d,
|
||||||
|
@ -3,8 +3,8 @@ import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
|||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
||||||
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 { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import { Filter } from '@ghostfolio/common/interfaces';
|
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
@ -36,6 +36,7 @@ import { UpdateOrderDto } from './update-order.dto';
|
|||||||
@Controller('order')
|
@Controller('order')
|
||||||
export class OrderController {
|
export class OrderController {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly apiService: ApiService,
|
||||||
private readonly impersonationService: ImpersonationService,
|
private readonly impersonationService: ImpersonationService,
|
||||||
private readonly orderService: OrderService,
|
private readonly orderService: OrderService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
@ -73,43 +74,25 @@ export class OrderController {
|
|||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<Activities> {
|
): Promise<Activities> {
|
||||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
filterByAccounts,
|
||||||
const tagIds = filterByTags?.split(',') ?? [];
|
filterByAssetClasses,
|
||||||
|
filterByTags
|
||||||
const filters: Filter[] = [
|
});
|
||||||
...accountIds.map((accountId) => {
|
|
||||||
return <Filter>{
|
|
||||||
id: accountId,
|
|
||||||
type: 'ACCOUNT'
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
...assetClasses.map((assetClass) => {
|
|
||||||
return <Filter>{
|
|
||||||
id: assetClass,
|
|
||||||
type: 'ASSET_CLASS'
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
...tagIds.map((tagId) => {
|
|
||||||
return <Filter>{
|
|
||||||
id: tagId,
|
|
||||||
type: 'TAG'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(
|
await this.impersonationService.validateImpersonationId(
|
||||||
impersonationId,
|
impersonationId,
|
||||||
this.request.user.id
|
this.request.user.id
|
||||||
);
|
);
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
|
||||||
let activities = await this.orderService.getOrders({
|
let activities = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
includeDrafts: true,
|
includeDrafts: true,
|
||||||
userId: impersonationUserId || this.request.user.id
|
userId: impersonationUserId || this.request.user.id,
|
||||||
|
withExcludedAccounts: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|||||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { ApiModule } from '@ghostfolio/api/services/api/api.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';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
@ -18,6 +19,7 @@ import { OrderService } from './order.service';
|
|||||||
controllers: [OrderController],
|
controllers: [OrderController],
|
||||||
exports: [OrderService],
|
exports: [OrderService],
|
||||||
imports: [
|
imports: [
|
||||||
|
ApiModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
|
@ -189,13 +189,15 @@ export class OrderService {
|
|||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
types,
|
types,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId,
|
||||||
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
types?: TypeOfOrder[];
|
types?: TypeOfOrder[];
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<Activity[]> {
|
}): Promise<Activity[]> {
|
||||||
const where: Prisma.OrderWhereInput = { userId };
|
const where: Prisma.OrderWhereInput = { userId };
|
||||||
|
|
||||||
@ -284,7 +286,11 @@ export class OrderService {
|
|||||||
},
|
},
|
||||||
orderBy: { date: 'asc' }
|
orderBy: { date: 'asc' }
|
||||||
})
|
})
|
||||||
).map((order) => {
|
)
|
||||||
|
.filter((order) => {
|
||||||
|
return withExcludedAccounts || order.Account?.isExcluded === false;
|
||||||
|
})
|
||||||
|
.map((order) => {
|
||||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -21,6 +21,17 @@ function mockGetValue(symbol: string, date: Date) {
|
|||||||
|
|
||||||
return { marketPrice: 0 };
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
|
case 'BTCUSD':
|
||||||
|
if (isSameDay(parseDate('2015-01-01'), date)) {
|
||||||
|
return { marketPrice: 314.25 };
|
||||||
|
} else if (isSameDay(parseDate('2017-12-31'), date)) {
|
||||||
|
return { marketPrice: 14156.4 };
|
||||||
|
} else if (isSameDay(parseDate('2018-01-01'), date)) {
|
||||||
|
return { marketPrice: 13657.2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
case 'NOVN.SW':
|
case 'NOVN.SW':
|
||||||
if (isSameDay(parseDate('2022-04-11'), date)) {
|
if (isSameDay(parseDate('2022-04-11'), date)) {
|
||||||
return { marketPrice: 87.8 };
|
return { marketPrice: 87.8 };
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get current positions', () => {
|
||||||
|
it.only('with BTCUSD buy and sell partially', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
currentRateService,
|
||||||
|
currency: 'CHF',
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2015-01-01',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Bitcoin USD',
|
||||||
|
quantity: new Big(2),
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: new Big(320.43)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2017-12-31',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Bitcoin USD',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: new Big(14156.4)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||||
|
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2015-01-01')
|
||||||
|
);
|
||||||
|
|
||||||
|
const investments = portfolioCalculator.getInvestments();
|
||||||
|
|
||||||
|
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
currentValue: new Big('13657.2'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('27172.74'),
|
||||||
|
grossPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
hasErrors: false,
|
||||||
|
netPerformance: new Big('27172.74'),
|
||||||
|
netPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('320.43'),
|
||||||
|
currency: 'CHF',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
firstBuyDate: '2015-01-01',
|
||||||
|
grossPerformance: new Big('27172.74'),
|
||||||
|
grossPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
investment: new Big('320.43'),
|
||||||
|
netPerformance: new Big('27172.74'),
|
||||||
|
netPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
marketPrice: 13657.2,
|
||||||
|
quantity: new Big('1'),
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalInvestment: new Big('320.43')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(investments).toEqual([
|
||||||
|
{ date: '2015-01-01', investment: new Big('640.86') },
|
||||||
|
{ date: '2017-12-31', investment: new Big('320.43') }
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(investmentsByMonth).toEqual([
|
||||||
|
{ date: '2015-01-01', investment: new Big('640.86') },
|
||||||
|
{ date: '2017-12-01', investment: new Big('-14156.4') }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -22,7 +22,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy and sell', async () => {
|
it.only('with NOVN.SW buy and sell partially', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
@ -14,14 +14,14 @@ import {
|
|||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
isBefore,
|
isBefore,
|
||||||
|
isSameDay,
|
||||||
isSameMonth,
|
isSameMonth,
|
||||||
isSameYear,
|
isSameYear,
|
||||||
isWithinInterval,
|
|
||||||
max,
|
max,
|
||||||
min,
|
min,
|
||||||
set
|
set
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { first, flatten, isNumber, sortBy } from 'lodash';
|
import { first, flatten, isNumber, last, sortBy } from 'lodash';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { CurrentPositions } from './interfaces/current-positions.interface';
|
import { CurrentPositions } from './interfaces/current-positions.interface';
|
||||||
@ -168,6 +168,133 @@ export class PortfolioCalculator {
|
|||||||
this.transactionPoints = transactionPoints;
|
this.transactionPoints = transactionPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getChartData(start: Date, end = new Date(Date.now()), step = 1) {
|
||||||
|
const symbols: { [symbol: string]: boolean } = {};
|
||||||
|
|
||||||
|
const transactionPointsBeforeEndDate =
|
||||||
|
this.transactionPoints?.filter((transactionPoint) => {
|
||||||
|
return isBefore(parseDate(transactionPoint.date), end);
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
|
const firstIndex = transactionPointsBeforeEndDate.length;
|
||||||
|
const dates: Date[] = [];
|
||||||
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
|
const currencies: { [symbol: string]: string } = {};
|
||||||
|
|
||||||
|
let day = start;
|
||||||
|
|
||||||
|
while (isBefore(day, end)) {
|
||||||
|
dates.push(resetHours(day));
|
||||||
|
day = addDays(day, step);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSameDay(last(dates), end)) {
|
||||||
|
dates.push(resetHours(end));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
||||||
|
dataGatheringItems.push({
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
symbol: item.symbol
|
||||||
|
});
|
||||||
|
currencies[item.symbol] = item.currency;
|
||||||
|
symbols[item.symbol] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketSymbols = await this.currentRateService.getValues({
|
||||||
|
currencies,
|
||||||
|
dataGatheringItems,
|
||||||
|
dateQuery: {
|
||||||
|
in: dates
|
||||||
|
},
|
||||||
|
userCurrency: this.currency
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketSymbolMap: {
|
||||||
|
[date: string]: { [symbol: string]: Big };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
for (const marketSymbol of marketSymbols) {
|
||||||
|
const dateString = format(marketSymbol.date, DATE_FORMAT);
|
||||||
|
if (!marketSymbolMap[dateString]) {
|
||||||
|
marketSymbolMap[dateString] = {};
|
||||||
|
}
|
||||||
|
if (marketSymbol.marketPriceInBaseCurrency) {
|
||||||
|
marketSymbolMap[dateString][marketSymbol.symbol] = new Big(
|
||||||
|
marketSymbol.marketPriceInBaseCurrency
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const netPerformanceValuesBySymbol: {
|
||||||
|
[symbol: string]: { [date: string]: Big };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const investmentValuesBySymbol: {
|
||||||
|
[symbol: string]: { [date: string]: Big };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const totalNetPerformanceValues: { [date: string]: Big } = {};
|
||||||
|
const totalInvestmentValues: { [date: string]: Big } = {};
|
||||||
|
|
||||||
|
for (const symbol of Object.keys(symbols)) {
|
||||||
|
const { netPerformanceValues, investmentValues } = this.getSymbolMetrics({
|
||||||
|
end,
|
||||||
|
marketSymbolMap,
|
||||||
|
start,
|
||||||
|
step,
|
||||||
|
symbol,
|
||||||
|
isChartMode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
netPerformanceValuesBySymbol[symbol] = netPerformanceValues;
|
||||||
|
investmentValuesBySymbol[symbol] = investmentValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const currentDate of dates) {
|
||||||
|
const dateString = format(currentDate, DATE_FORMAT);
|
||||||
|
|
||||||
|
for (const symbol of Object.keys(netPerformanceValuesBySymbol)) {
|
||||||
|
totalNetPerformanceValues[dateString] =
|
||||||
|
totalNetPerformanceValues[dateString] ?? new Big(0);
|
||||||
|
|
||||||
|
if (netPerformanceValuesBySymbol[symbol]?.[dateString]) {
|
||||||
|
totalNetPerformanceValues[dateString] = totalNetPerformanceValues[
|
||||||
|
dateString
|
||||||
|
].add(netPerformanceValuesBySymbol[symbol][dateString]);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalInvestmentValues[dateString] =
|
||||||
|
totalInvestmentValues[dateString] ?? new Big(0);
|
||||||
|
|
||||||
|
if (investmentValuesBySymbol[symbol]?.[dateString]) {
|
||||||
|
totalInvestmentValues[dateString] = totalInvestmentValues[
|
||||||
|
dateString
|
||||||
|
].add(investmentValuesBySymbol[symbol][dateString]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(totalNetPerformanceValues).map((date) => {
|
||||||
|
const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
|
||||||
|
? 0
|
||||||
|
: totalNetPerformanceValues[date]
|
||||||
|
.div(totalInvestmentValues[date])
|
||||||
|
.mul(100)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
netPerformanceInPercentage,
|
||||||
|
netPerformance: totalNetPerformanceValues[date].toNumber(),
|
||||||
|
totalInvestment: totalInvestmentValues[date].toNumber(),
|
||||||
|
value: totalInvestmentValues[date]
|
||||||
|
.plus(totalNetPerformanceValues[date])
|
||||||
|
.toNumber()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getCurrentPositions(
|
public async getCurrentPositions(
|
||||||
start: Date,
|
start: Date,
|
||||||
end = new Date(Date.now())
|
end = new Date(Date.now())
|
||||||
@ -710,15 +837,19 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
private getSymbolMetrics({
|
private getSymbolMetrics({
|
||||||
end,
|
end,
|
||||||
|
isChartMode = false,
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
start,
|
start,
|
||||||
|
step = 1,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
end: Date;
|
end: Date;
|
||||||
|
isChartMode?: boolean;
|
||||||
marketSymbolMap: {
|
marketSymbolMap: {
|
||||||
[date: string]: { [symbol: string]: Big };
|
[date: string]: { [symbol: string]: Big };
|
||||||
};
|
};
|
||||||
start: Date;
|
start: Date;
|
||||||
|
step?: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
let orders: PortfolioOrderItem[] = this.orders.filter((order) => {
|
let orders: PortfolioOrderItem[] = this.orders.filter((order) => {
|
||||||
@ -767,12 +898,14 @@ export class PortfolioCalculator {
|
|||||||
let grossPerformanceFromSells = new Big(0);
|
let grossPerformanceFromSells = new Big(0);
|
||||||
let initialValue: Big;
|
let initialValue: Big;
|
||||||
let investmentAtStartDate: Big;
|
let investmentAtStartDate: Big;
|
||||||
|
const investmentValues: { [date: string]: Big } = {};
|
||||||
let lastAveragePrice = new Big(0);
|
let lastAveragePrice = new Big(0);
|
||||||
let lastTransactionInvestment = new Big(0);
|
// let lastTransactionInvestment = new Big(0);
|
||||||
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
// let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
||||||
let maxTotalInvestment = new Big(0);
|
let maxTotalInvestment = new Big(0);
|
||||||
let timeWeightedGrossPerformancePercentage = new Big(1);
|
const netPerformanceValues: { [date: string]: Big } = {};
|
||||||
let timeWeightedNetPerformancePercentage = new Big(1);
|
// let timeWeightedGrossPerformancePercentage = new Big(1);
|
||||||
|
// let timeWeightedNetPerformancePercentage = new Big(1);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
|
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
|
||||||
let totalUnits = new Big(0);
|
let totalUnits = new Big(0);
|
||||||
@ -805,6 +938,41 @@ export class PortfolioCalculator {
|
|||||||
unitPrice: unitPriceAtEndDate
|
unitPrice: unitPriceAtEndDate
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let day = start;
|
||||||
|
let lastUnitPrice: Big;
|
||||||
|
|
||||||
|
if (isChartMode) {
|
||||||
|
const datesWithOrders = {};
|
||||||
|
|
||||||
|
for (const order of orders) {
|
||||||
|
datesWithOrders[order.date] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (isBefore(day, end)) {
|
||||||
|
const hasDate = datesWithOrders[format(day, DATE_FORMAT)];
|
||||||
|
|
||||||
|
if (!hasDate) {
|
||||||
|
orders.push({
|
||||||
|
symbol,
|
||||||
|
currency: null,
|
||||||
|
date: format(day, DATE_FORMAT),
|
||||||
|
dataSource: null,
|
||||||
|
fee: new Big(0),
|
||||||
|
name: '',
|
||||||
|
quantity: new Big(0),
|
||||||
|
type: TypeOfOrder.BUY,
|
||||||
|
unitPrice:
|
||||||
|
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
|
||||||
|
lastUnitPrice
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUnitPrice = last(orders).unitPrice;
|
||||||
|
|
||||||
|
day = addDays(day, step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort orders so that the start and end placeholder order are at the right
|
// Sort orders so that the start and end placeholder order are at the right
|
||||||
// position
|
// position
|
||||||
orders = sortBy(orders, (order) => {
|
orders = sortBy(orders, (order) => {
|
||||||
@ -832,6 +1000,12 @@ export class PortfolioCalculator {
|
|||||||
for (let i = 0; i < orders.length; i += 1) {
|
for (let i = 0; i < orders.length; i += 1) {
|
||||||
const order = orders[i];
|
const order = orders[i];
|
||||||
|
|
||||||
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
|
console.log();
|
||||||
|
console.log();
|
||||||
|
console.log(i + 1, order.type, order.itemType);
|
||||||
|
}
|
||||||
|
|
||||||
if (order.itemType === 'start') {
|
if (order.itemType === 'start') {
|
||||||
// Take the unit price of the order as the market price if there are no
|
// Take the unit price of the order as the market price if there are no
|
||||||
// orders of this symbol before the start date
|
// orders of this symbol before the start date
|
||||||
@ -859,9 +1033,21 @@ export class PortfolioCalculator {
|
|||||||
valueAtStartDate = valueOfInvestmentBeforeTransaction;
|
valueAtStartDate = valueOfInvestmentBeforeTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transactionInvestment = order.quantity
|
const transactionInvestment =
|
||||||
.mul(order.unitPrice)
|
order.type === 'BUY'
|
||||||
.mul(this.getFactor(order.type));
|
? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||||
|
: totalUnits.gt(0)
|
||||||
|
? totalInvestment
|
||||||
|
.div(totalUnits)
|
||||||
|
.mul(order.quantity)
|
||||||
|
.mul(this.getFactor(order.type))
|
||||||
|
: new Big(0);
|
||||||
|
|
||||||
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
|
console.log('totalInvestment', totalInvestment.toNumber());
|
||||||
|
console.log('order.quantity', order.quantity.toNumber());
|
||||||
|
console.log('transactionInvestment', transactionInvestment.toNumber());
|
||||||
|
}
|
||||||
|
|
||||||
totalInvestment = totalInvestment.plus(transactionInvestment);
|
totalInvestment = totalInvestment.plus(transactionInvestment);
|
||||||
|
|
||||||
@ -910,70 +1096,101 @@ export class PortfolioCalculator {
|
|||||||
? new Big(0)
|
? new Big(0)
|
||||||
: totalInvestmentWithGrossPerformanceFromSell.div(totalUnits);
|
: totalInvestmentWithGrossPerformanceFromSell.div(totalUnits);
|
||||||
|
|
||||||
const newGrossPerformance = valueOfInvestment
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
.minus(totalInvestmentWithGrossPerformanceFromSell)
|
console.log(
|
||||||
.plus(grossPerformanceFromSells);
|
'totalInvestmentWithGrossPerformanceFromSell',
|
||||||
|
totalInvestmentWithGrossPerformanceFromSell.toNumber()
|
||||||
if (
|
|
||||||
i > indexOfStartOrder &&
|
|
||||||
!lastValueOfInvestmentBeforeTransaction
|
|
||||||
.plus(lastTransactionInvestment)
|
|
||||||
.eq(0)
|
|
||||||
) {
|
|
||||||
const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
|
||||||
.minus(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.div(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
timeWeightedGrossPerformancePercentage =
|
'grossPerformanceFromSells',
|
||||||
timeWeightedGrossPerformancePercentage.mul(
|
grossPerformanceFromSells.toNumber()
|
||||||
new Big(1).plus(grossHoldingPeriodReturn)
|
|
||||||
);
|
|
||||||
|
|
||||||
const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
|
||||||
.minus(fees.minus(feesAtStartDate))
|
|
||||||
.minus(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.div(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
timeWeightedNetPerformancePercentage =
|
|
||||||
timeWeightedNetPerformancePercentage.mul(
|
|
||||||
new Big(1).plus(netHoldingPeriodReturn)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newGrossPerformance = valueOfInvestment
|
||||||
|
.minus(totalInvestment)
|
||||||
|
.plus(grossPerformanceFromSells);
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// i > indexOfStartOrder &&
|
||||||
|
// !lastValueOfInvestmentBeforeTransaction
|
||||||
|
// .plus(lastTransactionInvestment)
|
||||||
|
// .eq(0)
|
||||||
|
// ) {
|
||||||
|
// const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
||||||
|
// .minus(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// .div(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// timeWeightedGrossPerformancePercentage =
|
||||||
|
// timeWeightedGrossPerformancePercentage.mul(
|
||||||
|
// new Big(1).plus(grossHoldingPeriodReturn)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
||||||
|
// .minus(fees.minus(feesAtStartDate))
|
||||||
|
// .minus(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// .div(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// timeWeightedNetPerformancePercentage =
|
||||||
|
// timeWeightedNetPerformancePercentage.mul(
|
||||||
|
// new Big(1).plus(netHoldingPeriodReturn)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
grossPerformance = newGrossPerformance;
|
grossPerformance = newGrossPerformance;
|
||||||
|
|
||||||
lastTransactionInvestment = transactionInvestment;
|
// lastTransactionInvestment = transactionInvestment;
|
||||||
|
|
||||||
lastValueOfInvestmentBeforeTransaction =
|
// lastValueOfInvestmentBeforeTransaction =
|
||||||
valueOfInvestmentBeforeTransaction;
|
// valueOfInvestmentBeforeTransaction;
|
||||||
|
|
||||||
if (order.itemType === 'start') {
|
if (order.itemType === 'start') {
|
||||||
feesAtStartDate = fees;
|
feesAtStartDate = fees;
|
||||||
grossPerformanceAtStartDate = grossPerformance;
|
grossPerformanceAtStartDate = grossPerformance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isChartMode && i > indexOfStartOrder) {
|
||||||
|
netPerformanceValues[order.date] = grossPerformance
|
||||||
|
.minus(grossPerformanceAtStartDate)
|
||||||
|
.minus(fees.minus(feesAtStartDate));
|
||||||
|
|
||||||
|
investmentValues[order.date] = maxTotalInvestment;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeWeightedGrossPerformancePercentage =
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
timeWeightedGrossPerformancePercentage.minus(1);
|
console.log('totalInvestment', totalInvestment.toNumber());
|
||||||
|
console.log(
|
||||||
|
'totalGrossPerformance',
|
||||||
|
grossPerformance.minus(grossPerformanceAtStartDate).toNumber()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
timeWeightedNetPerformancePercentage =
|
if (i === indexOfEndOrder) {
|
||||||
timeWeightedNetPerformancePercentage.minus(1);
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeWeightedGrossPerformancePercentage =
|
||||||
|
// timeWeightedGrossPerformancePercentage.minus(1);
|
||||||
|
|
||||||
|
// timeWeightedNetPerformancePercentage =
|
||||||
|
// timeWeightedNetPerformancePercentage.minus(1);
|
||||||
|
|
||||||
const totalGrossPerformance = grossPerformance.minus(
|
const totalGrossPerformance = grossPerformance.minus(
|
||||||
grossPerformanceAtStartDate
|
grossPerformanceAtStartDate
|
||||||
@ -1052,7 +1269,9 @@ export class PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
initialValue,
|
initialValue,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
|
investmentValues,
|
||||||
netPerformancePercentage,
|
netPerformancePercentage,
|
||||||
|
netPerformanceValues,
|
||||||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
||||||
netPerformance: totalNetPerformance,
|
netPerformance: totalNetPerformance,
|
||||||
grossPerformance: totalGrossPerformance
|
grossPerformance: totalGrossPerformance
|
||||||
|
@ -7,18 +7,15 @@ import {
|
|||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
||||||
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 { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
|
||||||
import {
|
import {
|
||||||
Filter,
|
|
||||||
PortfolioChart,
|
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport
|
||||||
PortfolioSummary
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import type {
|
import type {
|
||||||
@ -40,7 +37,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { ViewMode } from '@prisma/client';
|
import Big from 'big.js';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
||||||
@ -53,6 +50,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accessService: AccessService,
|
private readonly accessService: AccessService,
|
||||||
|
private readonly apiService: ApiService,
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly portfolioService: PortfolioService,
|
private readonly portfolioService: PortfolioService,
|
||||||
@ -62,75 +60,6 @@ export class PortfolioController {
|
|||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('chart')
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
public async getChart(
|
|
||||||
@Headers('impersonation-id') impersonationId: string,
|
|
||||||
@Query('range') range
|
|
||||||
): Promise<PortfolioChart> {
|
|
||||||
const historicalDataContainer = await this.portfolioService.getChart(
|
|
||||||
impersonationId,
|
|
||||||
range
|
|
||||||
);
|
|
||||||
|
|
||||||
let chartData = historicalDataContainer.items;
|
|
||||||
|
|
||||||
let hasError = false;
|
|
||||||
|
|
||||||
chartData.forEach((chartDataItem) => {
|
|
||||||
if (hasNotDefinedValuesInObject(chartDataItem)) {
|
|
||||||
hasError = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
impersonationId ||
|
|
||||||
this.userService.isRestrictedView(this.request.user)
|
|
||||||
) {
|
|
||||||
let maxValue = 0;
|
|
||||||
|
|
||||||
chartData.forEach((portfolioItem) => {
|
|
||||||
if (portfolioItem.value > maxValue) {
|
|
||||||
maxValue = portfolioItem.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
chartData = chartData.map((historicalDataItem) => {
|
|
||||||
return {
|
|
||||||
...historicalDataItem,
|
|
||||||
marketPrice: Number((historicalDataItem.value / maxValue).toFixed(2))
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasError,
|
|
||||||
chart: chartData,
|
|
||||||
isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
|
|
||||||
isAllTimeLow: historicalDataContainer.isAllTimeLow
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@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)
|
||||||
@ -139,48 +68,38 @@ export class PortfolioController {
|
|||||||
@Headers('impersonation-id') impersonationId: string,
|
@Headers('impersonation-id') impersonationId: string,
|
||||||
@Query('accounts') filterByAccounts?: string,
|
@Query('accounts') filterByAccounts?: string,
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') range?: DateRange,
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
||||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
filterByAccounts,
|
||||||
const tagIds = filterByTags?.split(',') ?? [];
|
filterByAssetClasses,
|
||||||
|
filterByTags
|
||||||
|
});
|
||||||
|
|
||||||
const filters: Filter[] = [
|
const {
|
||||||
...accountIds.map((accountId) => {
|
accounts,
|
||||||
return <Filter>{
|
filteredValueInBaseCurrency,
|
||||||
id: accountId,
|
filteredValueInPercentage,
|
||||||
type: 'ACCOUNT'
|
hasErrors,
|
||||||
};
|
holdings,
|
||||||
}),
|
summary,
|
||||||
...assetClasses.map((assetClass) => {
|
totalValueInBaseCurrency
|
||||||
return <Filter>{
|
} = await this.portfolioService.getDetails({
|
||||||
id: assetClass,
|
dateRange,
|
||||||
type: 'ASSET_CLASS'
|
filters,
|
||||||
};
|
|
||||||
}),
|
|
||||||
...tagIds.map((tagId) => {
|
|
||||||
return <Filter>{
|
|
||||||
id: tagId,
|
|
||||||
type: 'TAG'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
const { accounts, holdings, hasErrors } =
|
|
||||||
await this.portfolioService.getDetails(
|
|
||||||
impersonationId,
|
impersonationId,
|
||||||
this.request.user.id,
|
userId: this.request.user.id
|
||||||
range,
|
});
|
||||||
filters
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||||
hasError = true;
|
hasError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let portfolioSummary = summary;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId ||
|
impersonationId ||
|
||||||
this.userService.isRestrictedView(this.request.user)
|
this.userService.isRestrictedView(this.request.user)
|
||||||
@ -196,7 +115,7 @@ export class PortfolioController {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||||
portfolioPosition.currency,
|
portfolioPosition.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.settings.baseCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
@ -214,6 +133,22 @@ export class PortfolioController {
|
|||||||
accounts[name].current = current / totalValue;
|
accounts[name].current = current / totalValue;
|
||||||
accounts[name].original = original / totalInvestment;
|
accounts[name].original = original / totalInvestment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
portfolioSummary = nullifyValuesInObject(summary, [
|
||||||
|
'cash',
|
||||||
|
'committedFunds',
|
||||||
|
'currentGrossPerformance',
|
||||||
|
'currentNetPerformance',
|
||||||
|
'currentValue',
|
||||||
|
'dividend',
|
||||||
|
'emergencyFund',
|
||||||
|
'excludedAccountsAndActivities',
|
||||||
|
'fees',
|
||||||
|
'items',
|
||||||
|
'netWorth',
|
||||||
|
'totalBuy',
|
||||||
|
'totalSell'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasDetails = true;
|
let hasDetails = true;
|
||||||
@ -235,8 +170,12 @@ export class PortfolioController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
|
filteredValueInBaseCurrency,
|
||||||
|
filteredValueInPercentage,
|
||||||
hasError,
|
hasError,
|
||||||
holdings
|
holdings,
|
||||||
|
totalValueInBaseCurrency,
|
||||||
|
summary: hasDetails ? portfolioSummary : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +183,7 @@ export class PortfolioController {
|
|||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getInvestments(
|
public async getInvestments(
|
||||||
@Headers('impersonation-id') impersonationId: string,
|
@Headers('impersonation-id') impersonationId: string,
|
||||||
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('groupBy') groupBy?: GroupBy
|
@Query('groupBy') groupBy?: GroupBy
|
||||||
): Promise<PortfolioInvestments> {
|
): Promise<PortfolioInvestments> {
|
||||||
if (
|
if (
|
||||||
@ -259,12 +199,16 @@ export class PortfolioController {
|
|||||||
let investments: InvestmentItem[];
|
let investments: InvestmentItem[];
|
||||||
|
|
||||||
if (groupBy === 'month') {
|
if (groupBy === 'month') {
|
||||||
investments = await this.portfolioService.getInvestments(
|
investments = await this.portfolioService.getInvestments({
|
||||||
|
dateRange,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
'month'
|
groupBy: 'month'
|
||||||
);
|
});
|
||||||
} else {
|
} else {
|
||||||
investments = await this.portfolioService.getInvestments(impersonationId);
|
investments = await this.portfolioService.getInvestments({
|
||||||
|
dateRange,
|
||||||
|
impersonationId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -282,29 +226,51 @@ export class PortfolioController {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { firstOrderDate: parseDate(investments[0]?.date), investments };
|
return { investments };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('performance')
|
@Get('performance')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getPerformance(
|
@Version('2')
|
||||||
|
public async getPerformanceV2(
|
||||||
@Headers('impersonation-id') impersonationId: string,
|
@Headers('impersonation-id') impersonationId: string,
|
||||||
@Query('range') range
|
@Query('range') dateRange: DateRange = 'max'
|
||||||
): Promise<PortfolioPerformanceResponse> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const performanceInformation = await this.portfolioService.getPerformance(
|
const performanceInformation = await this.portfolioService.getPerformance({
|
||||||
|
dateRange,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
range
|
userId: this.request.user.id
|
||||||
);
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId ||
|
impersonationId ||
|
||||||
this.request.user.Settings.viewMode === ViewMode.ZEN ||
|
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
||||||
this.userService.isRestrictedView(this.request.user)
|
this.userService.isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
|
performanceInformation.chart = performanceInformation.chart.map(
|
||||||
|
({ date, netPerformanceInPercentage, totalInvestment, value }) => {
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
netPerformanceInPercentage,
|
||||||
|
totalInvestment: new Big(totalInvestment)
|
||||||
|
.div(performanceInformation.performance.totalInvestment)
|
||||||
|
.toNumber(),
|
||||||
|
value: new Big(value)
|
||||||
|
.div(performanceInformation.performance.currentValue)
|
||||||
|
.toNumber()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
performanceInformation.performance = nullifyValuesInObject(
|
performanceInformation.performance = nullifyValuesInObject(
|
||||||
performanceInformation.performance,
|
performanceInformation.performance,
|
||||||
['currentGrossPerformance', 'currentValue']
|
[
|
||||||
|
'currentGrossPerformance',
|
||||||
|
'currentNetPerformance',
|
||||||
|
'currentValue',
|
||||||
|
'totalInvestment'
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,11 +282,11 @@ export class PortfolioController {
|
|||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getPositions(
|
public async getPositions(
|
||||||
@Headers('impersonation-id') impersonationId: string,
|
@Headers('impersonation-id') impersonationId: string,
|
||||||
@Query('range') range
|
@Query('range') dateRange: DateRange = 'max'
|
||||||
): Promise<PortfolioPositions> {
|
): Promise<PortfolioPositions> {
|
||||||
const result = await this.portfolioService.getPositions(
|
const result = await this.portfolioService.getPositions(
|
||||||
impersonationId,
|
impersonationId,
|
||||||
range
|
dateRange
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -361,12 +327,12 @@ export class PortfolioController {
|
|||||||
hasDetails = user.subscription.type === 'Premium';
|
hasDetails = user.subscription.type === 'Premium';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { holdings } = await this.portfolioService.getDetails(
|
const { holdings } = await this.portfolioService.getDetails({
|
||||||
access.userId,
|
dateRange: 'max',
|
||||||
access.userId,
|
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
||||||
'max',
|
impersonationId: access.userId,
|
||||||
[{ id: 'EQUITY', type: 'ASSET_CLASS' }]
|
userId: user.id
|
||||||
);
|
});
|
||||||
|
|
||||||
const portfolioPublicDetails: PortfolioPublicDetails = {
|
const portfolioPublicDetails: PortfolioPublicDetails = {
|
||||||
hasDetails,
|
hasDetails,
|
||||||
@ -379,7 +345,8 @@ export class PortfolioController {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||||
portfolioPosition.currency,
|
portfolioPosition.currency,
|
||||||
this.request.user?.Settings?.currency ?? this.baseCurrency
|
this.request.user?.Settings?.settings.baseCurrency ??
|
||||||
|
this.baseCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
@ -402,46 +369,6 @@ export class PortfolioController {
|
|||||||
return portfolioPublicDetails;
|
return portfolioPublicDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('summary')
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
public async getSummary(
|
|
||||||
@Headers('impersonation-id') impersonationId
|
|
||||||
): Promise<PortfolioSummary> {
|
|
||||||
if (
|
|
||||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
|
||||||
this.request.user.subscription.type === 'Basic'
|
|
||||||
) {
|
|
||||||
throw new HttpException(
|
|
||||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
|
||||||
StatusCodes.FORBIDDEN
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let summary = await this.portfolioService.getSummary(impersonationId);
|
|
||||||
|
|
||||||
if (
|
|
||||||
impersonationId ||
|
|
||||||
this.userService.isRestrictedView(this.request.user)
|
|
||||||
) {
|
|
||||||
summary = nullifyValuesInObject(summary, [
|
|
||||||
'cash',
|
|
||||||
'committedFunds',
|
|
||||||
'currentGrossPerformance',
|
|
||||||
'currentNetPerformance',
|
|
||||||
'currentValue',
|
|
||||||
'dividend',
|
|
||||||
'emergencyFund',
|
|
||||||
'fees',
|
|
||||||
'items',
|
|
||||||
'netWorth',
|
|
||||||
'totalBuy',
|
|
||||||
'totalSell'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('position/:dataSource/:symbol')
|
@Get('position/:dataSource/:symbol')
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
@ -2,6 +2,7 @@ import { AccessModule } from '@ghostfolio/api/app/access/access.module';
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { ApiModule } from '@ghostfolio/api/services/api/api.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';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
@ -22,6 +23,7 @@ import { RulesService } from './rules.service';
|
|||||||
exports: [PortfolioService],
|
exports: [PortfolioService],
|
||||||
imports: [
|
imports: [
|
||||||
AccessModule,
|
AccessModule,
|
||||||
|
ApiModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,12 @@
|
|||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import type {
|
||||||
import type { DateRange } from '@ghostfolio/common/types';
|
ColorScheme,
|
||||||
import { ViewMode } from '@prisma/client';
|
DateRange,
|
||||||
|
ViewMode
|
||||||
|
} from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsIn,
|
IsIn,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsObject,
|
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString
|
IsString
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@ -15,9 +16,13 @@ export class UpdateUserSettingDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
baseCurrency?: string;
|
baseCurrency?: string;
|
||||||
|
|
||||||
@IsObject()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
benchmark?: UniqueAsset;
|
benchmark?: string;
|
||||||
|
|
||||||
|
@IsIn(<ColorScheme[]>['DARK', 'LIGHT'])
|
||||||
|
@IsOptional()
|
||||||
|
colorScheme?: ColorScheme;
|
||||||
|
|
||||||
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
|
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -22,6 +22,7 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { User as UserModel } from '@prisma/client';
|
import { User as UserModel } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
import { size } from 'lodash';
|
||||||
|
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
||||||
@ -100,6 +101,12 @@ export class UserController {
|
|||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
||||||
if (
|
if (
|
||||||
|
size(data) === 1 &&
|
||||||
|
(data.benchmark || data.dateRange) &&
|
||||||
|
this.request.user.role === 'DEMO'
|
||||||
|
) {
|
||||||
|
// Allow benchmark or date range change for demo user
|
||||||
|
} else if (
|
||||||
!hasPermission(
|
!hasPermission(
|
||||||
this.request.user.permissions,
|
this.request.user.permissions,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
|
@ -69,8 +69,8 @@ export class UserService {
|
|||||||
}),
|
}),
|
||||||
accounts: Account,
|
accounts: Account,
|
||||||
settings: {
|
settings: {
|
||||||
...(<UserSettings>(<unknown>Settings.settings)),
|
...(<UserSettings>Settings.settings),
|
||||||
locale: (<UserSettings>(<unknown>Settings.settings))?.locale ?? aLocale
|
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -88,10 +88,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isRestrictedView(aUser: UserWithSettings) {
|
public isRestrictedView(aUser: UserWithSettings) {
|
||||||
return (
|
return aUser.Settings.settings.isRestrictedView ?? false;
|
||||||
(aUser.Settings.settings as unknown as UserSettings)?.isRestrictedView ??
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async user(
|
public async user(
|
||||||
@ -134,11 +131,9 @@ export class UserService {
|
|||||||
} else if (user) {
|
} else if (user) {
|
||||||
// Set default settings if needed
|
// Set default settings if needed
|
||||||
user.Settings = {
|
user.Settings = {
|
||||||
currency: null,
|
|
||||||
settings: {},
|
settings: {},
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
userId: user?.id,
|
userId: user?.id
|
||||||
viewMode: 'DEFAULT'
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,10 +234,12 @@ export class UserService {
|
|||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
create: {
|
create: {
|
||||||
|
settings: {
|
||||||
currency: this.baseCurrency
|
currency: this.baseCurrency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.provider === 'ANONYMOUS') {
|
if (data.provider === 'ANONYMOUS') {
|
||||||
|
@ -41,6 +41,14 @@ export class RedactValuesInResponseInterceptor<T>
|
|||||||
return activity;
|
return activity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.filteredValueInBaseCurrency) {
|
||||||
|
data.filteredValueInBaseCurrency = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.totalValueInBaseCurrency) {
|
||||||
|
data.totalValueInBaseCurrency = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
9
apps/api/src/services/api/api.module.ts
Normal file
9
apps/api/src/services/api/api.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [ApiService],
|
||||||
|
providers: [ApiService]
|
||||||
|
})
|
||||||
|
export class ApiModule {}
|
42
apps/api/src/services/api/api.service.ts
Normal file
42
apps/api/src/services/api/api.service.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Filter } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiService {
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public buildFiltersFromQueryParams({
|
||||||
|
filterByAccounts,
|
||||||
|
filterByAssetClasses,
|
||||||
|
filterByTags
|
||||||
|
}: {
|
||||||
|
filterByAccounts?: string;
|
||||||
|
filterByAssetClasses?: string;
|
||||||
|
filterByTags?: string;
|
||||||
|
}): Filter[] {
|
||||||
|
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||||
|
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||||
|
const tagIds = filterByTags?.split(',') ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...accountIds.map((accountId) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: accountId,
|
||||||
|
type: 'ACCOUNT'
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...assetClasses.map((assetClass) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: assetClass,
|
||||||
|
type: 'ASSET_CLASS'
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...tagIds.map((tagId) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: tagId,
|
||||||
|
type: 'TAG'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -38,7 +38,7 @@ export class ConfigurationService {
|
|||||||
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
|
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
|
||||||
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
||||||
PORT: port({ default: 3333 }),
|
PORT: port({ default: 3333 }),
|
||||||
RAKUTEN_RAPID_API_KEY: str({ default: '' }),
|
RAPID_API_API_KEY: str({ default: '' }),
|
||||||
REDIS_HOST: host({ default: 'localhost' }),
|
REDIS_HOST: host({ default: 'localhost' }),
|
||||||
REDIS_PASSWORD: str({ default: '' }),
|
REDIS_PASSWORD: str({ default: '' }),
|
||||||
REDIS_PORT: port({ default: 6379 }),
|
REDIS_PORT: port({ default: 6379 }),
|
||||||
|
@ -280,7 +280,7 @@ export class DataGatheringService {
|
|||||||
return (
|
return (
|
||||||
dataSource !== DataSource.GHOSTFOLIO &&
|
dataSource !== DataSource.GHOSTFOLIO &&
|
||||||
dataSource !== DataSource.MANUAL &&
|
dataSource !== DataSource.MANUAL &&
|
||||||
dataSource !== DataSource.RAKUTEN
|
dataSource !== DataSource.RAPID_API
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map(({ dataSource, symbol }) => {
|
.map(({ dataSource, symbol }) => {
|
||||||
|
@ -5,7 +5,7 @@ import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider
|
|||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service';
|
import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service';
|
||||||
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
|
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service';
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||||
@ -27,7 +27,7 @@ import { DataProviderService } from './data-provider.service';
|
|||||||
GhostfolioScraperApiService,
|
GhostfolioScraperApiService,
|
||||||
GoogleSheetsService,
|
GoogleSheetsService,
|
||||||
ManualService,
|
ManualService,
|
||||||
RakutenRapidApiService,
|
RapidApiService,
|
||||||
YahooFinanceService,
|
YahooFinanceService,
|
||||||
{
|
{
|
||||||
inject: [
|
inject: [
|
||||||
@ -36,7 +36,7 @@ import { DataProviderService } from './data-provider.service';
|
|||||||
GhostfolioScraperApiService,
|
GhostfolioScraperApiService,
|
||||||
GoogleSheetsService,
|
GoogleSheetsService,
|
||||||
ManualService,
|
ManualService,
|
||||||
RakutenRapidApiService,
|
RapidApiService,
|
||||||
YahooFinanceService
|
YahooFinanceService
|
||||||
],
|
],
|
||||||
provide: 'DataProviderInterfaces',
|
provide: 'DataProviderInterfaces',
|
||||||
@ -46,7 +46,7 @@ import { DataProviderService } from './data-provider.service';
|
|||||||
ghostfolioScraperApiService,
|
ghostfolioScraperApiService,
|
||||||
googleSheetsService,
|
googleSheetsService,
|
||||||
manualService,
|
manualService,
|
||||||
rakutenRapidApiService,
|
rapidApiService,
|
||||||
yahooFinanceService
|
yahooFinanceService
|
||||||
) => [
|
) => [
|
||||||
alphaVantageService,
|
alphaVantageService,
|
||||||
@ -54,7 +54,7 @@ import { DataProviderService } from './data-provider.service';
|
|||||||
ghostfolioScraperApiService,
|
ghostfolioScraperApiService,
|
||||||
googleSheetsService,
|
googleSheetsService,
|
||||||
manualService,
|
manualService,
|
||||||
rakutenRapidApiService,
|
rapidApiService,
|
||||||
yahooFinanceService
|
yahooFinanceService
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export interface IRakutenRapidApiResponse {}
|
|
@ -0,0 +1 @@
|
|||||||
|
export interface IRapidApiResponse {}
|
@ -15,14 +15,14 @@ import bent from 'bent';
|
|||||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RakutenRapidApiService implements DataProviderInterface {
|
export class RapidApiService implements DataProviderInterface {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
return !!this.configurationService.get('RAKUTEN_RAPID_API_KEY');
|
return !!this.configurationService.get('RAPID_API_API_KEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAssetProfile(
|
public async getAssetProfile(
|
||||||
@ -103,7 +103,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getName(): DataSource {
|
public getName(): DataSource {
|
||||||
return DataSource.RAKUTEN;
|
return DataSource.RAPID_API;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes(
|
public async getQuotes(
|
||||||
@ -129,7 +129,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'RakutenRapidApiService');
|
Logger.error(error, 'RapidApiService');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
@ -155,16 +155,14 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
{
|
{
|
||||||
useQueryString: true,
|
useQueryString: true,
|
||||||
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
||||||
'x-rapidapi-key': this.configurationService.get(
|
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
|
||||||
'RAKUTEN_RAPID_API_KEY'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { fgi } = await get();
|
const { fgi } = await get();
|
||||||
return fgi;
|
return fgi;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'RakutenRapidApiService');
|
Logger.error(error, 'RapidApiService');
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@ -57,8 +58,15 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
* DOGEUSD -> DOGE-USD
|
* DOGEUSD -> DOGE-USD
|
||||||
*/
|
*/
|
||||||
public convertToYahooFinanceSymbol(aSymbol: string) {
|
public convertToYahooFinanceSymbol(aSymbol: string) {
|
||||||
if (aSymbol.includes(this.baseCurrency) && aSymbol.length >= 6) {
|
if (
|
||||||
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
|
aSymbol.includes(this.baseCurrency) &&
|
||||||
|
aSymbol.length > this.baseCurrency.length
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
isCurrency(
|
||||||
|
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return `${aSymbol}=X`;
|
return `${aSymbol}=X`;
|
||||||
} else if (
|
} else if (
|
||||||
this.cryptocurrencyService.isCryptocurrency(
|
this.cryptocurrencyService.isCryptocurrency(
|
||||||
@ -90,7 +98,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||||
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
||||||
modules: ['price', 'summaryProfile']
|
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||||
});
|
});
|
||||||
|
|
||||||
const { assetClass, assetSubClass } = this.parseAssetClass(
|
const { assetClass, assetSubClass } = this.parseAssetClass(
|
||||||
@ -109,7 +117,16 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
});
|
});
|
||||||
response.symbol = aSymbol;
|
response.symbol = aSymbol;
|
||||||
|
|
||||||
if (
|
if (assetSubClass === AssetSubClass.MUTUALFUND) {
|
||||||
|
response.sectors = [];
|
||||||
|
|
||||||
|
for (const sectorWeighting of assetProfile.topHoldings
|
||||||
|
?.sectorWeightings ?? []) {
|
||||||
|
for (const [sector, weight] of Object.entries(sectorWeighting)) {
|
||||||
|
response.sectors.push({ weight, name: this.parseSector(sector) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
assetSubClass === AssetSubClass.STOCK &&
|
assetSubClass === AssetSubClass.STOCK &&
|
||||||
assetProfile.summaryProfile?.country
|
assetProfile.summaryProfile?.country
|
||||||
) {
|
) {
|
||||||
@ -183,10 +200,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
for (const historicalItem of historicalResult) {
|
for (const historicalItem of historicalResult) {
|
||||||
let marketPrice = historicalItem.close;
|
let marketPrice = historicalItem.close;
|
||||||
|
|
||||||
if (symbol === 'USDGBp') {
|
if (symbol === `${this.baseCurrency}GBp`) {
|
||||||
// Convert GPB to GBp (pence)
|
// Convert GPB to GBp (pence)
|
||||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||||
} else if (symbol === 'USDILA') {
|
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||||
// Convert ILS to ILA
|
// Convert ILS to ILA
|
||||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||||
}
|
}
|
||||||
@ -246,9 +263,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
marketPrice: quote.regularMarketPrice || 0
|
marketPrice: quote.regularMarketPrice || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
if (symbol === 'USDGBP' && yahooFinanceSymbols.includes('USDGBp=X')) {
|
if (
|
||||||
|
symbol === `${this.baseCurrency}GBP` &&
|
||||||
|
yahooFinanceSymbols.includes(`${this.baseCurrency}GBp=X`)
|
||||||
|
) {
|
||||||
// Convert GPB to GBp (pence)
|
// Convert GPB to GBp (pence)
|
||||||
response['USDGBp'] = {
|
response[`${this.baseCurrency}GBp`] = {
|
||||||
...response[symbol],
|
...response[symbol],
|
||||||
currency: 'GBp',
|
currency: 'GBp',
|
||||||
marketPrice: new Big(response[symbol].marketPrice)
|
marketPrice: new Big(response[symbol].marketPrice)
|
||||||
@ -256,11 +276,11 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
.toNumber()
|
.toNumber()
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
symbol === 'USDILS' &&
|
symbol === `${this.baseCurrency}ILS` &&
|
||||||
yahooFinanceSymbols.includes('USDILA=X')
|
yahooFinanceSymbols.includes(`${this.baseCurrency}ILA=X`)
|
||||||
) {
|
) {
|
||||||
// Convert ILS to ILA
|
// Convert ILS to ILA
|
||||||
response['USDILA'] = {
|
response[`${this.baseCurrency}ILA`] = {
|
||||||
...response[symbol],
|
...response[symbol],
|
||||||
currency: 'ILA',
|
currency: 'ILA',
|
||||||
marketPrice: new Big(response[symbol].marketPrice)
|
marketPrice: new Big(response[symbol].marketPrice)
|
||||||
@ -270,9 +290,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yahooFinanceSymbols.includes('USDUSX=X')) {
|
if (yahooFinanceSymbols.includes(`${this.baseCurrency}USX=X`)) {
|
||||||
// Convert USD to USX (cent)
|
// Convert USD to USX (cent)
|
||||||
response['USDUSX'] = {
|
response[`${this.baseCurrency}USX`] = {
|
||||||
currency: 'USX',
|
currency: 'USX',
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
marketPrice: new Big(1).mul(100).toNumber(),
|
marketPrice: new Big(1).mul(100).toNumber(),
|
||||||
@ -434,4 +454,46 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
return { assetClass, assetSubClass };
|
return { assetClass, assetSubClass };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseSector(aString: string): string {
|
||||||
|
let sector = UNKNOWN_KEY;
|
||||||
|
|
||||||
|
switch (aString) {
|
||||||
|
case 'basic_materials':
|
||||||
|
sector = 'Basic Materials';
|
||||||
|
break;
|
||||||
|
case 'communication_services':
|
||||||
|
sector = 'Communication Services';
|
||||||
|
break;
|
||||||
|
case 'consumer_cyclical':
|
||||||
|
sector = 'Consumer Cyclical';
|
||||||
|
break;
|
||||||
|
case 'consumer_defensive':
|
||||||
|
sector = 'Consumer Staples';
|
||||||
|
break;
|
||||||
|
case 'energy':
|
||||||
|
sector = 'Energy';
|
||||||
|
break;
|
||||||
|
case 'financial_services':
|
||||||
|
sector = 'Financial Services';
|
||||||
|
break;
|
||||||
|
case 'healthcare':
|
||||||
|
sector = 'Healthcare';
|
||||||
|
break;
|
||||||
|
case 'industrials':
|
||||||
|
sector = 'Industrials';
|
||||||
|
break;
|
||||||
|
case 'realestate':
|
||||||
|
sector = 'Real Estate';
|
||||||
|
break;
|
||||||
|
case 'technology':
|
||||||
|
sector = 'Technology';
|
||||||
|
break;
|
||||||
|
case 'utilities':
|
||||||
|
sector = 'Utilities';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sector;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,10 +99,12 @@ export class ExchangeRateDataService {
|
|||||||
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
||||||
|
|
||||||
if (!this.exchangeRates[symbol]) {
|
if (!this.exchangeRates[symbol]) {
|
||||||
// Not found, calculate indirectly via USD
|
// Not found, calculate indirectly via base currency
|
||||||
this.exchangeRates[symbol] =
|
this.exchangeRates[symbol] =
|
||||||
resultExtended[`${currency1}${'USD'}`]?.[date]?.marketPrice *
|
resultExtended[`${currency1}${this.baseCurrency}`]?.[date]
|
||||||
resultExtended[`${'USD'}${currency2}`]?.[date]?.marketPrice;
|
?.marketPrice *
|
||||||
|
resultExtended[`${this.baseCurrency}${currency2}`]?.[date]
|
||||||
|
?.marketPrice;
|
||||||
|
|
||||||
// Calculate the opposite direction
|
// Calculate the opposite direction
|
||||||
this.exchangeRates[`${currency2}${currency1}`] =
|
this.exchangeRates[`${currency2}${currency1}`] =
|
||||||
@ -126,9 +128,11 @@ export class ExchangeRateDataService {
|
|||||||
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
||||||
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
||||||
} else {
|
} else {
|
||||||
// Calculate indirectly via USD
|
// Calculate indirectly via base currency
|
||||||
const factor1 = this.exchangeRates[`${aFromCurrency}${'USD'}`];
|
const factor1 =
|
||||||
const factor2 = this.exchangeRates[`${'USD'}${aToCurrency}`];
|
this.exchangeRates[`${aFromCurrency}${this.baseCurrency}`];
|
||||||
|
const factor2 =
|
||||||
|
this.exchangeRates[`${this.baseCurrency}${aToCurrency}`];
|
||||||
|
|
||||||
factor = factor1 * factor2;
|
factor = factor1 * factor2;
|
||||||
|
|
||||||
@ -166,21 +170,6 @@ export class ExchangeRateDataService {
|
|||||||
currencies.push(account.currency);
|
currencies.push(account.currency);
|
||||||
});
|
});
|
||||||
|
|
||||||
(
|
|
||||||
await this.prismaService.settings.findMany({
|
|
||||||
distinct: ['currency'],
|
|
||||||
orderBy: [{ currency: 'asc' }],
|
|
||||||
select: { currency: true },
|
|
||||||
where: {
|
|
||||||
currency: {
|
|
||||||
not: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).forEach((userSettings) => {
|
|
||||||
currencies.push(userSettings.currency);
|
|
||||||
});
|
|
||||||
|
|
||||||
(
|
(
|
||||||
await this.prismaService.symbolProfile.findMany({
|
await this.prismaService.symbolProfile.findMany({
|
||||||
distinct: ['currency'],
|
distinct: ['currency'],
|
||||||
|
@ -26,7 +26,7 @@ export interface Environment extends CleanedEnvAccessors {
|
|||||||
MAX_ACTIVITIES_TO_IMPORT: number;
|
MAX_ACTIVITIES_TO_IMPORT: number;
|
||||||
MAX_ITEM_IN_CACHE: number;
|
MAX_ITEM_IN_CACHE: number;
|
||||||
PORT: number;
|
PORT: number;
|
||||||
RAKUTEN_RAPID_API_KEY: string;
|
RAPID_API_API_KEY: string;
|
||||||
REDIS_HOST: string;
|
REDIS_HOST: string;
|
||||||
REDIS_PASSWORD: string;
|
REDIS_PASSWORD: string;
|
||||||
REDIS_PORT: number;
|
REDIS_PORT: number;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class PropertyDto {
|
export class PropertyDto {
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,23 @@ export class SymbolProfileService {
|
|||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSymbolProfilesByIds(
|
||||||
|
symbolProfileIds: string[]
|
||||||
|
): Promise<EnhancedSymbolProfile[]> {
|
||||||
|
return this.prismaService.symbolProfile
|
||||||
|
.findMany({
|
||||||
|
include: { SymbolProfileOverrides: true },
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: symbolProfileIds.map((symbolProfileId) => {
|
||||||
|
return symbolProfileId;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
|
@ -53,13 +53,15 @@ export class TwitterBotService {
|
|||||||
symbolItem.marketPrice
|
symbolItem.marketPrice
|
||||||
);
|
);
|
||||||
|
|
||||||
let status = `Current Market Mood: ${emoji} ${text} (${symbolItem.marketPrice}/100)`;
|
let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${
|
||||||
|
symbolItem.marketPrice
|
||||||
|
}/100)`;
|
||||||
|
|
||||||
const benchmarkListing = await this.getBenchmarkListing(3);
|
const benchmarkListing = await this.getBenchmarkListing(3);
|
||||||
|
|
||||||
if (benchmarkListing?.length > 1) {
|
if (benchmarkListing?.length > 1) {
|
||||||
status += '\n\n';
|
status += '\n\n';
|
||||||
status += '±% from ATH\n';
|
status += '± from ATH in %\n';
|
||||||
status += benchmarkListing;
|
status += benchmarkListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
apps/client-e2e/project.json
Normal file
22
apps/client-e2e/project.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/client-e2e/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"targets": {
|
||||||
|
"e2e": {
|
||||||
|
"executor": "@nrwl/cypress:cypress",
|
||||||
|
"options": {
|
||||||
|
"cypressConfig": "apps/client-e2e/cypress.json",
|
||||||
|
"tsConfig": "apps/client-e2e/tsconfig.e2e.json",
|
||||||
|
"devServerTarget": "client:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "client:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [],
|
||||||
|
"implicitDependencies": ["client"]
|
||||||
|
}
|
201
apps/client/project.json
Normal file
201
apps/client/project.json
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"generators": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceRoot": "apps/client/src",
|
||||||
|
"prefix": "gf",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/client",
|
||||||
|
"index": "apps/client/src/index.html",
|
||||||
|
"main": "apps/client/src/main.ts",
|
||||||
|
"polyfills": "apps/client/src/polyfills.ts",
|
||||||
|
"tsConfig": "apps/client/tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "assetlinks.json",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../.well-known"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "CHANGELOG.md",
|
||||||
|
"input": "",
|
||||||
|
"output": "./../assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "LICENSE",
|
||||||
|
"input": "",
|
||||||
|
"output": "./../assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "robots.txt",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "sitemap.xml",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "node_modules/ionicons/dist/ionicons",
|
||||||
|
"output": "./../ionicons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*.js",
|
||||||
|
"input": "node_modules/ionicons/dist/",
|
||||||
|
"output": "./../"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../assets/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": ["apps/client/src/styles.scss"],
|
||||||
|
"scripts": ["node_modules/marked/marked.min.js"],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development-de": {
|
||||||
|
"baseHref": "/de/",
|
||||||
|
"localize": ["de"]
|
||||||
|
},
|
||||||
|
"development-en": {
|
||||||
|
"baseHref": "/en/",
|
||||||
|
"localize": ["en"]
|
||||||
|
},
|
||||||
|
"development-es": {
|
||||||
|
"baseHref": "/es/",
|
||||||
|
"localize": ["es"]
|
||||||
|
},
|
||||||
|
"development-it": {
|
||||||
|
"baseHref": "/it/",
|
||||||
|
"localize": ["it"]
|
||||||
|
},
|
||||||
|
"development-nl": {
|
||||||
|
"baseHref": "/nl/",
|
||||||
|
"localize": ["nl"]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "apps/client/src/environments/environment.ts",
|
||||||
|
"with": "apps/client/src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"defaultConfiguration": ""
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "client:build",
|
||||||
|
"proxyConfig": "apps/client/proxy.conf.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development-de": {
|
||||||
|
"browserTarget": "client:build:development-de"
|
||||||
|
},
|
||||||
|
"development-en": {
|
||||||
|
"browserTarget": "client:build:development-en"
|
||||||
|
},
|
||||||
|
"development-es": {
|
||||||
|
"browserTarget": "client:build:development-es"
|
||||||
|
},
|
||||||
|
"development-it": {
|
||||||
|
"browserTarget": "client:build:development-it"
|
||||||
|
},
|
||||||
|
"development-nl": {
|
||||||
|
"browserTarget": "client:build:development-nl"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "client:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"executor": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "client:build",
|
||||||
|
"includeContext": true,
|
||||||
|
"outputPath": "src/locales",
|
||||||
|
"targetFiles": [
|
||||||
|
"messages.de.xlf",
|
||||||
|
"messages.es.xlf",
|
||||||
|
"messages.it.xlf",
|
||||||
|
"messages.nl.xlf"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["apps/client/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "apps/client/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
},
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/apps/client"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"locales": {
|
||||||
|
"de": {
|
||||||
|
"baseHref": "/de/",
|
||||||
|
"translation": "apps/client/src/locales/messages.de.xlf"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"baseHref": "/es/",
|
||||||
|
"translation": "apps/client/src/locales/messages.es.xlf"
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"baseHref": "/it/",
|
||||||
|
"translation": "apps/client/src/locales/messages.it.xlf"
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"baseHref": "/nl/",
|
||||||
|
"translation": "apps/client/src/locales/messages.nl.xlf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceLocale": "en"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -95,6 +95,13 @@ const routes: Routes = [
|
|||||||
'./pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module'
|
'./pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module'
|
||||||
).then((m) => m.FiveHundredStarsOnGitHubPageModule)
|
).then((m) => m.FiveHundredStarsOnGitHubPageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'blog/2022/10/hacktoberfest-2022',
|
||||||
|
loadChildren: () =>
|
||||||
|
import(
|
||||||
|
'./pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.module'
|
||||||
|
).then((m) => m.Hacktoberfest2022PageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'demo',
|
path: 'demo',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { ColorScheme } from '@ghostfolio/common/types';
|
||||||
import { MaterialCssVarsService } from 'angular-material-css-vars';
|
import { MaterialCssVarsService } from 'angular-material-css-vars';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -77,6 +78,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
permissions.createUserAccount
|
permissions.createUserAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.initializeTheme(this.user?.settings.colorScheme);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -97,13 +100,17 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeTheme() {
|
private initializeTheme(userPreferredColorScheme?: ColorScheme) {
|
||||||
this.materialCssVarsService.setDarkTheme(
|
const isDarkTheme = userPreferredColorScheme
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
? userPreferredColorScheme === 'DARK'
|
||||||
);
|
: window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
this.materialCssVarsService.setDarkTheme(isDarkTheme);
|
||||||
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => {
|
window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => {
|
||||||
|
if (!this.user?.settings.colorScheme) {
|
||||||
this.materialCssVarsService.setDarkTheme(event.matches);
|
this.materialCssVarsService.setDarkTheme(event.matches);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.materialCssVarsService.setPrimaryColor(primaryColorHex);
|
this.materialCssVarsService.setPrimaryColor(primaryColorHex);
|
||||||
|
@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
||||||
this.accountType = accountType;
|
this.accountType = accountType;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.platformName = Platform?.name;
|
this.platformName = Platform?.name ?? '-';
|
||||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
@ -21,10 +21,12 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value size="medium" [value]="accountType">Account Type</gf-value>
|
<gf-value i18n size="medium" [value]="accountType"
|
||||||
|
>Account Type</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value size="medium" [value]="platformName">Platform</gf-value>
|
<gf-value i18n size="medium" [value]="platformName">Platform</gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
||||||
<mat-form-field appearance="outline" class="flex-grow-1">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline flex-grow-1 mr-2 without-hint"
|
||||||
|
>
|
||||||
<mat-select formControlName="status">
|
<mat-select formControlName="status">
|
||||||
<mat-option></mat-option>
|
<mat-option></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
@ -13,7 +16,7 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button
|
<button
|
||||||
class="ml-1"
|
class="mt-1"
|
||||||
color="warn"
|
color="warn"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="onDeleteJobs()"
|
(click)="onDeleteJobs()"
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[isAnimated]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[showXAxis]="true"
|
[showXAxis]="true"
|
||||||
[showYAxis]="true"
|
[showYAxis]="true"
|
||||||
|
@ -99,7 +99,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
...this.coupons,
|
...this.coupons,
|
||||||
{ code: this.generateCouponCode(16), duration: this.couponDuration }
|
{ code: this.generateCouponCode(16), duration: this.couponDuration }
|
||||||
];
|
];
|
||||||
this.putCoupons(coupons);
|
this.putAdminSetting({ key: PROPERTY_COUPONS, value: coupons });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onAddCurrency() {
|
public onAddCurrency() {
|
||||||
@ -107,7 +107,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
if (currency) {
|
if (currency) {
|
||||||
const currencies = uniq([...this.customCurrencies, currency]);
|
const currencies = uniq([...this.customCurrencies, currency]);
|
||||||
this.putCurrencies(currencies);
|
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
const coupons = this.coupons.filter((coupon) => {
|
const coupons = this.coupons.filter((coupon) => {
|
||||||
return coupon.code !== aCouponCode;
|
return coupon.code !== aCouponCode;
|
||||||
});
|
});
|
||||||
this.putCoupons(coupons);
|
this.putAdminSetting({ key: PROPERTY_COUPONS, value: coupons });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,12 +137,12 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
const currencies = this.customCurrencies.filter((currency) => {
|
const currencies = this.customCurrencies.filter((currency) => {
|
||||||
return currency !== aCurrency;
|
return currency !== aCurrency;
|
||||||
});
|
});
|
||||||
this.putCurrencies(currencies);
|
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteSystemMessage() {
|
public onDeleteSystemMessage() {
|
||||||
this.putSystemMessage('');
|
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFlushCache() {
|
public onFlushCache() {
|
||||||
@ -192,14 +192,20 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onReadOnlyModeChange(aEvent: MatSlideToggleChange) {
|
public onReadOnlyModeChange(aEvent: MatSlideToggleChange) {
|
||||||
this.setReadOnlyMode(aEvent.checked);
|
this.putAdminSetting({
|
||||||
|
key: PROPERTY_IS_READ_ONLY_MODE,
|
||||||
|
value: aEvent.checked ? true : undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSetSystemMessage() {
|
public onSetSystemMessage() {
|
||||||
const systemMessage = prompt($localize`Please set your system message:`);
|
const systemMessage = prompt($localize`Please set your system message:`);
|
||||||
|
|
||||||
if (systemMessage) {
|
if (systemMessage) {
|
||||||
this.putSystemMessage(systemMessage);
|
this.putAdminSetting({
|
||||||
|
key: PROPERTY_SYSTEM_MESSAGE,
|
||||||
|
value: systemMessage
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,49 +242,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
return couponCode;
|
return couponCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private putCoupons(aCoupons: Coupon[]) {
|
private putAdminSetting({ key, value }: { key: string; value: any }) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putAdminSetting(PROPERTY_COUPONS, {
|
.putAdminSetting(key, {
|
||||||
value: JSON.stringify(aCoupons)
|
value: value ? JSON.stringify(value) : undefined
|
||||||
})
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private putCurrencies(aCurrencies: string[]) {
|
|
||||||
this.dataService
|
|
||||||
.putAdminSetting(PROPERTY_CURRENCIES, {
|
|
||||||
value: JSON.stringify(aCurrencies)
|
|
||||||
})
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private putSystemMessage(aSystemMessage: string) {
|
|
||||||
this.dataService
|
|
||||||
.putAdminSetting(PROPERTY_SYSTEM_MESSAGE, {
|
|
||||||
value: aSystemMessage
|
|
||||||
})
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private setReadOnlyMode(aValue: boolean) {
|
|
||||||
this.dataService
|
|
||||||
.putAdminSetting(PROPERTY_IS_READ_ONLY_MODE, {
|
|
||||||
value: aValue ? 'true' : ''
|
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
@ -5,15 +5,26 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>User Count</div>
|
<div class="w-50" i18n>User Count</div>
|
||||||
<div class="w-50">{{ userCount }}</div>
|
<div class="w-50">
|
||||||
|
<gf-value
|
||||||
|
precision="0"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[value]="userCount"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>Activity Count</div>
|
<div class="w-50" i18n>Activity Count</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<ng-container *ngIf="transactionCount">
|
<gf-value
|
||||||
{{ transactionCount }} ({{ transactionCount / userCount | number
|
precision="0"
|
||||||
: '1.2-2' }} <span i18n>per User</span>)
|
[locale]="user?.settings?.locale"
|
||||||
</ng-container>
|
[value]="transactionCount"
|
||||||
|
></gf-value>
|
||||||
|
<div *ngIf="transactionCount && userCount">
|
||||||
|
{{ transactionCount / userCount | number : '1.2-2' }}
|
||||||
|
<span i18n>per User</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
@ -108,10 +119,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="align-items-start d-flex my-3">
|
||||||
|
<div class="w-50" i18n>Benchmarks</div>
|
||||||
|
<div class="w-50">
|
||||||
|
<table>
|
||||||
|
<tr *ngFor="let benchmark of info?.benchmarks">
|
||||||
|
<td class="pl-1">{{ benchmark.symbol }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||||
<div class="w-50" i18n>System Message</div>
|
<div class="w-50" i18n>System Message</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<div *ngIf="info.systemMessage">
|
<div *ngIf="info?.systemMessage">
|
||||||
<span>{{ info.systemMessage }}</span>
|
<span>{{ info.systemMessage }}</span>
|
||||||
<button
|
<button
|
||||||
class="mini-icon mx-1 no-min-width px-2"
|
class="mini-icon mx-1 no-min-width px-2"
|
||||||
@ -122,7 +143,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
*ngIf="!info.systemMessage"
|
*ngIf="!info?.systemMessage"
|
||||||
color="accent"
|
color="accent"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="onSetSystemMessage()"
|
(click)="onSetSystemMessage()"
|
||||||
@ -162,8 +183,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<form #couponForm="ngForm">
|
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||||
<mat-form-field appearance="outline" class="mr-2">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline mr-2 without-hint"
|
||||||
|
>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="duration"
|
name="duration"
|
||||||
[value]="couponDuration"
|
[value]="couponDuration"
|
||||||
@ -176,6 +200,7 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button
|
<button
|
||||||
|
class="mt-1"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="onAddCoupon()"
|
(click)="onAddCoupon()"
|
||||||
|
@ -43,23 +43,23 @@
|
|||||||
<td class="mat-cell px-1 py-2 text-right">
|
<td class="mat-cell px-1 py-2 text-right">
|
||||||
{{ formatDistanceToNow(userItem.createdAt) }}
|
{{ formatDistanceToNow(userItem.createdAt) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2">
|
<td class="mat-cell px-1 py-2 text-right">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="align-items-end"
|
class="d-inline-block justify-content-end"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[value]="userItem.accountCount"
|
[value]="userItem.accountCount"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2">
|
<td class="mat-cell px-1 py-2 text-right">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="align-items-end"
|
class="d-inline-block justify-content-end"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[value]="userItem.transactionCount"
|
[value]="userItem.transactionCount"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2">
|
<td class="mat-cell px-1 py-2 text-right">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="align-items-end"
|
class="d-inline-block justify-content-end"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[precision]="0"
|
[precision]="0"
|
||||||
[value]="userItem.engagement"
|
[value]="userItem.engagement"
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
<div class="row">
|
<div class="mb-2 row">
|
||||||
<div class="col-md-6 col-xs-12 d-flex">
|
<div class="col-md-6 col-xs-12 d-flex">
|
||||||
<div class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate">
|
<div class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate">
|
||||||
<span i18n>Benchmarks</span>
|
<span i18n>Performance</span>
|
||||||
<sup i18n>Beta</sup>
|
|
||||||
<gf-premium-indicator
|
<gf-premium-indicator
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
@ -10,31 +9,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
|
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
|
||||||
<mat-form-field appearance="outline" class="w-100" color="accent">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="w-100 without-hint"
|
||||||
|
color="accent"
|
||||||
|
[hidden]="benchmarks?.length === 0"
|
||||||
|
>
|
||||||
<mat-label i18n>Compare with...</mat-label>
|
<mat-label i18n>Compare with...</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="benchmark"
|
name="benchmark"
|
||||||
[compareWith]="compareUniqueAssets"
|
|
||||||
[value]="benchmark"
|
[value]="benchmark"
|
||||||
(selectionChange)="onChangeBenchmark($event.value)"
|
(selectionChange)="onChangeBenchmark($event.value)"
|
||||||
>
|
>
|
||||||
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let currentBenchmark of benchmarks"
|
*ngFor="let symbolProfile of benchmarks"
|
||||||
[value]="currentBenchmark"
|
[value]="symbolProfile.id"
|
||||||
>{{ currentBenchmark.symbol }}</mat-option
|
>{{ symbolProfile.name }}</mat-option
|
||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
|
|
||||||
<gf-toggle
|
|
||||||
[defaultValue]="user?.settings?.dateRange"
|
|
||||||
[isLoading]="isLoading"
|
|
||||||
[options]="dateRangeOptions"
|
|
||||||
(change)="onChangeDateRange($event.value)"
|
|
||||||
></gf-toggle>
|
|
||||||
</div>
|
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
*ngIf="isLoading"
|
*ngIf="isLoading"
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
|
||||||
import {
|
import {
|
||||||
getTooltipOptions,
|
getTooltipOptions,
|
||||||
getTooltipPositionerMapTop,
|
getTooltipPositionerMapTop,
|
||||||
@ -23,12 +22,9 @@ import {
|
|||||||
getTextColor,
|
getTextColor,
|
||||||
parseDate
|
parseDate
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
import {
|
import { LineChartItem, User } from '@ghostfolio/common/interfaces';
|
||||||
LineChartItem,
|
import { ColorScheme } from '@ghostfolio/common/types';
|
||||||
UniqueAsset,
|
import { SymbolProfile } from '@prisma/client';
|
||||||
User
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
LineController,
|
LineController,
|
||||||
@ -48,21 +44,20 @@ import annotationPlugin from 'chartjs-plugin-annotation';
|
|||||||
})
|
})
|
||||||
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||||
@Input() benchmarkDataItems: LineChartItem[] = [];
|
@Input() benchmarkDataItems: LineChartItem[] = [];
|
||||||
@Input() benchmark: UniqueAsset;
|
@Input() benchmark: string;
|
||||||
@Input() benchmarks: UniqueAsset[];
|
@Input() benchmarks: Partial<SymbolProfile>[];
|
||||||
|
@Input() colorScheme: ColorScheme;
|
||||||
@Input() daysInMarket: number;
|
@Input() daysInMarket: number;
|
||||||
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performanceDataItems: LineChartItem[];
|
@Input() performanceDataItems: LineChartItem[];
|
||||||
@Input() user: User;
|
@Input() user: User;
|
||||||
|
|
||||||
@Output() benchmarkChanged = new EventEmitter<UniqueAsset>();
|
@Output() benchmarkChanged = new EventEmitter<string>();
|
||||||
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
|
||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas;
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
|
|
||||||
public chart: Chart<any>;
|
public chart: Chart<any>;
|
||||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
|
||||||
public isLoading = true;
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
Chart.register(
|
Chart.register(
|
||||||
@ -85,22 +80,8 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public compareUniqueAssets(
|
public onChangeBenchmark(symbolProfileId: string) {
|
||||||
uniqueAsset1: UniqueAsset,
|
this.benchmarkChanged.next(symbolProfileId);
|
||||||
uniqueAsset2: UniqueAsset
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
uniqueAsset1?.dataSource === uniqueAsset2?.dataSource &&
|
|
||||||
uniqueAsset1?.symbol === uniqueAsset2?.symbol
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeBenchmark(benchmark: UniqueAsset) {
|
|
||||||
this.benchmarkChanged.next(benchmark);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeDateRange(dateRange: DateRange) {
|
|
||||||
this.dateRangeChanged.next(dateRange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
@ -108,8 +89,6 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
@ -150,7 +129,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
tension: 0
|
tension: 0
|
||||||
},
|
},
|
||||||
point: {
|
point: {
|
||||||
hoverBackgroundColor: getBackgroundColor(),
|
hoverBackgroundColor: getBackgroundColor(this.colorScheme),
|
||||||
hoverRadius: 2,
|
hoverRadius: 2,
|
||||||
radius: 0
|
radius: 0
|
||||||
}
|
}
|
||||||
@ -161,7 +140,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
annotation: {
|
annotation: {
|
||||||
annotations: {
|
annotations: {
|
||||||
yAxis: {
|
yAxis: {
|
||||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
scaleID: 'y',
|
scaleID: 'y',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@ -174,7 +153,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
},
|
},
|
||||||
tooltip: this.getTooltipPluginConfiguration(),
|
tooltip: this.getTooltipPluginConfiguration(),
|
||||||
verticalHoverLine: {
|
verticalHoverLine: {
|
||||||
color: `rgba(${getTextColor()}, 0.1)`
|
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
responsive: true,
|
responsive: true,
|
||||||
@ -182,9 +161,9 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
x: {
|
x: {
|
||||||
display: true,
|
display: true,
|
||||||
grid: {
|
grid: {
|
||||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
color: `rgba(${getTextColor()}, 0.8)`,
|
color: `rgba(${getTextColor(this.colorScheme)}, 0.8)`,
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
type: 'time',
|
type: 'time',
|
||||||
@ -196,8 +175,8 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
y: {
|
y: {
|
||||||
display: true,
|
display: true,
|
||||||
grid: {
|
grid: {
|
||||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`,
|
||||||
color: `rgba(${getTextColor()}, 0.8)`,
|
color: `rgba(${getTextColor(this.colorScheme)}, 0.8)`,
|
||||||
display: false,
|
display: false,
|
||||||
drawBorder: false
|
drawBorder: false
|
||||||
},
|
},
|
||||||
@ -213,22 +192,23 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
|
plugins: [
|
||||||
|
getVerticalHoverLinePlugin(this.chartCanvas, this.colorScheme)
|
||||||
|
],
|
||||||
type: 'line'
|
type: 'line'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTooltipPluginConfiguration() {
|
private getTooltipPluginConfiguration() {
|
||||||
return {
|
return {
|
||||||
...getTooltipOptions({
|
...getTooltipOptions({
|
||||||
|
colorScheme: this.colorScheme,
|
||||||
locale: this.locale,
|
locale: this.locale,
|
||||||
unit: '%'
|
unit: '%'
|
||||||
}),
|
}),
|
||||||
mode: 'index',
|
mode: 'x',
|
||||||
position: <unknown>'top',
|
position: <unknown>'top',
|
||||||
xAlign: 'center',
|
xAlign: 'center',
|
||||||
yAlign: 'bottom'
|
yAlign: 'bottom'
|
||||||
|
@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
||||||
@ -13,7 +12,6 @@ import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfToggleModule,
|
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
|
@ -127,6 +127,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
colorScheme: this.user?.settings?.colorScheme,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
hasPermissionToReportDataGlitch: hasPermission(
|
||||||
|
@ -24,7 +24,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
public fearLabel = $localize`Fear`;
|
public fearLabel = $localize`Fear`;
|
||||||
public greedLabel = $localize`Greed`;
|
public greedLabel = $localize`Greed`;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public historicalData: HistoricalDataItem[];
|
public historicalDataItems: HistoricalDataItem[];
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public readonly numberOfDays = 180;
|
public readonly numberOfDays = 180;
|
||||||
@ -67,7 +67,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ historicalData, marketPrice }) => {
|
.subscribe(({ historicalData, marketPrice }) => {
|
||||||
this.fearAndGreedIndex = marketPrice;
|
this.fearAndGreedIndex = marketPrice;
|
||||||
this.historicalData = [
|
this.historicalDataItems = [
|
||||||
...historicalData,
|
...historicalData,
|
||||||
{
|
{
|
||||||
date: resetHours(new Date()).toISOString(),
|
date: resetHours(new Date()).toISOString(),
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
symbol="Fear & Greed Index"
|
symbol="Fear & Greed Index"
|
||||||
yMax="100"
|
yMax="100"
|
||||||
yMin="0"
|
yMin="0"
|
||||||
[historicalDataItems]="historicalData"
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[isAnimated]="true"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showXAxis]="true"
|
[showXAxis]="true"
|
||||||
[showYAxis]="true"
|
[showYAxis]="true"
|
||||||
|
@ -76,8 +76,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
!this.hasImpersonationId &&
|
!this.hasImpersonationId &&
|
||||||
!this.user.settings.isRestrictedView &&
|
!this.user.settings.isRestrictedView &&
|
||||||
this.user.settings.viewMode !== 'ZEN';
|
this.user.settings.viewMode !== 'ZEN';
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeDateRange(dateRange: DateRange) {
|
public onChangeDateRange(dateRange: DateRange) {
|
||||||
@ -104,36 +102,29 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
|
this.historicalDataItems = null;
|
||||||
this.isLoadingPerformance = true;
|
this.isLoadingPerformance = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({
|
.fetchPortfolioPerformance({
|
||||||
range: this.user?.settings?.dateRange,
|
range: this.user?.settings?.dateRange
|
||||||
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
|
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((chartData) => {
|
|
||||||
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
|
||||||
return {
|
|
||||||
date: chartDataItem.date,
|
|
||||||
value: chartDataItem.value
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.isAllTimeHigh = chartData.isAllTimeHigh;
|
|
||||||
this.isAllTimeLow = chartData.isAllTimeLow;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchPortfolioPerformance({ range: this.user?.settings?.dateRange })
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.errors = response.errors;
|
this.errors = response.errors;
|
||||||
this.hasError = response.hasErrors;
|
this.hasError = response.hasErrors;
|
||||||
this.performance = response.performance;
|
this.performance = response.performance;
|
||||||
this.isLoadingPerformance = false;
|
this.isLoadingPerformance = false;
|
||||||
|
|
||||||
|
this.historicalDataItems = response.chart.map(
|
||||||
|
({ date, netPerformanceInPercentage }) => {
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
value: netPerformanceInPercentage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,16 +15,17 @@
|
|||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="position-absolute"
|
class="position-absolute"
|
||||||
symbol="Performance"
|
symbol="Performance"
|
||||||
[currency]="user?.settings?.isExperimentalFeatures ? undefined : user?.settings?.baseCurrency"
|
unit="%"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[hidden]="historicalDataItems?.length === 0"
|
[hidden]="historicalDataItems?.length === 0"
|
||||||
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||||
[showGradient]="true"
|
[showGradient]="true"
|
||||||
[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>
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
MatSnackBar,
|
||||||
|
MatSnackBarRef,
|
||||||
|
TextOnlySnackBar
|
||||||
|
} from '@angular/material/snack-bar';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
InfoItem,
|
||||||
|
PortfolioSummary,
|
||||||
|
User
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public info: InfoItem;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||||
public summary: PortfolioSummary;
|
public summary: PortfolioSummary;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private router: Router,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
|
this.info = this.dataService.fetchInfo();
|
||||||
|
|
||||||
|
this.hasPermissionForSubscription = hasPermission(
|
||||||
|
this.info?.globalPermissions,
|
||||||
|
permissions.enableSubscription
|
||||||
|
);
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeEmergencyFund(emergencyFund: number) {
|
public onChangeEmergencyFund(emergencyFund: number) {
|
||||||
@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioSummary()
|
.fetchPortfolioDetails({})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe(({ summary }) => {
|
||||||
this.summary = response;
|
this.summary = summary;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
if (!this.summary) {
|
||||||
|
this.snackBarRef = this.snackBar.open(
|
||||||
|
$localize`This feature requires a subscription.`,
|
||||||
|
this.hasPermissionForSubscription
|
||||||
|
? $localize`Upgrade Plan`
|
||||||
|
: undefined,
|
||||||
|
{ duration: 6000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.snackBarRef.afterDismissed().subscribe(() => {
|
||||||
|
this.snackBarRef = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.snackBarRef.onAction().subscribe(() => {
|
||||||
|
this.router.navigate(['/pricing']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,14 +15,16 @@ import {
|
|||||||
} from '@ghostfolio/common/chart-helper';
|
} from '@ghostfolio/common/chart-helper';
|
||||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
getBackgroundColor,
|
getBackgroundColor,
|
||||||
getDateFormatString,
|
getDateFormatString,
|
||||||
getTextColor,
|
getTextColor,
|
||||||
parseDate,
|
parseDate,
|
||||||
transformTickToAbbreviation
|
transformTickToAbbreviation
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
|
import { LineChartItem } from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { GroupBy } from '@ghostfolio/common/types';
|
import { ColorScheme, DateRange, GroupBy } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
BarController,
|
BarController,
|
||||||
BarElement,
|
BarElement,
|
||||||
@ -35,7 +37,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
import { addDays, format, isAfter, parseISO, subDays } from 'date-fns';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-investment-chart',
|
selector: 'gf-investment-chart',
|
||||||
@ -44,19 +46,21 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
|||||||
styleUrls: ['./investment-chart.component.scss']
|
styleUrls: ['./investment-chart.component.scss']
|
||||||
})
|
})
|
||||||
export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||||
|
@Input() benchmarkDataItems: InvestmentItem[] = [];
|
||||||
|
@Input() colorScheme: ColorScheme;
|
||||||
@Input() currency: string;
|
@Input() currency: string;
|
||||||
@Input() daysInMarket: number;
|
@Input() daysInMarket: number;
|
||||||
@Input() groupBy: GroupBy;
|
@Input() groupBy: GroupBy;
|
||||||
@Input() investments: InvestmentItem[];
|
@Input() historicalDataItems: LineChartItem[] = [];
|
||||||
@Input() isInPercent = false;
|
@Input() isInPercent = false;
|
||||||
|
@Input() isLoading = false;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
|
@Input() range: DateRange = 'max';
|
||||||
@Input() savingsRate = 0;
|
@Input() savingsRate = 0;
|
||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas;
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
|
|
||||||
public chart: Chart;
|
public chart: Chart<any>;
|
||||||
public isLoading = true;
|
|
||||||
|
|
||||||
private data: InvestmentItem[];
|
private data: InvestmentItem[];
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
@ -77,7 +81,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
if (this.investments) {
|
if (this.benchmarkDataItems && this.historicalDataItems) {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,58 +91,72 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
// Create a clone
|
// Create a clone
|
||||||
this.data = this.investments.map((a) => Object.assign({}, a));
|
this.data = this.benchmarkDataItems.map((item) => Object.assign({}, item));
|
||||||
|
|
||||||
if (!this.groupBy && this.data?.length > 0) {
|
if (!this.groupBy && this.data?.length > 0) {
|
||||||
|
if (this.range === 'max') {
|
||||||
// Extend chart by 5% of days in market (before)
|
// Extend chart by 5% of days in market (before)
|
||||||
const firstItem = this.data[0];
|
const firstItem = this.data[0];
|
||||||
this.data.unshift({
|
this.data.unshift({
|
||||||
...firstItem,
|
...firstItem,
|
||||||
date: subDays(
|
date: format(
|
||||||
parseISO(firstItem.date),
|
subDays(parseISO(firstItem.date), this.daysInMarket * 0.05 || 90),
|
||||||
this.daysInMarket * 0.05 || 90
|
DATE_FORMAT
|
||||||
).toISOString(),
|
),
|
||||||
investment: 0
|
investment: 0
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Extend chart by 5% of days in market (after)
|
// Extend chart by 5% of days in market (after)
|
||||||
const lastItem = this.data[this.data.length - 1];
|
const lastItem = this.data[this.data.length - 1];
|
||||||
this.data.push({
|
this.data.push({
|
||||||
...lastItem,
|
...lastItem,
|
||||||
date: addDays(
|
date: format(
|
||||||
parseDate(lastItem.date),
|
addDays(parseDate(lastItem.date), this.daysInMarket * 0.05 || 90),
|
||||||
this.daysInMarket * 0.05 || 90
|
DATE_FORMAT
|
||||||
).toISOString()
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: this.data.map((investmentItem) => {
|
labels: this.historicalDataItems.map(({ date }) => {
|
||||||
return investmentItem.date;
|
return parseDate(date);
|
||||||
}),
|
}),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||||
borderWidth: this.groupBy ? 0 : 2,
|
borderWidth: this.groupBy ? 0 : 1,
|
||||||
data: this.data.map((position) => {
|
data: this.data.map(({ date, investment }) => {
|
||||||
return this.isInPercent
|
return {
|
||||||
? position.investment * 100
|
x: parseDate(date),
|
||||||
: position.investment;
|
y: this.isInPercent ? investment * 100 : investment
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
label: $localize`Deposit`,
|
label: $localize`Deposit`,
|
||||||
segment: {
|
segment: {
|
||||||
borderColor: (context: unknown) =>
|
borderColor: (context: unknown) =>
|
||||||
this.isInFuture(
|
this.isInFuture(
|
||||||
context,
|
context,
|
||||||
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)`
|
`rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)`
|
||||||
),
|
),
|
||||||
borderDash: (context: unknown) => this.isInFuture(context, [2, 2])
|
borderDash: (context: unknown) => this.isInFuture(context, [2, 2])
|
||||||
},
|
},
|
||||||
stepped: true
|
stepped: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
|
borderWidth: 2,
|
||||||
|
data: this.historicalDataItems.map(({ date, value }) => {
|
||||||
|
return {
|
||||||
|
x: parseDate(date),
|
||||||
|
y: this.isInPercent ? value * 100 : value
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
fill: false,
|
||||||
|
label: $localize`Total Amount`,
|
||||||
|
pointRadius: 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -160,7 +178,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
tension: 0
|
tension: 0
|
||||||
},
|
},
|
||||||
point: {
|
point: {
|
||||||
hoverBackgroundColor: getBackgroundColor(),
|
hoverBackgroundColor: getBackgroundColor(this.colorScheme),
|
||||||
hoverRadius: 2,
|
hoverRadius: 2,
|
||||||
radius: 0
|
radius: 0
|
||||||
}
|
}
|
||||||
@ -172,13 +190,13 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
annotations: {
|
annotations: {
|
||||||
savingsRate: this.savingsRate
|
savingsRate: this.savingsRate
|
||||||
? {
|
? {
|
||||||
borderColor: `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.75)`,
|
borderColor: `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.75)`,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
label: {
|
label: {
|
||||||
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
color: 'white',
|
color: 'white',
|
||||||
content: 'Savings Rate',
|
content: $localize`Savings Rate`,
|
||||||
display: true,
|
display: true,
|
||||||
font: { size: '10px', weight: 'normal' },
|
font: { size: '10px', weight: 'normal' },
|
||||||
padding: {
|
padding: {
|
||||||
@ -193,7 +211,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
yAxis: {
|
yAxis: {
|
||||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
scaleID: 'y',
|
scaleID: 'y',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@ -206,7 +224,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
},
|
},
|
||||||
tooltip: this.getTooltipPluginConfiguration(),
|
tooltip: this.getTooltipPluginConfiguration(),
|
||||||
verticalHoverLine: {
|
verticalHoverLine: {
|
||||||
color: `rgba(${getTextColor()}, 0.1)`
|
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
responsive: true,
|
responsive: true,
|
||||||
@ -214,9 +232,9 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
x: {
|
x: {
|
||||||
display: true,
|
display: true,
|
||||||
grid: {
|
grid: {
|
||||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`,
|
||||||
borderWidth: this.groupBy ? 0 : 1,
|
borderWidth: this.groupBy ? 0 : 1,
|
||||||
color: `rgba(${getTextColor()}, 0.8)`,
|
color: `rgba(${getTextColor(this.colorScheme)}, 0.8)`,
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
type: 'time',
|
type: 'time',
|
||||||
@ -228,8 +246,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
y: {
|
y: {
|
||||||
display: !this.isInPercent,
|
display: !this.isInPercent,
|
||||||
grid: {
|
grid: {
|
||||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`,
|
||||||
color: `rgba(${getTextColor()}, 0.8)`,
|
color: `rgba(${getTextColor(this.colorScheme)}, 0.8)`,
|
||||||
display: false,
|
display: false,
|
||||||
drawBorder: false
|
drawBorder: false
|
||||||
},
|
},
|
||||||
@ -245,18 +263,19 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
|
plugins: [
|
||||||
|
getVerticalHoverLinePlugin(this.chartCanvas, this.colorScheme)
|
||||||
|
],
|
||||||
type: this.groupBy ? 'bar' : 'line'
|
type: this.groupBy ? 'bar' : 'line'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTooltipPluginConfiguration() {
|
private getTooltipPluginConfiguration() {
|
||||||
return {
|
return {
|
||||||
...getTooltipOptions({
|
...getTooltipOptions({
|
||||||
|
colorScheme: this.colorScheme,
|
||||||
currency: this.isInPercent ? undefined : this.currency,
|
currency: this.isInPercent ? undefined : this.currency,
|
||||||
locale: this.isInPercent ? undefined : this.locale,
|
locale: this.isInPercent ? undefined : this.locale,
|
||||||
unit: this.isInPercent ? '%' : undefined
|
unit: this.isInPercent ? '%' : undefined
|
||||||
|
@ -22,9 +22,6 @@
|
|||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<span *ngIf="summary?.totalSell || summary?.totalSell === 0" class="mr-1"
|
|
||||||
>-</span
|
|
||||||
>
|
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
[currency]="baseCurrency"
|
[currency]="baseCurrency"
|
||||||
@ -172,6 +169,17 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Excluded from Analysis</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import { ColorScheme } from '@ghostfolio/common/types';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface PositionDetailDialogParams {
|
export interface PositionDetailDialogParams {
|
||||||
baseCurrency: string;
|
baseCurrency: string;
|
||||||
|
colorScheme: ColorScheme;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
hasImpersonationId: boolean;
|
hasImpersonationId: boolean;
|
||||||
|
@ -20,11 +20,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="mb-4"
|
|
||||||
benchmarkLabel="Average Unit Price"
|
benchmarkLabel="Average Unit Price"
|
||||||
|
class="mb-4"
|
||||||
[benchmarkDataItems]="benchmarkDataItems"
|
[benchmarkDataItems]="benchmarkDataItems"
|
||||||
|
[colorScheme]="data.colorScheme"
|
||||||
[currency]="SymbolProfile?.currency"
|
[currency]="SymbolProfile?.currency"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[isAnimated]="true"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[showGradient]="true"
|
[showGradient]="true"
|
||||||
[showXAxis]="true"
|
[showXAxis]="true"
|
||||||
@ -187,6 +189,7 @@
|
|||||||
<div class="h5" i18n>Sectors</div>
|
<div class="h5" i18n>Sectors</div>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="data.colorScheme"
|
||||||
[isInPercent]="true"
|
[isInPercent]="true"
|
||||||
[keys]="['name']"
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -198,6 +201,7 @@
|
|||||||
<div class="h5" i18n>Countries</div>
|
<div class="h5" i18n>Countries</div>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="data.colorScheme"
|
||||||
[isInPercent]="true"
|
[isInPercent]="true"
|
||||||
[keys]="['name']"
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
@ -5,9 +5,9 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot
|
RouterStateSnapshot
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { ViewMode } from '@prisma/client';
|
|
||||||
import { EMPTY } from 'rxjs';
|
import { EMPTY } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private dataService: DataService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
@ -75,18 +76,27 @@ export class AuthGuard implements CanActivate {
|
|||||||
const userLanguage = user?.settings?.language;
|
const userLanguage = user?.settings?.language;
|
||||||
|
|
||||||
if (userLanguage && document.documentElement.lang !== userLanguage) {
|
if (userLanguage && document.documentElement.lang !== userLanguage) {
|
||||||
window.location.href = `../${userLanguage}`;
|
this.dataService
|
||||||
resolve(false);
|
.putUserSetting({ language: document.documentElement.lang })
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
state.url.startsWith('/home') &&
|
state.url.startsWith('/home') &&
|
||||||
user.settings.viewMode === ViewMode.ZEN
|
user.settings.viewMode === 'ZEN'
|
||||||
) {
|
) {
|
||||||
this.router.navigate(['/zen']);
|
this.router.navigate(['/zen']);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else if (state.url.startsWith('/start')) {
|
} else if (state.url.startsWith('/start')) {
|
||||||
if (user.settings.viewMode === ViewMode.ZEN) {
|
if (user.settings.viewMode === 'ZEN') {
|
||||||
this.router.navigate(['/zen']);
|
this.router.navigate(['/zen']);
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate(['/home']);
|
this.router.navigate(['/home']);
|
||||||
@ -96,7 +106,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
state.url.startsWith('/zen') &&
|
state.url.startsWith('/zen') &&
|
||||||
user.settings.viewMode === ViewMode.DEFAULT
|
user.settings.viewMode === 'DEFAULT'
|
||||||
) {
|
) {
|
||||||
this.router.navigate(['/home']);
|
this.router.navigate(['/home']);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
|
@ -108,7 +108,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 24 hours)"
|
subLabel="(Last 24 hours)"
|
||||||
[value]="statistics?.activeUsers1d ?? '-'"
|
[value]="statistics?.activeUsers1d ?? '-'"
|
||||||
@ -117,7 +116,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 30 days)"
|
subLabel="(Last 30 days)"
|
||||||
[value]="statistics?.newUsers30d ?? '-'"
|
[value]="statistics?.newUsers30d ?? '-'"
|
||||||
@ -126,7 +124,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 30 days)"
|
subLabel="(Last 30 days)"
|
||||||
[value]="statistics?.activeUsers30d ?? '-'"
|
[value]="statistics?.activeUsers30d ?? '-'"
|
||||||
@ -139,7 +136,6 @@
|
|||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.slackCommunityUsers ?? '-'"
|
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||||
>Users in Slack community</gf-value
|
>Users in Slack community</gf-value
|
||||||
@ -152,7 +148,6 @@
|
|||||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.gitHubContributors ?? '-'"
|
[value]="statistics?.gitHubContributors ?? '-'"
|
||||||
>Contributors on GitHub</gf-value
|
>Contributors on GitHub</gf-value
|
||||||
@ -165,7 +160,6 @@
|
|||||||
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.gitHubStargazers ?? '-'"
|
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||||
>Stars on GitHub</gf-value
|
>Stars on GitHub</gf-value
|
||||||
@ -183,7 +177,7 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-stroked-button
|
mat-flat-button
|
||||||
[routerLink]="['/faq']"
|
[routerLink]="['/faq']"
|
||||||
>FAQ</a
|
>FAQ</a
|
||||||
>
|
>
|
||||||
@ -195,7 +189,7 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-stroked-button
|
mat-flat-button
|
||||||
[routerLink]="['/about', 'changelog']"
|
[routerLink]="['/about', 'changelog']"
|
||||||
>Changelog & License</a
|
>Changelog & License</a
|
||||||
>
|
>
|
||||||
@ -204,7 +198,7 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-stroked-button
|
mat-flat-button
|
||||||
[routerLink]="['/about', 'privacy-policy']"
|
[routerLink]="['/about', 'privacy-policy']"
|
||||||
>Privacy Policy</a
|
>Privacy Policy</a
|
||||||
>
|
>
|
||||||
|
@ -42,6 +42,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
signInWithFingerprintElement: MatSlideToggle;
|
signInWithFingerprintElement: MatSlideToggle;
|
||||||
|
|
||||||
public accesses: Access[];
|
public accesses: Access[];
|
||||||
|
public appearancePlaceholder = $localize`Auto`;
|
||||||
public baseCurrency: string;
|
public baseCurrency: string;
|
||||||
public coupon: number;
|
public coupon: number;
|
||||||
public couponId: string;
|
public couponId: string;
|
||||||
@ -54,7 +55,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToUpdateViewMode: boolean;
|
public hasPermissionToUpdateViewMode: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
public language = document.documentElement.lang;
|
public language = document.documentElement.lang;
|
||||||
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
|
public locales = ['de', 'de-CH', 'en-GB', 'en-US', 'es', 'it', 'nl'];
|
||||||
public price: number;
|
public price: number;
|
||||||
public priceId: string;
|
public priceId: string;
|
||||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||||
|
@ -94,7 +94,10 @@
|
|||||||
<ng-container i18n>Base Currency</ng-container>
|
<ng-container i18n>Base Currency</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline w-100 without-hint"
|
||||||
|
>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="baseCurrency"
|
name="baseCurrency"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
@ -116,7 +119,10 @@
|
|||||||
<div class="hint-text text-muted" i18n>Beta</div>
|
<div class="hint-text text-muted" i18n>Beta</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline w-100 without-hint"
|
||||||
|
>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="language"
|
name="language"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
@ -126,6 +132,9 @@
|
|||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option value="de">Deutsch</mat-option>
|
<mat-option value="de">Deutsch</mat-option>
|
||||||
<mat-option value="en">English</mat-option>
|
<mat-option value="en">English</mat-option>
|
||||||
|
<mat-option value="es">Español</mat-option>
|
||||||
|
<mat-option value="it">Italiano</mat-option>
|
||||||
|
<mat-option value="nl">Nederlands</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -138,7 +147,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline w-100 without-hint"
|
||||||
|
>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="locale"
|
name="locale"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
@ -155,13 +167,16 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex mb-2">
|
||||||
<div class="align-items-center d-flex pr-1 pt-1 w-50">
|
<div class="align-items-center d-flex pr-1 pt-1 w-50">
|
||||||
<ng-container i18n>View Mode</ng-container>
|
<ng-container i18n>View Mode</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<div class="align-items-center d-flex overflow-hidden">
|
<div class="align-items-center d-flex overflow-hidden">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline w-100 without-hint"
|
||||||
|
>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="viewMode"
|
name="viewMode"
|
||||||
[disabled]="!hasPermissionToUpdateViewMode"
|
[disabled]="!hasPermissionToUpdateViewMode"
|
||||||
@ -175,6 +190,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="align-items-center d-flex pr-1 pt-1 w-50">
|
||||||
|
<ng-container i18n>Appearance</ng-container>
|
||||||
|
</div>
|
||||||
|
<div class="pl-1 w-50">
|
||||||
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="compact-with-outline w-100 without-hint"
|
||||||
|
>
|
||||||
|
<mat-select
|
||||||
|
class="with-placeholder-as-option"
|
||||||
|
name="colorScheme"
|
||||||
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
|
[placeholder]="appearancePlaceholder"
|
||||||
|
[value]="user?.settings?.colorScheme"
|
||||||
|
(selectionChange)="onChangeUserSetting('colorScheme', $event.value)"
|
||||||
|
>
|
||||||
|
<mat-option i18n [value]="null">Auto</mat-option>
|
||||||
|
<mat-option i18n value="LIGHT">Light</mat-option>
|
||||||
|
<mat-option i18n value="DARK">Dark</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex mt-4 py-1">
|
<div class="align-items-center d-flex mt-4 py-1">
|
||||||
@ -228,8 +267,8 @@
|
|||||||
class="align-items-center d-flex justify-content-center"
|
class="align-items-center d-flex justify-content-center"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-fab
|
mat-fab
|
||||||
[routerLink]="[]"
|
|
||||||
[queryParams]="{ createDialog: true }"
|
[queryParams]="{ createDialog: true }"
|
||||||
|
[routerLink]="[]"
|
||||||
>
|
>
|
||||||
<ion-icon name="add-outline" size="large"></ion-icon>
|
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
|
@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.openCreateAccountDialog();
|
this.openCreateAccountDialog();
|
||||||
} else if (params['editDialog']) {
|
} else if (params['editDialog']) {
|
||||||
if (this.accounts) {
|
if (this.accounts) {
|
||||||
const account = this.accounts.find((account) => {
|
const account = this.accounts.find(({ id }) => {
|
||||||
return account.id === params['accountId'];
|
return id === params['accountId'];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.openUpdateAccountDialog(account);
|
this.openUpdateAccountDialog(account);
|
||||||
@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
balance,
|
balance,
|
||||||
currency,
|
currency,
|
||||||
id,
|
id,
|
||||||
|
isExcluded,
|
||||||
name,
|
name,
|
||||||
platformId
|
platformId
|
||||||
}: AccountModel): void {
|
}: AccountModel): void {
|
||||||
@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
balance,
|
balance,
|
||||||
currency,
|
currency,
|
||||||
id,
|
id,
|
||||||
|
isExcluded,
|
||||||
name,
|
name,
|
||||||
platformId
|
platformId
|
||||||
}
|
}
|
||||||
@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
accountType: AccountType.SECURITIES,
|
accountType: AccountType.SECURITIES,
|
||||||
balance: 0,
|
balance: 0,
|
||||||
currency: this.user?.settings?.baseCurrency,
|
currency: this.user?.settings?.baseCurrency,
|
||||||
|
isExcluded: false,
|
||||||
name: null,
|
name: null,
|
||||||
platformId: null
|
platformId: null
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,14 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3 px-2">
|
||||||
|
<mat-checkbox
|
||||||
|
color="primary"
|
||||||
|
name="isExcluded"
|
||||||
|
[(ngModel)]="data.account.isExcluded"
|
||||||
|
>Exclude from Analysis</mat-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<div *ngIf="data.account.id">
|
<div *ngIf="data.account.id">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Account ID</mat-label>
|
<mat-label i18n>Account ID</mat-label>
|
||||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
|
@ -119,7 +119,7 @@
|
|||||||
Anlagestrategie? Ich freue mich über alle, die Ghostfolio
|
Anlagestrategie? Ich freue mich über alle, die Ghostfolio
|
||||||
ausprobieren. Bist du überzeugt vom Potential der Software? Jede
|
ausprobieren. Bist du überzeugt vom Potential der Software? Jede
|
||||||
Unterstützung für Ghostfolio ist willkommen. Sei es mit einer
|
Unterstützung für Ghostfolio ist willkommen. Sei es mit einer
|
||||||
<a href="https://ghostfol.io/pricing">Ghostfolio Premium</a>
|
<a [routerLink]="['/pricing']">Ghostfolio Premium</a>
|
||||||
Subscription zur Finanzierung des Hostings, einem positiven Rating
|
Subscription zur Finanzierung des Hostings, einem positiven Rating
|
||||||
im
|
im
|
||||||
<a
|
<a
|
||||||
|
@ -115,7 +115,7 @@
|
|||||||
strategy? I'm happy for everyone who tries Ghostfolio. Are you
|
strategy? I'm happy for everyone who tries Ghostfolio. Are you
|
||||||
convinced of its potential? Any support for Ghostfolio is welcome.
|
convinced of its potential? Any support for Ghostfolio is welcome.
|
||||||
Be it with a
|
Be it with a
|
||||||
<a href="https://ghostfol.io/pricing">Ghostfolio Premium</a>
|
<a [routerLink]="['/pricing']">Ghostfolio Premium</a>
|
||||||
Subscription to finance the hosting, a positive rating in the
|
Subscription to finance the hosting, a positive rating in the
|
||||||
<a
|
<a
|
||||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
<a [routerLink]="['/markets']">economic situation</a> at this time,
|
<a [routerLink]="['/markets']">economic situation</a> at this time,
|
||||||
the goal set at the beginning of the year to build a sustainable
|
the goal set at the beginning of the year to build a sustainable
|
||||||
business and reach break-even with the SaaS offering (<a
|
business and reach break-even with the SaaS offering (<a
|
||||||
[routerLink]="['/markets']"
|
[routerLink]="['/pricing']"
|
||||||
>Ghostfolio Premium</a
|
>Ghostfolio Premium</a
|
||||||
>) has been achieved. We will continue to leverage the revenue to
|
>) has been achieved. We will continue to leverage the revenue to
|
||||||
further improve the fully managed cloud offering for our paying
|
further improve the fully managed cloud offering for our paying
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { Hacktoberfest2022PageComponent } from './hacktoberfest-2022-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
component: Hacktoberfest2022PageComponent,
|
||||||
|
path: '',
|
||||||
|
title: 'Hacktoberfest 2022'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class Hacktoberfest2022RoutingModule {}
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
selector: 'gf-hacktoberfest-2022-page',
|
||||||
|
styleUrls: ['./hacktoberfest-2022-page.scss'],
|
||||||
|
templateUrl: './hacktoberfest-2022-page.html'
|
||||||
|
})
|
||||||
|
export class Hacktoberfest2022PageComponent {}
|
@ -0,0 +1,178 @@
|
|||||||
|
<div class="blog container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<article>
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h1 class="mb-1">Hacktoberfest 2022</h1>
|
||||||
|
<div class="mb-3 text-muted"><small>2022-10-01</small></div>
|
||||||
|
<img
|
||||||
|
alt="Hacktoberfest 2022 with Ghostfolio Teaser"
|
||||||
|
class="rounded w-100"
|
||||||
|
src="../assets/images/blog/hacktoberfest-2022.png"
|
||||||
|
title="Hacktoberfest 2022 with Ghostfolio"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<section class="mb-4">
|
||||||
|
<p>
|
||||||
|
We are very excited to join
|
||||||
|
<a href="https://hacktoberfest.com">Hacktoberfest</a> for the first
|
||||||
|
time with <a href="https://ghostfol.io">Ghostfolio</a> and meet new
|
||||||
|
and ambitious open-source contributors. Hacktoberfest is a
|
||||||
|
month-long celebration of open-source projects, their maintainers,
|
||||||
|
and the entire community of contributors. Each October, open source
|
||||||
|
maintainers from all over the world give extra attention to new
|
||||||
|
contributors while guiding them through their first pull requests on
|
||||||
|
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<h2 class="h4">About Ghostfolio</h2>
|
||||||
|
<p>
|
||||||
|
Ghostfolio is a modern web application to manage your personal
|
||||||
|
finance. The software presents the current assets in real time and
|
||||||
|
supports the decision making of future investments. Whether
|
||||||
|
rebalancing the asset classes (stocks, ETFs, cryptocurrencies, etc.)
|
||||||
|
of your portfolio or financing an apartment, Ghostfolio offers
|
||||||
|
solid, data-driven decision support.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Ghostfolio is written in
|
||||||
|
<a href="https://www.typescriptlang.org">TypeScript</a> and
|
||||||
|
organized as an <a href="https://nx.dev">Nx</a> workspace. The
|
||||||
|
backend is based on <a href="https://nestjs.com">NestJS</a> using
|
||||||
|
<a href="https://www.postgresql.org">PostgreSQL</a> as a database
|
||||||
|
together with <a href="https://www.prisma.io">Prisma</a> and
|
||||||
|
<a href="https://redis.io">Redis</a> for caching. The frontend is
|
||||||
|
built with <a href="https://angular.io">Angular</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<h2 class="h4">How to contribute?</h2>
|
||||||
|
<p>
|
||||||
|
Every contribution matters. This can be implementing new features,
|
||||||
|
fixing bugs, refactoring the code, improving the documentation,
|
||||||
|
adding more unit tests, or translating into another language.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Are you not yet familiar with our code base? That is not a problem.
|
||||||
|
We have applied the label <code>hacktoberfest</code> to a few
|
||||||
|
<a
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest"
|
||||||
|
>issues</a
|
||||||
|
>
|
||||||
|
and
|
||||||
|
<a
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio/discussions?discussions_q=label%3Ahacktoberfest"
|
||||||
|
>ideas</a
|
||||||
|
>
|
||||||
|
that are well suited for newcomers.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The official Hacktoberfest website provides some valuable
|
||||||
|
<a
|
||||||
|
href="https://hacktoberfest.com/participation/#beginner-resources"
|
||||||
|
>resources for beginners</a
|
||||||
|
>
|
||||||
|
to start contributing in open source.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<h2 class="h4">Get support</h2>
|
||||||
|
<p>
|
||||||
|
If you have further questions or ideas, please join our growing
|
||||||
|
<a href="https://ghostfolio.slack.com">Slack community</a> or get in
|
||||||
|
touch on Twitter
|
||||||
|
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> or by
|
||||||
|
email via <a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We look forward to hearing from you.<br />
|
||||||
|
Thomas from Ghostfolio
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<ul class="list-inline">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Angular</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Community</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Cryptocurrency</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">ETF</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Finance</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Fintech</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Ghostfolio</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">GitHub</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Hacktoberfest</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Investment</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">NestJS</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Nx</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">October</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Open Source</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">OSS</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Personal Finance</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Portfolio</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Portfolio Tracker</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Prisma</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Software</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Stock</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">TypeScript</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Wealth</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Wealth Management</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Web3</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Web 3.0</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,13 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { Hacktoberfest2022RoutingModule } from './hacktoberfest-2022-page-routing.module';
|
||||||
|
import { Hacktoberfest2022PageComponent } from './hacktoberfest-2022-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Hacktoberfest2022PageComponent],
|
||||||
|
imports: [CommonModule, Hacktoberfest2022RoutingModule, RouterModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class Hacktoberfest2022PageModule {}
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -2,6 +2,30 @@
|
|||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Blog</h3>
|
<h3 class="mb-3 text-center" i18n>Blog</h3>
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="container p-0">
|
||||||
|
<div class="flex-nowrap no-gutters row">
|
||||||
|
<a
|
||||||
|
class="d-flex w-100"
|
||||||
|
href="../en/blog/2022/10/hacktoberfest-2022"
|
||||||
|
>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="h6 m-0 text-truncate">Hacktoberfest 2022</div>
|
||||||
|
<div class="d-flex text-muted">2022-10-01</div>
|
||||||
|
</div>
|
||||||
|
<div class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="chevron text-muted"
|
||||||
|
name="chevron-forward-outline"
|
||||||
|
size="small"
|
||||||
|
></ion-icon>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="container p-0">
|
<div class="container p-0">
|
||||||
|
@ -192,6 +192,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
|
<mat-card class="d-flex flex-column h-100">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h4>Multi-Language</h4>
|
||||||
|
<p class="m-0">
|
||||||
|
Use Ghostfolio in multiple languages: English, Dutch, German,
|
||||||
|
Italian and Spanish are currently supported.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -11,6 +15,9 @@ import { Subject } from 'rxjs';
|
|||||||
export class LandingPageComponent implements OnDestroy, OnInit {
|
export class LandingPageComponent implements OnDestroy, OnInit {
|
||||||
public currentYear = format(new Date(), 'yyyy');
|
public currentYear = format(new Date(), 'yyyy');
|
||||||
public demoAuthToken: string;
|
public demoAuthToken: string;
|
||||||
|
public deviceType: string;
|
||||||
|
public hasPermissionForStatistics: boolean;
|
||||||
|
public statistics: Statistics;
|
||||||
public testimonials = [
|
public testimonials = [
|
||||||
{
|
{
|
||||||
author: 'Philipp',
|
author: 'Philipp',
|
||||||
@ -36,9 +43,23 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor() {}
|
public constructor(
|
||||||
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService
|
||||||
|
) {
|
||||||
|
const { globalPermissions, statistics } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
public ngOnInit() {}
|
this.hasPermissionForStatistics = hasPermission(
|
||||||
|
globalPermissions,
|
||||||
|
permissions.enableStatistics
|
||||||
|
);
|
||||||
|
|
||||||
|
this.statistics = statistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<h1 class="font-weight-bold intro my-5">
|
<h1 class="font-weight-bold intro mt-5">
|
||||||
Manage your wealth like a boss
|
Manage your wealth like a boss
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<p class="lead mb-4">
|
||||||
|
Ghostfolio is a privacy-first, open source dashboard for your personal
|
||||||
|
finances. Break down your asset allocation, know your net worth and make
|
||||||
|
solid, data-driven investment decisions.
|
||||||
|
</p>
|
||||||
|
<div class="mb-4">
|
||||||
<a
|
<a
|
||||||
href="https://www.youtube.com/watch?v=yY6ObSQVJZk"
|
href="https://www.youtube.com/watch?v=yY6ObSQVJZk"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -23,9 +28,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="button-container row">
|
<div class="button-container mb-5 row">
|
||||||
<div class="align-items-center col d-flex justify-content-center">
|
<div class="align-items-center col d-flex justify-content-center">
|
||||||
<div class="py-5 text-center">
|
<div class="text-center">
|
||||||
<a
|
<a
|
||||||
class="d-inline-block"
|
class="d-inline-block"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -42,19 +47,147 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row my-5">
|
<div *ngIf="hasPermissionForStatistics" class="row mb-5">
|
||||||
|
<div
|
||||||
|
class="col-md-4 d-flex my-1"
|
||||||
|
[ngClass]="{ 'justify-content-center': this.deviceType !== 'mobile' }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="d-block"
|
||||||
|
title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
|
||||||
|
[routerLink]="['/about']"
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
icon="people-outline"
|
||||||
|
size="large"
|
||||||
|
[value]="statistics?.activeUsers30d ?? '-'"
|
||||||
|
>Monthly Active Users</gf-value
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4 d-flex my-1"
|
||||||
|
[ngClass]="{ 'justify-content-center': this.deviceType !== 'mobile' }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="d-block"
|
||||||
|
title="Ghostfolio in Numbers: Stars on GitHub"
|
||||||
|
[routerLink]="['/about']"
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
icon="star-outline"
|
||||||
|
size="large"
|
||||||
|
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||||
|
>Stars on GitHub</gf-value
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4 d-flex my-1"
|
||||||
|
[ngClass]="{ 'justify-content-center': this.deviceType !== 'mobile' }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="d-block"
|
||||||
|
title="Ghostfolio in Numbers: Pulls on Docker Hub"
|
||||||
|
[routerLink]="['/about']"
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
icon="cloud-download-outline"
|
||||||
|
size="large"
|
||||||
|
[value]="statistics?.dockerHubPulls ?? '-'"
|
||||||
|
>Pulls on Docker Hub</gf-value
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-12 text-center text-muted"><small>As seen in</small></div>
|
||||||
|
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||||
|
<a
|
||||||
|
class="d-block logo logo-alternative-to mask"
|
||||||
|
href="https://alternativeto.net"
|
||||||
|
target="_blank"
|
||||||
|
title="AlternativeTo - Crowdsourced software recommendations"
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||||
|
<a
|
||||||
|
class="d-block logo logo-awesome"
|
||||||
|
href="https://github.com/awesome-selfhosted/awesome-selfhosted"
|
||||||
|
target="_blank"
|
||||||
|
title="Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers"
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||||
|
<a
|
||||||
|
class="d-block logo logo-openstartup"
|
||||||
|
href="https://openstartup.tm"
|
||||||
|
target="_blank"
|
||||||
|
title="Open Startup: The most complete list of open startups"
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||||
|
<a
|
||||||
|
class="d-block logo logo-privacy-tools mask"
|
||||||
|
href="https://www.privacytools.io"
|
||||||
|
target="_blank"
|
||||||
|
title="Privacy Tools: Software Alternatives and Encryption"
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||||
|
<a
|
||||||
|
class="d-block logo logo-product-hunt"
|
||||||
|
href="https://www.producthunt.com"
|
||||||
|
target="_blank"
|
||||||
|
title="Product Hunt – The best new products in tech."
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||||
|
<a
|
||||||
|
class="d-block logo logo-unraid mask"
|
||||||
|
href="https://unraid.net"
|
||||||
|
target="_blank"
|
||||||
|
title="Unraid | Unleash Your Hardware"
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-3 row">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<h2 class="h4 mb-1 text-center">
|
<h2 class="h4 mb-1 text-center">
|
||||||
Protect your <strong>assets</strong>. Refine your
|
Protect your <strong>assets</strong>. Refine your
|
||||||
<strong>personal investment strategy</strong>.
|
<strong>personal investment strategy</strong>.
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">
|
<p class="lead m-0">
|
||||||
Ghostfolio empowers busy people to keep track of stocks, ETFs or
|
Ghostfolio empowers busy people to keep track of stocks, ETFs or
|
||||||
cryptocurrencies and make solid, data-driven investment decisions.
|
cryptocurrencies without being tracked.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-3">
|
||||||
|
<div class="col-md-4 my-2">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-title>360° View</mat-card-title>
|
||||||
|
Get the full picture of your personal finances across multiple
|
||||||
|
platforms.
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 my-2">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-title>Web3 Ready</mat-card-title>
|
||||||
|
Use Ghostfolio anonymously and own your financial data.
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 my-2">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-title>Open Source</mat-card-title>
|
||||||
|
Benefit from continuous improvements through a strong community.
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row my-5">
|
<div class="row my-5">
|
||||||
<div class="col-md-6 offset-md-3">
|
<div class="col-md-6 offset-md-3">
|
||||||
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
|
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
|
||||||
@ -101,7 +234,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mt-4 text-center">
|
<div class="mt-4 text-center">
|
||||||
<a [routerLink]="['/about']" mat-stroked-button
|
<a mat-stroked-button [routerLink]="['/about']"
|
||||||
>Learn more about Ghostfolio</a
|
>Learn more about Ghostfolio</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -133,24 +266,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row my-5">
|
<div class="row my-3">
|
||||||
<div class="col-md-6 offset-md-3">
|
<div class="col-12">
|
||||||
<h2 class="h4 mb-1 text-center">
|
<h2 class="h4 mb-1 text-center">
|
||||||
How does <strong>Ghostfolio</strong> work?
|
How does <strong>Ghostfolio</strong> work?
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
|
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
|
||||||
<ol class="m-0 pl-3">
|
</div>
|
||||||
<li class="mb-2">
|
<div class="col-md-4 my-2">
|
||||||
Sign up anonymously<br />(no e-mail address nor credit card required)
|
<mat-card class="d-flex flex-row h-100">
|
||||||
</li>
|
<div class="flex-grow-1">
|
||||||
<li class="mb-2">Add any of your historical transactions</li>
|
<div class="font-weight-bold">Sign up anonymously*</div>
|
||||||
<li>Get valuable insights of your portfolio composition</li>
|
<div class="text-muted">
|
||||||
</ol>
|
<small>* no e-mail address nor credit card required</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pl-2 text-muted text-right">1</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 my-2">
|
||||||
|
<mat-card class="d-flex flex-row h-100">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="font-weight-bold">
|
||||||
|
Add any of your historical transactions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pl-2 text-muted text-right">2</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 my-2">
|
||||||
|
<mat-card class="d-flex flex-row h-100">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="font-weight-bold">
|
||||||
|
Get valuable insights of your portfolio composition
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pl-2 text-muted text-right">3</div>
|
||||||
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row my-5">
|
<div class="row my-5">
|
||||||
<div class="col-md-6 offset-md-3">
|
<div class="col">
|
||||||
<h2 class="h4 mb-1 text-center">Are <strong>you</strong> ready?</h2>
|
<h2 class="h4 mb-1 text-center">Are <strong>you</strong> ready?</h2>
|
||||||
<p class="lead mb-3 text-center">
|
<p class="lead mb-3 text-center">
|
||||||
Join now or check out the example account
|
Join now or check out the example account
|
||||||
@ -194,7 +351,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="align-items-center d-flex flex-column w-100">
|
<div class="align-items-center d-flex flex-column w-100">
|
||||||
<a
|
<a
|
||||||
class="agplv3-logo"
|
class="logo logo-agplv3 mask"
|
||||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="GNU Affero General Public License Version 3"
|
title="GNU Affero General Public License Version 3"
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
||||||
import { LandingPageComponent } from './landing-page.component';
|
import { LandingPageComponent } from './landing-page.component';
|
||||||
@ -12,8 +14,10 @@ import { LandingPageComponent } from './landing-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfLogoModule,
|
GfLogoModule,
|
||||||
|
GfValueModule,
|
||||||
LandingPageRoutingModule,
|
LandingPageRoutingModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@ -3,16 +3,6 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.agplv3-logo {
|
|
||||||
background-color: rgba(var(--dark-primary-text));
|
|
||||||
height: 3rem;
|
|
||||||
mask-image: url('/assets/images/AGPLv3-logo.svg');
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
width: 7.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
.button-container {
|
||||||
.mat-stroked-button {
|
.mat-stroked-button {
|
||||||
background-color: var(--light-background);
|
background-color: var(--light-background);
|
||||||
@ -34,6 +24,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 3rem;
|
||||||
|
width: 7.5rem;
|
||||||
|
|
||||||
|
&.mask {
|
||||||
|
background-color: rgba(var(--dark-secondary-text));
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logo-agplv3 {
|
||||||
|
mask-image: url('/assets/images/logo-AGPLv3.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logo-alternative-to {
|
||||||
|
mask-image: url('/assets/images/logo-alternative-to.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logo-awesome {
|
||||||
|
background-image: url('/assets/images/logo-awesome.png');
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logo-openstartup {
|
||||||
|
background-image: url('/assets/images/logo-openstartup.png');
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logo-privacy-tools {
|
||||||
|
mask-image: url('/assets/images/logo-privacy-tools.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logo-product-hunt {
|
||||||
|
background-image: url('/assets/images/logo-product-hunt.png');
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logo-unraid {
|
||||||
|
mask-image: url('/assets/images/logo-unraid.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.outro-inner-container {
|
.outro-inner-container {
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
max-height: 66vh;
|
max-height: 66vh;
|
||||||
@ -56,16 +98,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
.agplv3-logo {
|
|
||||||
background-color: rgba(var(--light-primary-text));
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
.button-container {
|
||||||
.mat-stroked-button {
|
.mat-stroked-button {
|
||||||
background-color: var(--dark-background);
|
background-color: var(--dark-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
&.logo-agplv3,
|
||||||
|
&.logo-alternative-to,
|
||||||
|
&.logo-privacy-tools,
|
||||||
|
&.logo-unraid {
|
||||||
|
background-color: rgba(var(--light-primary-text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.outro-inner-container {
|
.outro-inner-container {
|
||||||
div {
|
div {
|
||||||
background-image: url('/assets/intro-dark.jpg') !important;
|
background-image: url('/assets/intro-dark.jpg') !important;
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Market, ToggleOption } from '@ghostfolio/common/types';
|
import { Market, ToggleOption } from '@ghostfolio/common/types';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { Account, AssetClass, DataSource } from '@prisma/client';
|
import { Account, AssetClass, DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -174,7 +175,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
for (const assetClass of Object.keys(AssetClass)) {
|
for (const assetClass of Object.keys(AssetClass)) {
|
||||||
assetClassFilters.push({
|
assetClassFilters.push({
|
||||||
id: assetClass,
|
id: assetClass,
|
||||||
label: assetClass,
|
label: translate(assetClass),
|
||||||
type: 'ASSET_CLASS'
|
type: 'ASSET_CLASS'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -450,6 +451,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
colorScheme: this.user?.settings?.colorScheme,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
hasPermissionToReportDataGlitch: hasPermission(
|
||||||
|
@ -10,6 +10,30 @@
|
|||||||
></gf-activities-filter>
|
></gf-activities-filter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header class="mb-2 overflow-hidden w-100">
|
||||||
|
<mat-card-title class="m-0 text-truncate" i18n
|
||||||
|
>Proportion of Net Worth</mat-card-title
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end l-2"
|
||||||
|
size="medium"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
|
||||||
|
></gf-value>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-progress-bar
|
||||||
|
mode="determinate"
|
||||||
|
[title]="(portfolioDetails?.filteredValueInPercentage * 100).toFixed(2) + '%'"
|
||||||
|
[value]="portfolioDetails?.filteredValueInPercentage * 100"
|
||||||
|
></mat-progress-bar>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="proportion-charts row">
|
<div class="proportion-charts row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
@ -26,6 +50,7 @@
|
|||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['id']"
|
[keys]="['id']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -55,6 +80,7 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['currency']"
|
[keys]="['currency']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -83,6 +109,7 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['assetClass', 'assetSubClass']"
|
[keys]="['assetClass', 'assetSubClass']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -109,6 +136,7 @@
|
|||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['symbol']"
|
[keys]="['symbol']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -139,6 +167,7 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['name']"
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -168,6 +197,7 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['name']"
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -196,6 +226,7 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['name']"
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||||
@ -22,7 +23,8 @@ import { AllocationsPageComponent } from './allocations-page.component';
|
|||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
GfWorldMapChartModule,
|
GfWorldMapChartModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatCardModule
|
MatCardModule,
|
||||||
|
MatProgressBarModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-card-header-text {
|
.mat-card-header-text {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,4 +29,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-progress-bar {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.mat-progress-bar-background {
|
||||||
|
fill: rgb(var(--palette-background-unselected-chip));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-progress-bar-buffer {
|
||||||
|
background-color: rgb(var(--palette-background-unselected-chip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
.mat-progress-bar {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-progress-bar-background {
|
||||||
|
fill: rgb(var(--palette-background-unselected-chip-dark));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-progress-bar-buffer {
|
||||||
|
background-color: rgb(var(--palette-background-unselected-chip-dark));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { 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 {
|
import {
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
Position,
|
Position,
|
||||||
UniqueAsset,
|
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -24,20 +25,23 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||||
public benchmarkDataItems: HistoricalDataItem[] = [];
|
public benchmarkDataItems: HistoricalDataItem[] = [];
|
||||||
public benchmarks: UniqueAsset[];
|
public benchmarks: Partial<SymbolProfile>[];
|
||||||
public bottom3: Position[];
|
public bottom3: Position[];
|
||||||
|
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||||
public daysInMarket: number;
|
public daysInMarket: number;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public firstOrderDate: Date;
|
public firstOrderDate: Date;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public investments: InvestmentItem[];
|
public investments: InvestmentItem[];
|
||||||
public investmentsByMonth: InvestmentItem[];
|
public investmentsByMonth: InvestmentItem[];
|
||||||
public mode: GroupBy;
|
public isLoadingBenchmarkComparator: boolean;
|
||||||
|
public isLoadingInvestmentChart: boolean;
|
||||||
|
public mode: GroupBy = 'month';
|
||||||
public modeOptions: ToggleOption[] = [
|
public modeOptions: ToggleOption[] = [
|
||||||
{ label: $localize`Monthly`, value: 'month' },
|
{ label: $localize`Monthly`, value: 'month' }
|
||||||
{ label: $localize`Accumulating`, value: undefined }
|
|
||||||
];
|
];
|
||||||
public performanceDataItems: HistoricalDataItem[];
|
public performanceDataItems: HistoricalDataItem[];
|
||||||
|
public performanceDataItemsInPercentage: HistoricalDataItem[];
|
||||||
public top3: Position[];
|
public top3: Position[];
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@ -75,9 +79,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeBenchmark(benchmark: UniqueAsset) {
|
public onChangeBenchmark(symbolProfileId: string) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ benchmark })
|
.putUserSetting({ benchmark: symbolProfileId })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.userService.remove();
|
this.userService.remove();
|
||||||
@ -121,32 +125,51 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
if (this.user.settings.isExperimentalFeatures) {
|
this.isLoadingBenchmarkComparator = true;
|
||||||
|
this.isLoadingInvestmentChart = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({ range: this.user?.settings?.dateRange, version: 2 })
|
.fetchPortfolioPerformance({
|
||||||
|
range: this.user?.settings?.dateRange
|
||||||
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ chart }) => {
|
.subscribe(({ chart, firstOrderDate }) => {
|
||||||
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());
|
this.firstOrderDate = firstOrderDate ?? new Date();
|
||||||
this.performanceDataItems = chart;
|
this.daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||||
|
|
||||||
|
this.investments = [];
|
||||||
|
this.performanceDataItems = [];
|
||||||
|
this.performanceDataItemsInPercentage = [];
|
||||||
|
|
||||||
|
for (const {
|
||||||
|
date,
|
||||||
|
netPerformanceInPercentage,
|
||||||
|
totalInvestment,
|
||||||
|
value
|
||||||
|
} of chart) {
|
||||||
|
this.investments.push({ date, investment: totalInvestment });
|
||||||
|
this.performanceDataItems.push({
|
||||||
|
date,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
this.performanceDataItemsInPercentage.push({
|
||||||
|
date,
|
||||||
|
value: netPerformanceInPercentage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoadingInvestmentChart = false;
|
||||||
|
|
||||||
this.updateBenchmarkDataItems();
|
this.updateBenchmarkDataItems();
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchInvestments()
|
.fetchInvestments({
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
groupBy: 'month',
|
||||||
.subscribe(({ firstOrderDate, investments }) => {
|
range: this.user?.settings?.dateRange
|
||||||
this.daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
})
|
||||||
this.investments = investments;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchInvestmentsByMonth()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ investments }) => {
|
.subscribe(({ investments }) => {
|
||||||
this.investmentsByMonth = investments;
|
this.investmentsByMonth = investments;
|
||||||
@ -155,7 +178,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositions({ range: 'max' })
|
.fetchPositions({ range: this.user?.settings?.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ positions }) => {
|
.subscribe(({ positions }) => {
|
||||||
const positionsSorted = sortBy(
|
const positionsSorted = sortBy(
|
||||||
@ -179,9 +202,15 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
private updateBenchmarkDataItems() {
|
private updateBenchmarkDataItems() {
|
||||||
if (this.user.settings.benchmark) {
|
if (this.user.settings.benchmark) {
|
||||||
|
const { dataSource, symbol } =
|
||||||
|
this.benchmarks.find(({ id }) => {
|
||||||
|
return id === this.user.settings.benchmark;
|
||||||
|
}) ?? {};
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchBenchmarkBySymbol({
|
.fetchBenchmarkBySymbol({
|
||||||
...this.user.settings.benchmark,
|
dataSource,
|
||||||
|
symbol,
|
||||||
startDate: this.firstOrderDate
|
startDate: this.firstOrderDate
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -193,8 +222,14 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.isLoadingBenchmarkComparator = false;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.benchmarkDataItems = [];
|
||||||
|
|
||||||
|
this.isLoadingBenchmarkComparator = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
<h3 class="d-flex justify-content-center" i18n>Analysis</h3>
|
||||||
<div *ngIf="user?.settings?.isExperimentalFeatures" class="mb-5 row">
|
<div *ngIf="user?.settings?.viewMode !== 'ZEN'" class="my-4 text-center">
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="user?.settings?.dateRange"
|
||||||
|
[isLoading]="isLoadingBenchmarkComparator"
|
||||||
|
[options]="dateRangeOptions"
|
||||||
|
(change)="onChangeDateRange($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5 row">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<gf-benchmark-comparator
|
<gf-benchmark-comparator
|
||||||
class="h-100"
|
class="h-100"
|
||||||
[benchmark]="user?.settings?.benchmark"
|
[benchmark]="user?.settings?.benchmark"
|
||||||
[benchmarkDataItems]="benchmarkDataItems"
|
[benchmarkDataItems]="benchmarkDataItems"
|
||||||
[benchmarks]="benchmarks"
|
[benchmarks]="benchmarks"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[daysInMarket]="daysInMarket"
|
[daysInMarket]="daysInMarket"
|
||||||
|
[isLoading]="isLoadingBenchmarkComparator"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[performanceDataItems]="performanceDataItems"
|
[performanceDataItems]="performanceDataItemsInPercentage"
|
||||||
[user]="user"
|
[user]="user"
|
||||||
(benchmarkChanged)="onChangeBenchmark($event)"
|
(benchmarkChanged)="onChangeBenchmark($event)"
|
||||||
(dateRangeChanged)="onChangeDateRange($event)"
|
|
||||||
></gf-benchmark-comparator>
|
></gf-benchmark-comparator>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -95,6 +104,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5 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>Portfolio Evolution</span>
|
||||||
|
<gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<gf-investment-chart
|
||||||
|
class="h-100"
|
||||||
|
[benchmarkDataItems]="investments"
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[daysInMarket]="daysInMarket"
|
||||||
|
[historicalDataItems]="performanceDataItems"
|
||||||
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
|
[isLoading]="isLoadingBenchmarkComparator"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[range]="user?.settings?.dateRange"
|
||||||
|
></gf-investment-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<div class="align-items-center d-flex mb-4">
|
<div class="align-items-center d-flex mb-4">
|
||||||
@ -116,24 +154,15 @@
|
|||||||
></gf-toggle>
|
></gf-toggle>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-container">
|
<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
|
<gf-investment-chart
|
||||||
class="h-100"
|
class="h-100"
|
||||||
groupBy="month"
|
groupBy="month"
|
||||||
|
[benchmarkDataItems]="investmentsByMonth"
|
||||||
[currency]="user?.settings?.baseCurrency"
|
[currency]="user?.settings?.baseCurrency"
|
||||||
[daysInMarket]="daysInMarket"
|
[daysInMarket]="daysInMarket"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[investments]="investmentsByMonth"
|
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[ngClass]="{ 'd-none': !mode }"
|
[range]="user?.settings?.dateRange"
|
||||||
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
||||||
></gf-investment-chart>
|
></gf-investment-chart>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioSummary()
|
.fetchPortfolioDetails({})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ cash, currentValue }) => {
|
.subscribe(({ summary }) => {
|
||||||
if (cash === null || currentValue === null) {
|
if (summary.cash === null || summary.currentValue === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fireWealth = new Big(currentValue);
|
this.fireWealth = new Big(summary.currentValue);
|
||||||
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
||||||
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="mb-3" i18n>Calculator</h4>
|
<h4 class="mb-3" i18n>Calculator</h4>
|
||||||
<gf-fire-calculator
|
<gf-fire-calculator
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[currency]="user?.settings?.baseCurrency"
|
[currency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[fireWealth]="fireWealth?.toNumber()"
|
[fireWealth]="fireWealth?.toNumber()"
|
||||||
@ -35,7 +36,7 @@
|
|||||||
}"
|
}"
|
||||||
></ngx-skeleton-loader>
|
></ngx-skeleton-loader>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!isLoading">
|
<div *ngIf="!isLoading" i18n>
|
||||||
If you retire today, you would be able to withdraw
|
If you retire today, you would be able to withdraw
|
||||||
<span class="font-weight-bold"
|
<span class="font-weight-bold"
|
||||||
><gf-value
|
><gf-value
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
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 { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { AssetClass, DataSource } from '@prisma/client';
|
import { AssetClass, DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -130,7 +131,7 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
for (const assetClass of Object.keys(AssetClass)) {
|
for (const assetClass of Object.keys(AssetClass)) {
|
||||||
assetClassFilters.push({
|
assetClassFilters.push({
|
||||||
id: assetClass,
|
id: assetClass,
|
||||||
label: assetClass,
|
label: translate(assetClass),
|
||||||
type: 'ASSET_CLASS'
|
type: 'ASSET_CLASS'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -192,6 +193,7 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
colorScheme: this.user?.settings?.colorScheme,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
hasPermissionToReportDataGlitch: hasPermission(
|
||||||
|
@ -14,6 +14,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
|||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
|
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
|
||||||
import { isUUID } from 'class-validator';
|
import { isUUID } from 'class-validator';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
@ -40,8 +41,12 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
@ViewChild('autocomplete') autocomplete;
|
@ViewChild('autocomplete') autocomplete;
|
||||||
|
|
||||||
public activityForm: FormGroup;
|
public activityForm: FormGroup;
|
||||||
public assetClasses = Object.keys(AssetClass);
|
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
|
||||||
public assetSubClasses = Object.keys(AssetSubClass);
|
return { id: assetClass, label: translate(assetClass) };
|
||||||
|
});
|
||||||
|
public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => {
|
||||||
|
return { id: assetSubClass, label: translate(assetSubClass) };
|
||||||
|
});
|
||||||
public currencies: string[] = [];
|
public currencies: string[] = [];
|
||||||
public currentMarketPrice = null;
|
public currentMarketPrice = null;
|
||||||
public filteredLookupItems: LookupItem[];
|
public filteredLookupItems: LookupItem[];
|
||||||
|
@ -4,17 +4,17 @@
|
|||||||
(keyup.enter)="activityForm.valid && onSubmit()"
|
(keyup.enter)="activityForm.valid && onSubmit()"
|
||||||
(ngSubmit)="onSubmit()"
|
(ngSubmit)="onSubmit()"
|
||||||
>
|
>
|
||||||
<h1 *ngIf="data.activity.id" mat-dialog-title i18n>Update activity</h1>
|
<h1 *ngIf="data.activity.id" i18n mat-dialog-title>Update activity</h1>
|
||||||
<h1 *ngIf="!data.activity.id" mat-dialog-title i18n>Add activity</h1>
|
<h1 *ngIf="!data.activity.id" i18n mat-dialog-title>Add activity</h1>
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
<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>
|
||||||
<mat-select formControlName="type">
|
<mat-select formControlName="type">
|
||||||
<mat-option value="BUY" i18n>BUY</mat-option>
|
<mat-option i18n value="BUY">BUY</mat-option>
|
||||||
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
|
<mat-option i18n value="DIVIDEND">DIVIDEND</mat-option>
|
||||||
<mat-option value="ITEM" i18n>ITEM</mat-option>
|
<mat-option i18n value="ITEM">ITEM</mat-option>
|
||||||
<mat-option value="SELL" i18n>SELL</mat-option>
|
<mat-option i18n value="SELL">SELL</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -156,8 +156,8 @@
|
|||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let assetClass of assetClasses"
|
*ngFor="let assetClass of assetClasses"
|
||||||
[value]="assetClass"
|
[value]="assetClass.id"
|
||||||
>{{ assetClass }}</mat-option
|
>{{ assetClass.label }}</mat-option
|
||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@ -171,8 +171,8 @@
|
|||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let assetSubClass of assetSubClasses"
|
*ngFor="let assetSubClass of assetSubClasses"
|
||||||
[value]="assetSubClass"
|
[value]="assetSubClass.id"
|
||||||
>{{ assetSubClass }}</mat-option
|
>{{ assetSubClass.label }}</mat-option
|
||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@ -188,7 +188,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
|
|
||||||
input.onchange = (event) => {
|
input.onchange = (event) => {
|
||||||
this.snackBar.open('⏳' + $localize`Importing data...`);
|
this.snackBar.open('⏳ ' + $localize`Importing data...`);
|
||||||
|
|
||||||
// Getting the file reference
|
// Getting the file reference
|
||||||
const file = (event.target as HTMLInputElement).files[0];
|
const file = (event.target as HTMLInputElement).files[0];
|
||||||
@ -334,9 +334,13 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
private handleImportSuccess() {
|
private handleImportSuccess() {
|
||||||
this.fetchActivities();
|
this.fetchActivities();
|
||||||
|
|
||||||
this.snackBar.open('✅' + $localize`Import has been completed`, undefined, {
|
this.snackBar.open(
|
||||||
|
'✅ ' + $localize`Import has been completed`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
duration: 3000
|
duration: 3000
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private openCreateTransactionDialog(aActivity?: Activity): void {
|
private openCreateTransactionDialog(aActivity?: Activity): void {
|
||||||
@ -405,6 +409,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
colorScheme: this.user?.settings?.colorScheme,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
hasPermissionToReportDataGlitch: hasPermission(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user