Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
5cd6edaf3a | |||
98be8745d9 | |||
861dff9210 | |||
f2364eed10 | |||
d5392de7c9 | |||
0f72673ef4 | |||
641fe4e8f4 | |||
18e06bb6e6 | |||
5b588c2000 | |||
162d19fa44 | |||
4a815d2031 | |||
d2aeeb3e88 | |||
ba926ffcf2 | |||
5ea455b98b | |||
39f315aba0 | |||
df2dfc20a1 | |||
81e83d4cea | |||
5d4156ecec | |||
4693a8baa2 | |||
773444b1e2 | |||
3c46bde8d5 | |||
63ee33b685 | |||
bc87c0a3e1 | |||
caa9fc3efa | |||
9ed82ac82b | |||
9c9ca4ab1e | |||
b0b0942162 | |||
9cbf789c22 | |||
ee5ab05d8a | |||
20731c67cb | |||
bf8856ad19 | |||
a31d79821d | |||
48ab862bb6 | |||
ba234a470e | |||
ccae660104 | |||
21ed91d184 | |||
5fd413e57e | |||
4c194c938a | |||
a4d049e53d | |||
f9c4408126 | |||
d046f1d498 | |||
ad96d6e53e | |||
747e5b63fa | |||
b1187cf880 | |||
ba9e6eab58 | |||
01feead017 | |||
6a0cfb8f77 | |||
6386786ac0 | |||
d3be6577c8 | |||
73a967a7e5 | |||
836ff6ec13 | |||
c5bb3023d3 | |||
695c378b48 | |||
fe975945d1 | |||
d8782b0d4c | |||
e14f08a8fb | |||
72c065a59d | |||
98dac4052a | |||
2083d28d02 | |||
addd5c36d9 | |||
aad8f77093 | |||
a904208d06 | |||
2733b78044 | |||
b43b515df1 | |||
70e14b4d3c | |||
0f7d1b7d59 | |||
c2ab6a6c44 | |||
c71a4c078e | |||
e17b217032 | |||
408e08d43c | |||
2fa324702f | |||
05b0efef82 | |||
7c91727eb1 | |||
0ee2258af8 | |||
308b218487 |
11
.storybook/main.js
Normal file
11
.storybook/main.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
stories: [],
|
||||||
|
addons: ['@storybook/addon-essentials']
|
||||||
|
// uncomment the property below if you want to apply some webpack config globally
|
||||||
|
// webpackFinal: async (config, { configType }) => {
|
||||||
|
// // Make whatever fine-grained changes you need that should apply to all storybook configs
|
||||||
|
|
||||||
|
// // Return the altered config
|
||||||
|
// return config;
|
||||||
|
// },
|
||||||
|
};
|
10
.storybook/tsconfig.json
Normal file
10
.storybook/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"exclude": [
|
||||||
|
"../**/*.spec.js",
|
||||||
|
"../**/*.spec.ts",
|
||||||
|
"../**/*.spec.tsx",
|
||||||
|
"../**/*.spec.jsx"
|
||||||
|
],
|
||||||
|
"include": ["../**/*"]
|
||||||
|
}
|
213
CHANGELOG.md
213
CHANGELOG.md
@ -5,6 +5,217 @@ 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.55.0 - 20.09.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed the default value of the data source attribute
|
||||||
|
- Upgraded `@storybook` dependencies
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the create or edit transaction dialog
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn prisma migrate deploy`)
|
||||||
|
|
||||||
|
## 1.54.0 - 18.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the data source attribute to the symbol profile model
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Respected the data source attribute in the data provider service
|
||||||
|
- Respected the data source attribute in the symbol data endpoint
|
||||||
|
- Improved the search functionality of the data management (multiple data sources)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hid the net performance in the _Presenter View_ (portfolio holdings and summary tab on the home page)
|
||||||
|
- Hid the sign if the performance is zero in the value component
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn prisma migrate deploy`)
|
||||||
|
|
||||||
|
## 1.53.0 - 13.09.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the annualized performance calculation
|
||||||
|
- Changed the data gathering selection from distinct orders to symbol profiles
|
||||||
|
|
||||||
|
## 1.52.0 - 11.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the annualized performance to the portfolio summary tab on the home page
|
||||||
|
- Added the Ghostfolio Slack channel to the about page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `3.0.0` to `4.1.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the sign in with fingerprint for some android devices
|
||||||
|
|
||||||
|
## 1.51.0 - 11.09.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Provided the name in the portfolio position endpoint
|
||||||
|
|
||||||
|
## 1.50.0 - 11.09.2021
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the _Fear & Greed Index_ (market mood)
|
||||||
|
- Fixed the overlap of the home button with tabs on iOS (_Add to Home Screen_)
|
||||||
|
|
||||||
|
## 1.49.0 - 08.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added labels to the allocation chart by symbol on desktop
|
||||||
|
|
||||||
|
## 1.48.0 - 07.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the attribute `precision` in the value component
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hid the performance in the _Presenter View_
|
||||||
|
|
||||||
|
## 1.47.1 - 06.09.2021
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the search functionality for cryptocurrency symbols
|
||||||
|
|
||||||
|
## 1.46.0 - 05.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the statistics section on the about page by the _GitHub_ contributors count
|
||||||
|
- Set up _Storybook_
|
||||||
|
- Added a story for the logo component
|
||||||
|
- Added a story for the no transactions info component
|
||||||
|
- Added a story for the trend indicator component
|
||||||
|
- Added a story for the value component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Switched from gross to net performance
|
||||||
|
- Restructured the portfolio summary tab on the home page (fees and net performance)
|
||||||
|
|
||||||
|
## 1.45.0 - 04.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a link below the holdings to manage the transactions
|
||||||
|
- Added the allocation chart by symbol
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Restructured the allocations page
|
||||||
|
- Upgraded `angular` from version `12.0.4` to `12.2.4`
|
||||||
|
- Upgraded `@angular/cdk` and `@angular/material` from version `12.0.6` to `12.2.4`
|
||||||
|
- Upgraded `Nx` from version `12.5.4` to `12.8.0`
|
||||||
|
- Upgraded `prisma` from version `2.24.1` to `2.30.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the value formatting for integers (transactions count)
|
||||||
|
|
||||||
|
## 1.44.0 - 30.08.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Extended the sub classification of assets by cash
|
||||||
|
- Upgraded `svgmap` from version `2.1.1` to `2.6.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Filtered out positions without any quantity in the positions table
|
||||||
|
- Improved the symbol lookup: allow saving with valid symbol in create or edit transaction dialog
|
||||||
|
|
||||||
|
## 1.43.0 - 24.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the data management of symbol profile data by countries (automated for stocks)
|
||||||
|
- Added a fallback for initially loading currencies if historical data is not yet available
|
||||||
|
|
||||||
|
## 1.42.0 - 22.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the subscription type to the users table of the admin control panel
|
||||||
|
- Introduced the sub classification of assets
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn prisma migrate deploy`)
|
||||||
|
|
||||||
|
## 1.41.0 - 21.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a link to the system status page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the wording for the _Restricted View_: _Presenter View_
|
||||||
|
- Improved the styling of the tables
|
||||||
|
- Ignored cash assets in the allocation chart by sector, continent and country
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the allocation chart by account (wrong calculation)
|
||||||
|
- Fixed an issue in the allocation chart by account (missing cash accounts)
|
||||||
|
|
||||||
|
## 1.40.0 - 19.08.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the fault tolerance of the portfolio details endpoint
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the node engine version mismatch in `package.json`
|
||||||
|
- Fixed an issue on the buy date in the position detail dialog
|
||||||
|
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `GBp` to `GBP`)
|
||||||
|
|
||||||
|
## 1.39.0 - 16.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an option to hide absolute values like performances and quantities (_Restricted View_)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Restructured the allocations page
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the performance in the portfolio summary tab on the home page (impersonation mode)
|
||||||
|
- Fixed various values in the impersonation mode which have not been nullified
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed the current net performance
|
||||||
|
- Removed the read foreign portfolio permission
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn prisma migrate deploy`)
|
||||||
|
|
||||||
## 1.38.0 - 14.08.2021
|
## 1.38.0 - 14.08.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -63,7 +274,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Todo
|
### Todo
|
||||||
|
|
||||||
- Apply data migration (`yarn database:push`)
|
- Apply data migration (`yarn prisma migrate deploy`)
|
||||||
|
|
||||||
## 1.34.0 - 07.08.2021
|
## 1.34.0 - 07.08.2021
|
||||||
|
|
||||||
|
10
README.md
10
README.md
@ -12,7 +12,7 @@
|
|||||||
<strong>Open Source Wealth Management Software made for Humans</strong>
|
<strong>Open Source Wealth Management Software made for Humans</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="#contributing">
|
<a href="#contributing">
|
||||||
@ -62,7 +62,7 @@ Ghostfolio is for you if you are...
|
|||||||
|
|
||||||
- ✅ Create, update and delete transactions
|
- ✅ Create, update and delete transactions
|
||||||
- ✅ Multi account management
|
- ✅ Multi account management
|
||||||
- ✅ Portfolio performance (`Today`, `YTD`, `1Y`, `5Y`, `Max`)
|
- ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `YTD`, `1Y`, `5Y`, `Max`
|
||||||
- ✅ Various charts
|
- ✅ Various charts
|
||||||
- ✅ Static analysis to identify potential risks in your portfolio
|
- ✅ Static analysis to identify potential risks in your portfolio
|
||||||
- ✅ Dark Mode
|
- ✅ Dark Mode
|
||||||
@ -116,6 +116,10 @@ Please make sure you have completed the instructions from [_Setup_](#Setup).
|
|||||||
|
|
||||||
Run `yarn start:client`
|
Run `yarn start:client`
|
||||||
|
|
||||||
|
### Start _Storybook_
|
||||||
|
|
||||||
|
Run `yarn start:storybook`
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run `yarn test`
|
Run `yarn test`
|
||||||
@ -124,7 +128,7 @@ Run `yarn test`
|
|||||||
|
|
||||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||||
|
|
||||||
Not sure what to work on? We have got some ideas. Please tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
89
angular.json
89
angular.json
@ -6,13 +6,16 @@
|
|||||||
"defaultProject": "api",
|
"defaultProject": "api",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@nrwl/angular:application": {
|
"@nrwl/angular:application": {
|
||||||
|
"linter": "eslint",
|
||||||
"unitTestRunner": "jest",
|
"unitTestRunner": "jest",
|
||||||
"e2eTestRunner": "cypress"
|
"e2eTestRunner": "cypress"
|
||||||
},
|
},
|
||||||
"@nrwl/angular:library": {
|
"@nrwl/angular:library": {
|
||||||
|
"linter": "eslint",
|
||||||
"unitTestRunner": "jest"
|
"unitTestRunner": "jest"
|
||||||
},
|
},
|
||||||
"@nrwl/nest": {}
|
"@nrwl/nest": {},
|
||||||
|
"@nrwl/angular:component": {}
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"api": {
|
"api": {
|
||||||
@ -239,6 +242,90 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"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.js",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/ui/src/**/*.ts", "libs/ui/src/**/*.html"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"builder": "@nrwl/storybook:storybook",
|
||||||
|
"options": {
|
||||||
|
"uiFramework": "@storybook/angular",
|
||||||
|
"port": 4400,
|
||||||
|
"config": {
|
||||||
|
"configFolder": "libs/ui/.storybook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build-storybook": {
|
||||||
|
"builder": "@nrwl/storybook:build",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"uiFramework": "@storybook/angular",
|
||||||
|
"outputPath": "dist/storybook/ui",
|
||||||
|
"config": {
|
||||||
|
"configFolder": "libs/ui/.storybook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ui-e2e": {
|
||||||
|
"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}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Access } from '@ghostfolio/common/interfaces';
|
import { Access } from '@ghostfolio/common/interfaces';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import {
|
import {
|
||||||
@ -5,7 +6,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -33,7 +34,8 @@ export class AccountController {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly impersonationService: ImpersonationService,
|
private readonly impersonationService: ImpersonationService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ -84,25 +86,22 @@ export class AccountController {
|
|||||||
public async getAllAccounts(
|
public async getAllAccounts(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId
|
||||||
): Promise<AccountModel[]> {
|
): Promise<AccountModel[]> {
|
||||||
const impersonationUserId = await this.impersonationService.validateImpersonationId(
|
const impersonationUserId =
|
||||||
impersonationId,
|
await this.impersonationService.validateImpersonationId(
|
||||||
this.request.user.id
|
impersonationId,
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let accounts = await this.accountService.getAccounts(
|
||||||
|
impersonationUserId || this.request.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
let accounts = await this.accountService.accounts({
|
|
||||||
include: { Order: true, Platform: true },
|
|
||||||
orderBy: { name: 'asc' },
|
|
||||||
where: { userId: impersonationUserId || this.request.user.id }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationUserId &&
|
impersonationUserId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
accounts = nullifyValuesInObjects(accounts, [
|
accounts = nullifyValuesInObjects(accounts, [
|
||||||
|
'balance',
|
||||||
'fee',
|
'fee',
|
||||||
'quantity',
|
'quantity',
|
||||||
'unitPrice'
|
'unitPrice'
|
||||||
|
@ -1,32 +1,26 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
|
||||||
import { AccountController } from './account.controller';
|
import { AccountController } from './account.controller';
|
||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
|
ImpersonationModule,
|
||||||
|
RedisCacheModule,
|
||||||
|
PrismaModule,
|
||||||
|
UserModule
|
||||||
|
],
|
||||||
controllers: [AccountController],
|
controllers: [AccountController],
|
||||||
providers: [
|
providers: [AccountService]
|
||||||
AccountService,
|
|
||||||
AlphaVantageService,
|
|
||||||
ConfigurationService,
|
|
||||||
DataProviderService,
|
|
||||||
ExchangeRateDataService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
ImpersonationService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class AccountModule {}
|
export class AccountModule {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Currency, Order, Prisma } from '@prisma/client';
|
import { Account, Currency, Order, Platform, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { CashDetails } from './interfaces/cash-details.interface';
|
import { CashDetails } from './interfaces/cash-details.interface';
|
||||||
|
|
||||||
@ -41,7 +41,12 @@ export class AccountService {
|
|||||||
cursor?: Prisma.AccountWhereUniqueInput;
|
cursor?: Prisma.AccountWhereUniqueInput;
|
||||||
where?: Prisma.AccountWhereInput;
|
where?: Prisma.AccountWhereInput;
|
||||||
orderBy?: Prisma.AccountOrderByInput;
|
orderBy?: Prisma.AccountOrderByInput;
|
||||||
}): Promise<Account[]> {
|
}): Promise<
|
||||||
|
(Account & {
|
||||||
|
Order?: Order[];
|
||||||
|
Platform?: Platform;
|
||||||
|
})[]
|
||||||
|
> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prismaService.account.findMany({
|
return this.prismaService.account.findMany({
|
||||||
@ -72,6 +77,22 @@ export class AccountService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getAccounts(aUserId: string) {
|
||||||
|
const accounts = await this.accounts({
|
||||||
|
include: { Order: true, Platform: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
where: { userId: aUserId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return accounts.map((account) => {
|
||||||
|
const result = { ...account, transactionCount: account.Order.length };
|
||||||
|
|
||||||
|
delete result.Order;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getCashDetails(
|
public async getCashDetails(
|
||||||
aUserId: string,
|
aUserId: string,
|
||||||
aCurrency: Currency
|
aCurrency: Currency
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
@ -1,31 +1,25 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
|
PrismaModule,
|
||||||
|
SubscriptionModule
|
||||||
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [
|
providers: [AdminService],
|
||||||
AdminService,
|
exports: [AdminService]
|
||||||
AlphaVantageService,
|
|
||||||
ConfigurationService,
|
|
||||||
DataGatheringService,
|
|
||||||
DataProviderService,
|
|
||||||
ExchangeRateDataService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
@ -9,9 +11,11 @@ import { differenceInDays } from 'date-fns';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly subscriptionService: SubscriptionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(): Promise<AdminData> {
|
public async get(): Promise<AdminData> {
|
||||||
@ -107,7 +111,8 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
id: true
|
id: true,
|
||||||
|
Subscription: true
|
||||||
},
|
},
|
||||||
take: 30,
|
take: 30,
|
||||||
where: {
|
where: {
|
||||||
@ -118,16 +123,23 @@ export class AdminService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return usersWithAnalytics.map(
|
return usersWithAnalytics.map(
|
||||||
({ _count, alias, Analytics, createdAt, id }) => {
|
({ _count, alias, Analytics, createdAt, id, Subscription }) => {
|
||||||
const daysSinceRegistration =
|
const daysSinceRegistration =
|
||||||
differenceInDays(new Date(), createdAt) + 1;
|
differenceInDays(new Date(), createdAt) + 1;
|
||||||
const engagement = Analytics.activityCount / daysSinceRegistration;
|
const engagement = Analytics.activityCount / daysSinceRegistration;
|
||||||
|
|
||||||
|
const subscription = this.configurationService.get(
|
||||||
|
'ENABLE_FEATURE_SUBSCRIPTION'
|
||||||
|
)
|
||||||
|
? this.subscriptionService.getSubscription(Subscription)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alias,
|
alias,
|
||||||
createdAt,
|
createdAt,
|
||||||
engagement,
|
engagement,
|
||||||
id,
|
id,
|
||||||
|
subscription,
|
||||||
accountCount: _count.Account || 0,
|
accountCount: _count.Account || 0,
|
||||||
lastActivity: Analytics.updatedAt,
|
lastActivity: Analytics.updatedAt,
|
||||||
transactionCount: _count.Order || 0
|
transactionCount: _count.Order || 0
|
||||||
|
@ -1,35 +1,30 @@
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { AuthDeviceModule } from '@ghostfolio/api/app/auth-device/auth-device.module';
|
import { AuthDeviceModule } from '@ghostfolio/api/app/auth-device/auth-device.module';
|
||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
|
import { CronService } from '@ghostfolio/api/services/cron.service';
|
||||||
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
|
||||||
import { CronService } from '../services/cron.service';
|
|
||||||
import { DataGatheringService } from '../services/data-gathering.service';
|
|
||||||
import { DataProviderService } from '../services/data-provider.service';
|
|
||||||
import { AlphaVantageService } from '../services/data-provider/alpha-vantage/alpha-vantage.service';
|
|
||||||
import { GhostfolioScraperApiService } from '../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
|
||||||
import { PrismaService } from '../services/prisma.service';
|
|
||||||
import { AccessModule } from './access/access.module';
|
import { AccessModule } from './access/access.module';
|
||||||
import { AccountModule } from './account/account.module';
|
import { AccountModule } from './account/account.module';
|
||||||
import { AdminModule } from './admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { CacheModule } from './cache/cache.module';
|
import { CacheModule } from './cache/cache.module';
|
||||||
import { CoreModule } from './core/core.module';
|
|
||||||
import { ExperimentalModule } from './experimental/experimental.module';
|
import { ExperimentalModule } from './experimental/experimental.module';
|
||||||
import { ExportModule } from './export/export.module';
|
import { ExportModule } from './export/export.module';
|
||||||
import { ImportModule } from './import/import.module';
|
import { ImportModule } from './import/import.module';
|
||||||
import { InfoModule } from './info/info.module';
|
import { InfoModule } from './info/info.module';
|
||||||
import { OrderModule } from './order/order.module';
|
import { OrderModule } from './order/order.module';
|
||||||
import { PortfolioModule } from './portfolio/portfolio.module';
|
import { PortfolioModule } from './portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
|
||||||
import { SubscriptionModule } from './subscription/subscription.module';
|
import { SubscriptionModule } from './subscription/subscription.module';
|
||||||
import { SymbolModule } from './symbol/symbol.module';
|
import { SymbolModule } from './symbol/symbol.module';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
@ -43,13 +38,17 @@ import { UserModule } from './user/user.module';
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
CoreModule,
|
ConfigurationModule,
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
ExperimentalModule,
|
ExperimentalModule,
|
||||||
ExportModule,
|
ExportModule,
|
||||||
ImportModule,
|
ImportModule,
|
||||||
InfoModule,
|
InfoModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
@ -71,17 +70,6 @@ import { UserModule } from './user/user.module';
|
|||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [CronService]
|
||||||
AlphaVantageService,
|
|
||||||
ConfigurationService,
|
|
||||||
CronService,
|
|
||||||
DataGatheringService,
|
|
||||||
DataProviderService,
|
|
||||||
ExchangeRateDataService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
|
@ -62,10 +62,10 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('webauthn/generate-attestation-options')
|
@Get('webauthn/generate-registration-options')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async generateAttestationOptions() {
|
public async generateRegistrationOptions() {
|
||||||
return this.webAuthService.generateAttestationOptions();
|
return this.webAuthService.generateRegistrationOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('webauthn/verify-attestation')
|
@Post('webauthn/verify-attestation')
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||||
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
|
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
|
||||||
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
import { UserService } from '../user/user.service';
|
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { GoogleStrategy } from './google.strategy';
|
import { GoogleStrategy } from './google.strategy';
|
||||||
@ -17,7 +18,8 @@ import { JwtStrategy } from './jwt.strategy';
|
|||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET_KEY,
|
secret: process.env.JWT_SECRET_KEY,
|
||||||
signOptions: { expiresIn: '180 days' }
|
signOptions: { expiresIn: '180 days' }
|
||||||
})
|
}),
|
||||||
|
SubscriptionModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AuthDeviceService,
|
AuthDeviceService,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
|
||||||
import { UserService } from '../user/user.service';
|
|
||||||
import { ValidateOAuthLoginParams } from './interfaces/interfaces';
|
import { ValidateOAuthLoginParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
|
||||||
import { UserService } from '../user/user.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
|
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
|
||||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
@ -10,19 +11,18 @@ import {
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import {
|
import {
|
||||||
GenerateAssertionOptionsOpts,
|
GenerateAuthenticationOptionsOpts,
|
||||||
GenerateAttestationOptionsOpts,
|
GenerateRegistrationOptionsOpts,
|
||||||
VerifiedAssertion,
|
VerifiedAuthenticationResponse,
|
||||||
VerifiedAttestation,
|
VerifiedRegistrationResponse,
|
||||||
VerifyAssertionResponseOpts,
|
VerifyAuthenticationResponseOpts,
|
||||||
VerifyAttestationResponseOpts,
|
VerifyRegistrationResponseOpts,
|
||||||
generateAssertionOptions,
|
generateAuthenticationOptions,
|
||||||
generateAttestationOptions,
|
generateRegistrationOptions,
|
||||||
verifyAssertionResponse,
|
verifyAuthenticationResponse,
|
||||||
verifyAttestationResponse
|
verifyRegistrationResponse
|
||||||
} from '@simplewebauthn/server';
|
} from '@simplewebauthn/server';
|
||||||
|
|
||||||
import { UserService } from '../user/user.service';
|
|
||||||
import {
|
import {
|
||||||
AssertionCredentialJSON,
|
AssertionCredentialJSON,
|
||||||
AttestationCredentialJSON
|
AttestationCredentialJSON
|
||||||
@ -46,10 +46,10 @@ export class WebAuthService {
|
|||||||
return this.configurationService.get('ROOT_URL');
|
return this.configurationService.get('ROOT_URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generateAttestationOptions() {
|
public async generateRegistrationOptions() {
|
||||||
const user = this.request.user;
|
const user = this.request.user;
|
||||||
|
|
||||||
const opts: GenerateAttestationOptionsOpts = {
|
const opts: GenerateRegistrationOptionsOpts = {
|
||||||
rpName: 'Ghostfolio',
|
rpName: 'Ghostfolio',
|
||||||
rpID: this.rpID,
|
rpID: this.rpID,
|
||||||
userID: user.id,
|
userID: user.id,
|
||||||
@ -63,7 +63,7 @@ export class WebAuthService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = generateAttestationOptions(opts);
|
const options = generateRegistrationOptions(opts);
|
||||||
|
|
||||||
await this.userService.updateUser({
|
await this.userService.updateUser({
|
||||||
data: {
|
data: {
|
||||||
@ -84,27 +84,27 @@ export class WebAuthService {
|
|||||||
const user = this.request.user;
|
const user = this.request.user;
|
||||||
const expectedChallenge = user.authChallenge;
|
const expectedChallenge = user.authChallenge;
|
||||||
|
|
||||||
let verification: VerifiedAttestation;
|
let verification: VerifiedRegistrationResponse;
|
||||||
try {
|
try {
|
||||||
const opts: VerifyAttestationResponseOpts = {
|
const opts: VerifyRegistrationResponseOpts = {
|
||||||
credential,
|
credential,
|
||||||
expectedChallenge,
|
expectedChallenge,
|
||||||
expectedOrigin: this.expectedOrigin,
|
expectedOrigin: this.expectedOrigin,
|
||||||
expectedRPID: this.rpID
|
expectedRPID: this.rpID
|
||||||
};
|
};
|
||||||
verification = await verifyAttestationResponse(opts);
|
verification = await verifyRegistrationResponse(opts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new InternalServerErrorException(error.message);
|
throw new InternalServerErrorException(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verified, attestationInfo } = verification;
|
const { registrationInfo, verified } = verification;
|
||||||
|
|
||||||
const devices = await this.deviceService.authDevices({
|
const devices = await this.deviceService.authDevices({
|
||||||
where: { userId: user.id }
|
where: { userId: user.id }
|
||||||
});
|
});
|
||||||
if (verified && attestationInfo) {
|
if (registrationInfo && verified) {
|
||||||
const { credentialPublicKey, credentialID, counter } = attestationInfo;
|
const { counter, credentialID, credentialPublicKey } = registrationInfo;
|
||||||
|
|
||||||
let existingDevice = devices.find(
|
let existingDevice = devices.find(
|
||||||
(device) => device.credentialId === credentialID
|
(device) => device.credentialId === credentialID
|
||||||
@ -115,9 +115,9 @@ export class WebAuthService {
|
|||||||
* Add the returned device to the user's list of devices
|
* Add the returned device to the user's list of devices
|
||||||
*/
|
*/
|
||||||
existingDevice = await this.deviceService.createAuthDevice({
|
existingDevice = await this.deviceService.createAuthDevice({
|
||||||
|
counter,
|
||||||
credentialPublicKey,
|
credentialPublicKey,
|
||||||
credentialId: credentialID,
|
credentialId: credentialID,
|
||||||
counter,
|
|
||||||
User: { connect: { id: user.id } }
|
User: { connect: { id: user.id } }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -138,20 +138,20 @@ export class WebAuthService {
|
|||||||
throw new Error('Device not found');
|
throw new Error('Device not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts: GenerateAssertionOptionsOpts = {
|
const opts: GenerateAuthenticationOptionsOpts = {
|
||||||
timeout: 60000,
|
|
||||||
allowCredentials: [
|
allowCredentials: [
|
||||||
{
|
{
|
||||||
id: device.credentialId,
|
id: device.credentialId,
|
||||||
type: 'public-key',
|
transports: ['internal'],
|
||||||
transports: ['internal']
|
type: 'public-key'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
userVerification: 'preferred',
|
rpID: this.rpID,
|
||||||
rpID: this.rpID
|
timeout: 60000,
|
||||||
|
userVerification: 'preferred'
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = generateAssertionOptions(opts);
|
const options = generateAuthenticationOptions(opts);
|
||||||
|
|
||||||
await this.userService.updateUser({
|
await this.userService.updateUser({
|
||||||
data: {
|
data: {
|
||||||
@ -177,29 +177,29 @@ export class WebAuthService {
|
|||||||
|
|
||||||
const user = await this.userService.user({ id: device.userId });
|
const user = await this.userService.user({ id: device.userId });
|
||||||
|
|
||||||
let verification: VerifiedAssertion;
|
let verification: VerifiedAuthenticationResponse;
|
||||||
try {
|
try {
|
||||||
const opts: VerifyAssertionResponseOpts = {
|
const opts: VerifyAuthenticationResponseOpts = {
|
||||||
credential,
|
credential,
|
||||||
expectedChallenge: `${user.authChallenge}`,
|
|
||||||
expectedOrigin: this.expectedOrigin,
|
|
||||||
expectedRPID: this.rpID,
|
|
||||||
authenticator: {
|
authenticator: {
|
||||||
credentialID: device.credentialId,
|
credentialID: device.credentialId,
|
||||||
credentialPublicKey: device.credentialPublicKey,
|
credentialPublicKey: device.credentialPublicKey,
|
||||||
counter: device.counter
|
counter: device.counter
|
||||||
}
|
},
|
||||||
|
expectedChallenge: `${user.authChallenge}`,
|
||||||
|
expectedOrigin: this.expectedOrigin,
|
||||||
|
expectedRPID: this.rpID
|
||||||
};
|
};
|
||||||
verification = verifyAssertionResponse(opts);
|
verification = verifyAuthenticationResponse(opts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new InternalServerErrorException({ error: error.message });
|
throw new InternalServerErrorException({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verified, assertionInfo } = verification;
|
const { verified, authenticationInfo } = verification;
|
||||||
|
|
||||||
if (verified) {
|
if (verified) {
|
||||||
device.counter = assertionInfo.newCounter;
|
device.counter = authenticationInfo.newCounter;
|
||||||
|
|
||||||
await this.deviceService.updateAuthDevice({
|
await this.deviceService.updateAuthDevice({
|
||||||
data: device,
|
data: device,
|
||||||
|
7
apps/api/src/app/cache/cache.controller.ts
vendored
7
apps/api/src/app/cache/cache.controller.ts
vendored
@ -1,11 +1,10 @@
|
|||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
import { Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
|
||||||
import { CacheService } from './cache.service';
|
|
||||||
|
|
||||||
@Controller('cache')
|
@Controller('cache')
|
||||||
export class CacheController {
|
export class CacheController {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
6
apps/api/src/app/cache/cache.module.ts
vendored
6
apps/api/src/app/cache/cache.module.ts
vendored
@ -1,16 +1,16 @@
|
|||||||
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
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 { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-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 { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
|
||||||
import { CacheController } from './cache.controller';
|
import { CacheController } from './cache.controller';
|
||||||
import { CacheService } from './cache.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [RedisCacheModule],
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
|
||||||
import { MarketDataService } from './market-data.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
controllers: [],
|
|
||||||
providers: [
|
|
||||||
AlphaVantageService,
|
|
||||||
ConfigurationService,
|
|
||||||
CurrentRateService,
|
|
||||||
DataProviderService,
|
|
||||||
ExchangeRateDataService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
MarketDataService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class CoreModule {}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { DateQuery } from '@ghostfolio/api/app/core/interfaces/date-query.interface';
|
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface GetValuesParams {
|
|
||||||
currencies: { [symbol: string]: Currency };
|
|
||||||
dateQuery: DateQuery;
|
|
||||||
symbols: string[];
|
|
||||||
userCurrency: Currency;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { TransactionPointSymbol } from '@ghostfolio/api/app/core/interfaces/transaction-point-symbol.interface';
|
|
||||||
|
|
||||||
export interface TransactionPoint {
|
|
||||||
date: string;
|
|
||||||
items: TransactionPointSymbol[];
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { baseCurrency, benchmarks } from '@ghostfolio/common/config';
|
import { baseCurrency, benchmarks } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { isApiTokenAuthorized } from '@ghostfolio/common/permissions';
|
import { isApiTokenAuthorized } from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,34 +1,23 @@
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ExperimentalController } from './experimental.controller';
|
import { ExperimentalController } from './experimental.controller';
|
||||||
import { ExperimentalService } from './experimental.service';
|
import { ExperimentalService } from './experimental.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
|
RedisCacheModule,
|
||||||
|
PrismaModule
|
||||||
|
],
|
||||||
controllers: [ExperimentalController],
|
controllers: [ExperimentalController],
|
||||||
providers: [
|
providers: [AccountService, ExperimentalService]
|
||||||
AccountService,
|
|
||||||
AlphaVantageService,
|
|
||||||
ConfigurationService,
|
|
||||||
DataProviderService,
|
|
||||||
ExchangeRateDataService,
|
|
||||||
ExperimentalService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
RulesService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class ExperimentalModule {}
|
export class ExperimentalModule {}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -11,8 +10,7 @@ export class ExperimentalService {
|
|||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService
|
||||||
private readonly rulesService: RulesService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getBenchmark(aSymbol: string) {
|
public async getBenchmark(aSymbol: string) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Export } from '@ghostfolio/common/interfaces';
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
@ -1,32 +1,23 @@
|
|||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ExportController } from './export.controller';
|
import { ExportController } from './export.controller';
|
||||||
import { ExportService } from './export.service';
|
import { ExportService } from './export.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
PrismaModule,
|
||||||
|
RedisCacheModule
|
||||||
|
],
|
||||||
controllers: [ExportController],
|
controllers: [ExportController],
|
||||||
providers: [
|
providers: [CacheService, ExportService]
|
||||||
AlphaVantageService,
|
|
||||||
CacheService,
|
|
||||||
ConfigurationService,
|
|
||||||
DataGatheringService,
|
|
||||||
DataProviderService,
|
|
||||||
ExportService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class ExportModule {}
|
export class ExportModule {}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,34 +1,24 @@
|
|||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ImportController } from './import.controller';
|
import { ImportController } from './import.controller';
|
||||||
import { ImportService } from './import.service';
|
import { ImportService } from './import.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
PrismaModule,
|
||||||
|
RedisCacheModule
|
||||||
|
],
|
||||||
controllers: [ImportController],
|
controllers: [ImportController],
|
||||||
providers: [
|
providers: [CacheService, ImportService, OrderService]
|
||||||
AlphaVantageService,
|
|
||||||
CacheService,
|
|
||||||
ConfigurationService,
|
|
||||||
DataGatheringService,
|
|
||||||
DataProviderService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
ImportService,
|
|
||||||
OrderService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class ImportModule {}
|
export class ImportModule {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
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 { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-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';
|
||||||
|
@ -90,6 +90,27 @@ export class InfoService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async countGitHubContributors(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const get = bent(
|
||||||
|
`https://api.github.com/repos/ghostfolio/ghostfolio/contributors`,
|
||||||
|
'GET',
|
||||||
|
'json',
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
'User-Agent': 'request'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const contributors = await get();
|
||||||
|
return contributors?.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async countGitHubStargazers(): Promise<number> {
|
private async countGitHubStargazers(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const get = bent(
|
||||||
@ -131,11 +152,13 @@ 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 gitHubContributors = await this.countGitHubContributors();
|
||||||
const gitHubStargazers = await this.countGitHubStargazers();
|
const gitHubStargazers = await this.countGitHubStargazers();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeUsers1d,
|
activeUsers1d,
|
||||||
activeUsers30d,
|
activeUsers30d,
|
||||||
|
gitHubContributors,
|
||||||
gitHubStargazers
|
gitHubStargazers
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import {
|
import {
|
||||||
@ -5,7 +6,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -34,7 +35,8 @@ export class OrderController {
|
|||||||
public constructor(
|
public constructor(
|
||||||
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,
|
||||||
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ -89,11 +91,8 @@ export class OrderController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationUserId &&
|
impersonationUserId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']);
|
orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']);
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,28 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CacheService } from '../cache/cache.service';
|
|
||||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
|
||||||
import { OrderController } from './order.controller';
|
import { OrderController } from './order.controller';
|
||||||
import { OrderService } from './order.service';
|
import { OrderService } from './order.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ImpersonationModule,
|
||||||
|
PrismaModule,
|
||||||
|
RedisCacheModule,
|
||||||
|
UserModule
|
||||||
|
],
|
||||||
controllers: [OrderController],
|
controllers: [OrderController],
|
||||||
providers: [
|
providers: [CacheService, OrderService],
|
||||||
AlphaVantageService,
|
exports: [OrderService]
|
||||||
CacheService,
|
|
||||||
ConfigurationService,
|
|
||||||
DataGatheringService,
|
|
||||||
DataProviderService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
ImpersonationService,
|
|
||||||
OrderService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class OrderModule {}
|
export class OrderModule {}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
@ -5,8 +6,6 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { DataSource, Order, Prisma } from '@prisma/client';
|
import { DataSource, Order, Prisma } from '@prisma/client';
|
||||||
import { endOfToday, isAfter } from 'date-fns';
|
import { endOfToday, isAfter } from 'date-fns';
|
||||||
|
|
||||||
import { CacheService } from '../cache/cache.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -57,7 +56,9 @@ export class OrderService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataGatheringService.gatherProfileData([data.symbol]);
|
this.dataGatheringService.gatherProfileData([
|
||||||
|
{ dataSource: data.dataSource, symbol: data.symbol }
|
||||||
|
]);
|
||||||
|
|
||||||
await this.cacheService.flush();
|
await this.cacheService.flush();
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { Currency, MarketData } from '@prisma/client';
|
import { Currency, DataSource, MarketData } from '@prisma/client';
|
||||||
|
|
||||||
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { MarketDataService } from './market-data.service';
|
import { MarketDataService } from './market-data.service';
|
||||||
|
|
||||||
jest.mock('./market-data.service', () => {
|
jest.mock('./market-data.service', () => {
|
||||||
@ -14,6 +14,7 @@ jest.mock('./market-data.service', () => {
|
|||||||
date,
|
date,
|
||||||
symbol,
|
symbol,
|
||||||
createdAt: date,
|
createdAt: date,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584',
|
id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584',
|
||||||
marketPrice: 1847.839966
|
marketPrice: 1847.839966
|
||||||
});
|
});
|
||||||
@ -30,6 +31,7 @@ jest.mock('./market-data.service', () => {
|
|||||||
return Promise.resolve<MarketData[]>([
|
return Promise.resolve<MarketData[]>([
|
||||||
{
|
{
|
||||||
createdAt: dateRangeStart,
|
createdAt: dateRangeStart,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: dateRangeStart,
|
date: dateRangeStart,
|
||||||
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
||||||
marketPrice: 1841.823902,
|
marketPrice: 1841.823902,
|
||||||
@ -37,6 +39,7 @@ jest.mock('./market-data.service', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
createdAt: dateRangeEnd,
|
createdAt: dateRangeEnd,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: dateRangeEnd,
|
date: dateRangeEnd,
|
||||||
id: '082d6893-df27-4c91-8a5d-092e84315b56',
|
id: '082d6893-df27-4c91-8a5d-092e84315b56',
|
||||||
marketPrice: 1847.839966,
|
marketPrice: 1847.839966,
|
||||||
@ -106,11 +109,11 @@ describe('CurrentRateService', () => {
|
|||||||
expect(
|
expect(
|
||||||
await currentRateService.getValues({
|
await currentRateService.getValues({
|
||||||
currencies: { AMZN: Currency.USD },
|
currencies: { AMZN: Currency.USD },
|
||||||
|
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
|
||||||
dateQuery: {
|
dateQuery: {
|
||||||
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
|
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
|
||||||
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
|
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
|
||||||
},
|
},
|
||||||
symbols: ['AMZN'],
|
|
||||||
userCurrency: Currency.CHF
|
userCurrency: Currency.CHF
|
||||||
})
|
})
|
||||||
).toMatchObject([
|
).toMatchObject([
|
@ -1,13 +1,14 @@
|
|||||||
import { GetValueObject } from '@ghostfolio/api/app/core/interfaces/get-value-object.interface';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { GetValueParams } from '@ghostfolio/api/app/core/interfaces/get-value-params.interface';
|
|
||||||
import { GetValuesParams } from '@ghostfolio/api/app/core/interfaces/get-values-params.interface';
|
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { isBefore, isToday } from 'date-fns';
|
import { isBefore, isToday } from 'date-fns';
|
||||||
import { flatten } from 'lodash';
|
import { flatten } from 'lodash';
|
||||||
|
|
||||||
|
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||||
|
import { GetValueParams } from './interfaces/get-value-params.interface';
|
||||||
|
import { GetValuesParams } from './interfaces/get-values-params.interface';
|
||||||
import { MarketDataService } from './market-data.service';
|
import { MarketDataService } from './market-data.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -25,7 +26,9 @@ export class CurrentRateService {
|
|||||||
userCurrency
|
userCurrency
|
||||||
}: GetValueParams): Promise<GetValueObject> {
|
}: GetValueParams): Promise<GetValueObject> {
|
||||||
if (isToday(date)) {
|
if (isToday(date)) {
|
||||||
const dataProviderResult = await this.dataProviderService.get([symbol]);
|
const dataProviderResult = await this.dataProviderService.get([
|
||||||
|
{ symbol, dataSource: DataSource.YAHOO }
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
date: resetHours(date),
|
date: resetHours(date),
|
||||||
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0,
|
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0,
|
||||||
@ -55,8 +58,8 @@ export class CurrentRateService {
|
|||||||
|
|
||||||
public async getValues({
|
public async getValues({
|
||||||
currencies,
|
currencies,
|
||||||
|
dataGatheringItems,
|
||||||
dateQuery,
|
dateQuery,
|
||||||
symbols,
|
|
||||||
userCurrency
|
userCurrency
|
||||||
}: GetValuesParams): Promise<GetValueObject[]> {
|
}: GetValuesParams): Promise<GetValueObject[]> {
|
||||||
const includeToday =
|
const includeToday =
|
||||||
@ -75,24 +78,31 @@ export class CurrentRateService {
|
|||||||
if (includeToday) {
|
if (includeToday) {
|
||||||
const today = resetHours(new Date());
|
const today = resetHours(new Date());
|
||||||
promises.push(
|
promises.push(
|
||||||
this.dataProviderService.get(symbols).then((dataResultProvider) => {
|
this.dataProviderService
|
||||||
const result = [];
|
.get(dataGatheringItems)
|
||||||
for (const symbol of symbols) {
|
.then((dataResultProvider) => {
|
||||||
result.push({
|
const result = [];
|
||||||
symbol,
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
date: today,
|
result.push({
|
||||||
marketPrice: this.exchangeRateDataService.toCurrency(
|
date: today,
|
||||||
dataResultProvider?.[symbol]?.marketPrice ?? 0,
|
marketPrice: this.exchangeRateDataService.toCurrency(
|
||||||
dataResultProvider?.[symbol]?.currency,
|
dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice ??
|
||||||
userCurrency
|
0,
|
||||||
)
|
dataResultProvider?.[dataGatheringItem.symbol]?.currency,
|
||||||
});
|
userCurrency
|
||||||
}
|
),
|
||||||
return result;
|
symbol: dataGatheringItem.symbol
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
||||||
|
return dataGatheringItem.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
this.marketDataService
|
this.marketDataService
|
||||||
.getRange({
|
.getRange({
|
@ -6,6 +6,9 @@ export interface CurrentPositions {
|
|||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
|
netAnnualizedPerformance: Big;
|
||||||
|
netPerformance: Big;
|
||||||
|
netPerformancePercentage: Big;
|
||||||
currentValue: Big;
|
currentValue: Big;
|
||||||
totalInvestment: Big;
|
totalInvestment: Big;
|
||||||
}
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
|
import { DateQuery } from './date-query.interface';
|
||||||
|
|
||||||
|
export interface GetValuesParams {
|
||||||
|
currencies: { [symbol: string]: Currency };
|
||||||
|
dataGatheringItems: IDataGatheringItem[];
|
||||||
|
dateQuery: DateQuery;
|
||||||
|
userCurrency: Currency;
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface PortfolioOrder {
|
export interface PortfolioOrder {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
date: string;
|
date: string;
|
||||||
|
dataSource: DataSource;
|
||||||
|
fee: Big;
|
||||||
name: string;
|
name: string;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
@ -11,6 +11,9 @@ export interface PortfolioPositionDetail {
|
|||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
maxPrice: number;
|
maxPrice: number;
|
||||||
minPrice: number;
|
minPrice: number;
|
||||||
|
name: string;
|
||||||
|
netPerformance: number;
|
||||||
|
netPerformancePercent: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
|
@ -4,5 +4,6 @@ export interface TimelinePeriod {
|
|||||||
date: string;
|
date: string;
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
|
netPerformance: Big;
|
||||||
value: Big;
|
value: Big;
|
||||||
}
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface TransactionPointSymbol {
|
export interface TransactionPointSymbol {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
|
fee: Big;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
quantity: Big;
|
quantity: Big;
|
@ -0,0 +1,6 @@
|
|||||||
|
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
|
||||||
|
|
||||||
|
export interface TransactionPoint {
|
||||||
|
date: string;
|
||||||
|
items: TransactionPointSymbol[];
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,23 +1,14 @@
|
|||||||
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
|
|
||||||
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
|
|
||||||
import { GetValueObject } from '@ghostfolio/api/app/core/interfaces/get-value-object.interface';
|
|
||||||
import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-order.interface';
|
|
||||||
import { TimelinePeriod } from '@ghostfolio/api/app/core/interfaces/timeline-period.interface';
|
|
||||||
import {
|
|
||||||
Accuracy,
|
|
||||||
TimelineSpecification
|
|
||||||
} from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
|
|
||||||
import { TransactionPointSymbol } from '@ghostfolio/api/app/core/interfaces/transaction-point-symbol.interface';
|
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
|
|
||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
addMonths,
|
addMonths,
|
||||||
addYears,
|
addYears,
|
||||||
|
differenceInDays,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -25,7 +16,19 @@ import {
|
|||||||
max,
|
max,
|
||||||
min
|
min
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { flatten } from 'lodash';
|
import { flatten, isNumber } from 'lodash';
|
||||||
|
|
||||||
|
import { CurrentRateService } from './current-rate.service';
|
||||||
|
import { CurrentPositions } from './interfaces/current-positions.interface';
|
||||||
|
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||||
|
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
|
||||||
|
import { TimelinePeriod } from './interfaces/timeline-period.interface';
|
||||||
|
import {
|
||||||
|
Accuracy,
|
||||||
|
TimelineSpecification
|
||||||
|
} from './interfaces/timeline-specification.interface';
|
||||||
|
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
|
||||||
|
import { TransactionPoint } from './interfaces/transaction-point.interface';
|
||||||
|
|
||||||
export class PortfolioCalculator {
|
export class PortfolioCalculator {
|
||||||
private transactionPoints: TransactionPoint[];
|
private transactionPoints: TransactionPoint[];
|
||||||
@ -57,6 +60,8 @@ export class PortfolioCalculator {
|
|||||||
.plus(oldAccumulatedSymbol.quantity);
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
|
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
investment: newQuantity.eq(0)
|
investment: newQuantity.eq(0)
|
||||||
? new Big(0)
|
? new Big(0)
|
||||||
@ -71,6 +76,8 @@ export class PortfolioCalculator {
|
|||||||
} else {
|
} else {
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
|
fee: order.fee,
|
||||||
firstBuyDate: order.date,
|
firstBuyDate: order.date,
|
||||||
investment: unitPrice.mul(order.quantity).mul(factor),
|
investment: unitPrice.mul(order.quantity).mul(factor),
|
||||||
quantity: order.quantity.mul(factor),
|
quantity: order.quantity.mul(factor),
|
||||||
@ -100,6 +107,23 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket,
|
||||||
|
netPerformancePercent
|
||||||
|
}: {
|
||||||
|
daysInMarket: number;
|
||||||
|
netPerformancePercent: Big;
|
||||||
|
}): Big {
|
||||||
|
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||||
|
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||||
|
return new Big(
|
||||||
|
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
|
||||||
|
).minus(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Big(0);
|
||||||
|
}
|
||||||
|
|
||||||
public getTransactionPoints(): TransactionPoint[] {
|
public getTransactionPoints(): TransactionPoint[] {
|
||||||
return this.transactionPoints;
|
return this.transactionPoints;
|
||||||
}
|
}
|
||||||
@ -111,11 +135,14 @@ export class PortfolioCalculator {
|
|||||||
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
|
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
|
||||||
if (!this.transactionPoints?.length) {
|
if (!this.transactionPoints?.length) {
|
||||||
return {
|
return {
|
||||||
|
currentValue: new Big(0),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
positions: [],
|
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
currentValue: new Big(0),
|
netAnnualizedPerformance: new Big(0),
|
||||||
|
netPerformance: new Big(0),
|
||||||
|
netPerformancePercentage: new Big(0),
|
||||||
|
positions: [],
|
||||||
totalInvestment: new Big(0)
|
totalInvestment: new Big(0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -129,12 +156,15 @@ export class PortfolioCalculator {
|
|||||||
let firstTransactionPoint: TransactionPoint = null;
|
let firstTransactionPoint: TransactionPoint = null;
|
||||||
let firstIndex = this.transactionPoints.length;
|
let firstIndex = this.transactionPoints.length;
|
||||||
const dates = [];
|
const dates = [];
|
||||||
const symbols = new Set<string>();
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
const currencies: { [symbol: string]: Currency } = {};
|
const currencies: { [symbol: string]: Currency } = {};
|
||||||
|
|
||||||
dates.push(resetHours(start));
|
dates.push(resetHours(start));
|
||||||
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
||||||
symbols.add(item.symbol);
|
dataGatheringItems.push({
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
symbol: item.symbol
|
||||||
|
});
|
||||||
currencies[item.symbol] = item.currency;
|
currencies[item.symbol] = item.currency;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.transactionPoints.length; i++) {
|
for (let i = 0; i < this.transactionPoints.length; i++) {
|
||||||
@ -154,10 +184,10 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
const marketSymbols = await this.currentRateService.getValues({
|
const marketSymbols = await this.currentRateService.getValues({
|
||||||
currencies,
|
currencies,
|
||||||
|
dataGatheringItems,
|
||||||
dateQuery: {
|
dateQuery: {
|
||||||
in: dates
|
in: dates
|
||||||
},
|
},
|
||||||
symbols: Array.from(symbols),
|
|
||||||
userCurrency: this.currency
|
userCurrency: this.currency
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -180,7 +210,9 @@ export class PortfolioCalculator {
|
|||||||
const startString = format(start, DATE_FORMAT);
|
const startString = format(start, DATE_FORMAT);
|
||||||
|
|
||||||
const holdingPeriodReturns: { [symbol: string]: Big } = {};
|
const holdingPeriodReturns: { [symbol: string]: Big } = {};
|
||||||
|
const netHoldingPeriodReturns: { [symbol: string]: Big } = {};
|
||||||
const grossPerformance: { [symbol: string]: Big } = {};
|
const grossPerformance: { [symbol: string]: Big } = {};
|
||||||
|
const netPerformance: { [symbol: string]: Big } = {};
|
||||||
const todayString = format(today, DATE_FORMAT);
|
const todayString = format(today, DATE_FORMAT);
|
||||||
|
|
||||||
if (firstIndex > 0) {
|
if (firstIndex > 0) {
|
||||||
@ -189,6 +221,7 @@ export class PortfolioCalculator {
|
|||||||
const invalidSymbols = [];
|
const invalidSymbols = [];
|
||||||
const lastInvestments: { [symbol: string]: Big } = {};
|
const lastInvestments: { [symbol: string]: Big } = {};
|
||||||
const lastQuantities: { [symbol: string]: Big } = {};
|
const lastQuantities: { [symbol: string]: Big } = {};
|
||||||
|
const lastFees: { [symbol: string]: Big } = {};
|
||||||
const initialValues: { [symbol: string]: Big } = {};
|
const initialValues: { [symbol: string]: Big } = {};
|
||||||
|
|
||||||
for (let i = firstIndex; i < this.transactionPoints.length; i++) {
|
for (let i = firstIndex; i < this.transactionPoints.length; i++) {
|
||||||
@ -201,10 +234,6 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
const items = this.transactionPoints[i].items;
|
const items = this.transactionPoints[i].items;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
let oldHoldingPeriodReturn = holdingPeriodReturns[item.symbol];
|
|
||||||
if (!oldHoldingPeriodReturn) {
|
|
||||||
oldHoldingPeriodReturn = new Big(1);
|
|
||||||
}
|
|
||||||
if (!marketSymbolMap[nextDate]?.[item.symbol]) {
|
if (!marketSymbolMap[nextDate]?.[item.symbol]) {
|
||||||
invalidSymbols.push(item.symbol);
|
invalidSymbols.push(item.symbol);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
@ -223,6 +252,13 @@ export class PortfolioCalculator {
|
|||||||
const itemValue = marketSymbolMap[currentDate]?.[item.symbol];
|
const itemValue = marketSymbolMap[currentDate]?.[item.symbol];
|
||||||
let initialValue = itemValue?.mul(lastQuantity);
|
let initialValue = itemValue?.mul(lastQuantity);
|
||||||
let investedValue = itemValue?.mul(item.quantity);
|
let investedValue = itemValue?.mul(item.quantity);
|
||||||
|
const isFirstOrderAndIsStartBeforeCurrentDate =
|
||||||
|
i === firstIndex &&
|
||||||
|
isBefore(parseDate(this.transactionPoints[i].date), start);
|
||||||
|
const lastFee: Big = lastFees[item.symbol] ?? new Big(0);
|
||||||
|
const fee = isFirstOrderAndIsStartBeforeCurrentDate
|
||||||
|
? new Big(0)
|
||||||
|
: item.fee.minus(lastFee);
|
||||||
if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) {
|
if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) {
|
||||||
initialValue = item.investment;
|
initialValue = item.investment;
|
||||||
investedValue = item.investment;
|
investedValue = item.investment;
|
||||||
@ -246,18 +282,26 @@ export class PortfolioCalculator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow));
|
const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow));
|
||||||
holdingPeriodReturns[item.symbol] =
|
holdingPeriodReturns[item.symbol] = (
|
||||||
oldHoldingPeriodReturn.mul(holdingPeriodReturn);
|
holdingPeriodReturns[item.symbol] ?? new Big(1)
|
||||||
let oldGrossPerformance = grossPerformance[item.symbol];
|
).mul(holdingPeriodReturn);
|
||||||
if (!oldGrossPerformance) {
|
grossPerformance[item.symbol] = (
|
||||||
oldGrossPerformance = new Big(0);
|
grossPerformance[item.symbol] ?? new Big(0)
|
||||||
}
|
).plus(endValue.minus(investedValue));
|
||||||
const currentPerformance = endValue.minus(investedValue);
|
|
||||||
grossPerformance[item.symbol] =
|
const netHoldingPeriodReturn = endValue.div(
|
||||||
oldGrossPerformance.plus(currentPerformance);
|
initialValue.plus(cashFlow).plus(fee)
|
||||||
|
);
|
||||||
|
netHoldingPeriodReturns[item.symbol] = (
|
||||||
|
netHoldingPeriodReturns[item.symbol] ?? new Big(1)
|
||||||
|
).mul(netHoldingPeriodReturn);
|
||||||
|
netPerformance[item.symbol] = (
|
||||||
|
netPerformance[item.symbol] ?? new Big(0)
|
||||||
|
).plus(endValue.minus(investedValue).minus(fee));
|
||||||
}
|
}
|
||||||
lastInvestments[item.symbol] = item.investment;
|
lastInvestments[item.symbol] = item.investment;
|
||||||
lastQuantities[item.symbol] = item.quantity;
|
lastQuantities[item.symbol] = item.quantity;
|
||||||
|
lastFees[item.symbol] = item.fee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +315,7 @@ export class PortfolioCalculator {
|
|||||||
? new Big(0)
|
? new Big(0)
|
||||||
: item.investment.div(item.quantity),
|
: item.investment.div(item.quantity),
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
|
dataSource: item.dataSource,
|
||||||
firstBuyDate: item.firstBuyDate,
|
firstBuyDate: item.firstBuyDate,
|
||||||
grossPerformance: isValid
|
grossPerformance: isValid
|
||||||
? grossPerformance[item.symbol] ?? null
|
? grossPerformance[item.symbol] ?? null
|
||||||
@ -281,15 +326,17 @@ export class PortfolioCalculator {
|
|||||||
: null,
|
: null,
|
||||||
investment: item.investment,
|
investment: item.investment,
|
||||||
marketPrice: marketValue?.toNumber() ?? null,
|
marketPrice: marketValue?.toNumber() ?? null,
|
||||||
|
netPerformance: isValid ? netPerformance[item.symbol] ?? null : null,
|
||||||
|
netPerformancePercentage:
|
||||||
|
isValid && netHoldingPeriodReturns[item.symbol]
|
||||||
|
? netHoldingPeriodReturns[item.symbol].minus(1)
|
||||||
|
: null,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
transactionCount: item.transactionCount
|
transactionCount: item.transactionCount
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const overall = this.calculateOverallGrossPerformance(
|
const overall = this.calculateOverallPerformance(positions, initialValues);
|
||||||
positions,
|
|
||||||
initialValues
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...overall,
|
...overall,
|
||||||
@ -377,7 +424,7 @@ export class PortfolioCalculator {
|
|||||||
return flatten(timelinePeriods);
|
return flatten(timelinePeriods);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateOverallGrossPerformance(
|
private calculateOverallPerformance(
|
||||||
positions: TimelinePosition[],
|
positions: TimelinePosition[],
|
||||||
initialValues: { [p: string]: Big }
|
initialValues: { [p: string]: Big }
|
||||||
) {
|
) {
|
||||||
@ -386,7 +433,14 @@ export class PortfolioCalculator {
|
|||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let grossPerformance = new Big(0);
|
let grossPerformance = new Big(0);
|
||||||
let grossPerformancePercentage = new Big(0);
|
let grossPerformancePercentage = new Big(0);
|
||||||
|
let netPerformance = new Big(0);
|
||||||
|
let netPerformancePercentage = new Big(0);
|
||||||
let completeInitialValue = new Big(0);
|
let completeInitialValue = new Big(0);
|
||||||
|
let netAnnualizedPerformance = new Big(0);
|
||||||
|
|
||||||
|
// use Date.now() to use the mock for today
|
||||||
|
const today = new Date(Date.now());
|
||||||
|
|
||||||
for (const currentPosition of positions) {
|
for (const currentPosition of positions) {
|
||||||
if (currentPosition.marketPrice) {
|
if (currentPosition.marketPrice) {
|
||||||
currentValue = currentValue.add(
|
currentValue = currentValue.add(
|
||||||
@ -400,6 +454,7 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance = grossPerformance.plus(
|
grossPerformance = grossPerformance.plus(
|
||||||
currentPosition.grossPerformance
|
currentPosition.grossPerformance
|
||||||
);
|
);
|
||||||
|
netPerformance = netPerformance.plus(currentPosition.netPerformance);
|
||||||
} else if (!currentPosition.quantity.eq(0)) {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
@ -413,6 +468,18 @@ export class PortfolioCalculator {
|
|||||||
grossPerformancePercentage = grossPerformancePercentage.plus(
|
grossPerformancePercentage = grossPerformancePercentage.plus(
|
||||||
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
||||||
);
|
);
|
||||||
|
netAnnualizedPerformance = netAnnualizedPerformance.plus(
|
||||||
|
this.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: differenceInDays(
|
||||||
|
today,
|
||||||
|
parseDate(currentPosition.firstBuyDate)
|
||||||
|
),
|
||||||
|
netPerformancePercent: currentPosition.netPerformancePercentage
|
||||||
|
}).mul(currentInitialValue)
|
||||||
|
);
|
||||||
|
netPerformancePercentage = netPerformancePercentage.plus(
|
||||||
|
currentPosition.netPerformancePercentage.mul(currentInitialValue)
|
||||||
|
);
|
||||||
} else if (!currentPosition.quantity.eq(0)) {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
console.error(
|
console.error(
|
||||||
`Initial value is missing for symbol ${currentPosition.symbol}`
|
`Initial value is missing for symbol ${currentPosition.symbol}`
|
||||||
@ -424,6 +491,10 @@ export class PortfolioCalculator {
|
|||||||
if (!completeInitialValue.eq(0)) {
|
if (!completeInitialValue.eq(0)) {
|
||||||
grossPerformancePercentage =
|
grossPerformancePercentage =
|
||||||
grossPerformancePercentage.div(completeInitialValue);
|
grossPerformancePercentage.div(completeInitialValue);
|
||||||
|
netPerformancePercentage =
|
||||||
|
netPerformancePercentage.div(completeInitialValue);
|
||||||
|
netAnnualizedPerformance =
|
||||||
|
netAnnualizedPerformance.div(completeInitialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -431,6 +502,9 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
|
netAnnualizedPerformance,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercentage,
|
||||||
totalInvestment
|
totalInvestment
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -441,30 +515,35 @@ export class PortfolioCalculator {
|
|||||||
endDate: Date
|
endDate: Date
|
||||||
): Promise<TimelinePeriod[]> {
|
): Promise<TimelinePeriod[]> {
|
||||||
let investment: Big = new Big(0);
|
let investment: Big = new Big(0);
|
||||||
|
let fees: Big = new Big(0);
|
||||||
|
|
||||||
const marketSymbolMap: {
|
const marketSymbolMap: {
|
||||||
[date: string]: { [symbol: string]: Big };
|
[date: string]: { [symbol: string]: Big };
|
||||||
} = {};
|
} = {};
|
||||||
if (j >= 0) {
|
if (j >= 0) {
|
||||||
const currencies: { [name: string]: Currency } = {};
|
const currencies: { [name: string]: Currency } = {};
|
||||||
const symbols: string[] = [];
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
|
|
||||||
for (const item of this.transactionPoints[j].items) {
|
for (const item of this.transactionPoints[j].items) {
|
||||||
currencies[item.symbol] = item.currency;
|
currencies[item.symbol] = item.currency;
|
||||||
symbols.push(item.symbol);
|
dataGatheringItems.push({
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
symbol: item.symbol
|
||||||
|
});
|
||||||
investment = investment.add(item.investment);
|
investment = investment.add(item.investment);
|
||||||
|
fees = fees.add(item.fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
let marketSymbols: GetValueObject[] = [];
|
let marketSymbols: GetValueObject[] = [];
|
||||||
if (symbols.length > 0) {
|
if (dataGatheringItems.length > 0) {
|
||||||
try {
|
try {
|
||||||
marketSymbols = await this.currentRateService.getValues({
|
marketSymbols = await this.currentRateService.getValues({
|
||||||
|
currencies,
|
||||||
|
dataGatheringItems,
|
||||||
dateQuery: {
|
dateQuery: {
|
||||||
gte: startDate,
|
gte: startDate,
|
||||||
lt: endOfDay(endDate)
|
lt: endOfDay(endDate)
|
||||||
},
|
},
|
||||||
symbols,
|
|
||||||
currencies,
|
|
||||||
userCurrency: this.currency
|
userCurrency: this.currency
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -489,7 +568,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results: TimelinePeriod[] = [];
|
||||||
for (
|
for (
|
||||||
let currentDate = startDate;
|
let currentDate = startDate;
|
||||||
isBefore(currentDate, endDate);
|
isBefore(currentDate, endDate);
|
||||||
@ -512,11 +591,13 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!invalid) {
|
if (!invalid) {
|
||||||
|
const grossPerformance = value.minus(investment);
|
||||||
const result = {
|
const result = {
|
||||||
date: currentDateAsString,
|
grossPerformance,
|
||||||
grossPerformance: value.minus(investment),
|
|
||||||
investment,
|
investment,
|
||||||
value
|
value,
|
||||||
|
date: currentDateAsString,
|
||||||
|
netPerformance: grossPerformance.minus(fees)
|
||||||
};
|
};
|
||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
@ -1,22 +1,17 @@
|
|||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import {
|
import {
|
||||||
hasNotDefinedValuesInObject,
|
hasNotDefinedValuesInObject,
|
||||||
nullifyValuesInObject
|
nullifyValuesInObject
|
||||||
} from '@ghostfolio/api/helper/object.helper';
|
} from '@ghostfolio/api/helper/object.helper';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
|
||||||
import {
|
import {
|
||||||
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary
|
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 {
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
getPermissions,
|
|
||||||
hasPermission,
|
|
||||||
permissions
|
|
||||||
} from '@ghostfolio/common/permissions';
|
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -30,7 +25,6 @@ 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 Big from 'big.js';
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
@ -45,12 +39,12 @@ import { PortfolioService } from './portfolio.service';
|
|||||||
export class PortfolioController {
|
export class PortfolioController {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly impersonationService: ImpersonationService,
|
private readonly portfolioService: PortfolioService,
|
||||||
private portfolioService: PortfolioService,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/investments')
|
@Get('investments')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async findAll(
|
public async findAll(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId
|
||||||
@ -60,11 +54,8 @@ export class PortfolioController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId &&
|
impersonationId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
const maxInvestment = investments.reduce(
|
const maxInvestment = investments.reduce(
|
||||||
(investment, item) => Math.max(investment, item.investment),
|
(investment, item) => Math.max(investment, item.investment),
|
||||||
@ -105,11 +96,8 @@ export class PortfolioController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId &&
|
impersonationId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
let maxValue = 0;
|
let maxValue = 0;
|
||||||
|
|
||||||
@ -136,44 +124,25 @@ export class PortfolioController {
|
|||||||
@Headers('impersonation-id') impersonationId,
|
@Headers('impersonation-id') impersonationId,
|
||||||
@Query('range') range,
|
@Query('range') range,
|
||||||
@Res() res: Response
|
@Res() res: Response
|
||||||
): Promise<{ [symbol: string]: PortfolioPosition }> {
|
): Promise<PortfolioDetails> {
|
||||||
let details: { [symbol: string]: PortfolioPosition } = {};
|
const { accounts, holdings, hasErrors } =
|
||||||
|
await this.portfolioService.getDetails(impersonationId, range);
|
||||||
|
|
||||||
const impersonationUserId =
|
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||||
await this.impersonationService.validateImpersonationId(
|
|
||||||
impersonationId,
|
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
details = await this.portfolioService.getDetails(
|
|
||||||
impersonationUserId,
|
|
||||||
range
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
res.status(StatusCodes.ACCEPTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotDefinedValuesInObject(details)) {
|
|
||||||
res.status(StatusCodes.ACCEPTED);
|
res.status(StatusCodes.ACCEPTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId &&
|
impersonationId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
const totalInvestment = Object.values(details)
|
const totalInvestment = Object.values(holdings)
|
||||||
.map((portfolioPosition) => {
|
.map((portfolioPosition) => {
|
||||||
return portfolioPosition.investment;
|
return portfolioPosition.investment;
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const totalValue = Object.values(details)
|
const totalValue = Object.values(holdings)
|
||||||
.map((portfolioPosition) => {
|
.map((portfolioPosition) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||||
@ -183,24 +152,21 @@ export class PortfolioController {
|
|||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
for (const [symbol, portfolioPosition] of Object.entries(details)) {
|
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||||
portfolioPosition.grossPerformance = null;
|
portfolioPosition.grossPerformance = null;
|
||||||
portfolioPosition.investment =
|
portfolioPosition.investment =
|
||||||
portfolioPosition.investment / totalInvestment;
|
portfolioPosition.investment / totalInvestment;
|
||||||
|
|
||||||
for (const [account, { current, original }] of Object.entries(
|
|
||||||
portfolioPosition.accounts
|
|
||||||
)) {
|
|
||||||
portfolioPosition.accounts[account].current = current / totalValue;
|
|
||||||
portfolioPosition.accounts[account].original =
|
|
||||||
original / totalInvestment;
|
|
||||||
}
|
|
||||||
|
|
||||||
portfolioPosition.quantity = null;
|
portfolioPosition.quantity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [name, { current, original }] of Object.entries(accounts)) {
|
||||||
|
accounts[name].current = current / totalValue;
|
||||||
|
accounts[name].original = original / totalInvestment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <any>res.json(details);
|
return <any>res.json({ accounts, holdings });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('performance')
|
@Get('performance')
|
||||||
@ -221,15 +187,11 @@ export class PortfolioController {
|
|||||||
|
|
||||||
let performance = performanceInformation.performance;
|
let performance = performanceInformation.performance;
|
||||||
if (
|
if (
|
||||||
impersonationId &&
|
impersonationId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
performance = nullifyValuesInObject(performance, [
|
performance = nullifyValuesInObject(performance, [
|
||||||
'currentGrossPerformance',
|
'currentGrossPerformance',
|
||||||
'currentNetPerformance',
|
|
||||||
'currentValue'
|
'currentValue'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -253,6 +215,20 @@ export class PortfolioController {
|
|||||||
res.status(StatusCodes.ACCEPTED);
|
res.status(StatusCodes.ACCEPTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
impersonationId ||
|
||||||
|
this.userService.isRestrictedView(this.request.user)
|
||||||
|
) {
|
||||||
|
result.positions = result.positions.map((position) => {
|
||||||
|
return nullifyValuesInObject(position, [
|
||||||
|
'grossPerformance',
|
||||||
|
'investment',
|
||||||
|
'netPerformance',
|
||||||
|
'quantity'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return <any>res.json(result);
|
return <any>res.json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,11 +240,8 @@ export class PortfolioController {
|
|||||||
let summary = await this.portfolioService.getSummary(impersonationId);
|
let summary = await this.portfolioService.getSummary(impersonationId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationId &&
|
impersonationId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
summary = nullifyValuesInObject(summary, [
|
summary = nullifyValuesInObject(summary, [
|
||||||
'cash',
|
'cash',
|
||||||
@ -277,6 +250,7 @@ export class PortfolioController {
|
|||||||
'currentNetPerformance',
|
'currentNetPerformance',
|
||||||
'currentValue',
|
'currentValue',
|
||||||
'fees',
|
'fees',
|
||||||
|
'netWorth',
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
'totalSell'
|
'totalSell'
|
||||||
]);
|
]);
|
||||||
@ -298,13 +272,15 @@ export class PortfolioController {
|
|||||||
|
|
||||||
if (position) {
|
if (position) {
|
||||||
if (
|
if (
|
||||||
impersonationId &&
|
impersonationId ||
|
||||||
!hasPermission(
|
this.userService.isRestrictedView(this.request.user)
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
position = nullifyValuesInObject(position, ['grossPerformance']);
|
position = nullifyValuesInObject(position, [
|
||||||
|
'grossPerformance',
|
||||||
|
'investment',
|
||||||
|
'netPerformance',
|
||||||
|
'quantity'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return position;
|
return position;
|
||||||
|
@ -1,50 +1,40 @@
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
import { MarketDataService } from '@ghostfolio/api/app/core/market-data.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { CurrentRateService } from './current-rate.service';
|
||||||
|
import { MarketDataService } from './market-data.service';
|
||||||
import { PortfolioController } from './portfolio.controller';
|
import { PortfolioController } from './portfolio.controller';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
|
ImpersonationModule,
|
||||||
|
OrderModule,
|
||||||
|
PrismaModule,
|
||||||
|
UserModule
|
||||||
|
],
|
||||||
controllers: [PortfolioController],
|
controllers: [PortfolioController],
|
||||||
providers: [
|
providers: [
|
||||||
AccountService,
|
AccountService,
|
||||||
AlphaVantageService,
|
|
||||||
CacheService,
|
|
||||||
CurrentRateService,
|
CurrentRateService,
|
||||||
ConfigurationService,
|
|
||||||
DataGatheringService,
|
|
||||||
DataProviderService,
|
|
||||||
ExchangeRateDataService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
ImpersonationService,
|
|
||||||
MarketDataService,
|
MarketDataService,
|
||||||
OrderService,
|
|
||||||
PortfolioService,
|
PortfolioService,
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
RulesService,
|
RulesService,
|
||||||
SymbolProfileService,
|
SymbolProfileService
|
||||||
UserService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class PortfolioModule {}
|
export class PortfolioModule {}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
|
|
||||||
import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-order.interface';
|
|
||||||
import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
|
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
|
|
||||||
import { PortfolioCalculator } from '@ghostfolio/api/app/core/portfolio-calculator';
|
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||||
|
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
||||||
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator';
|
||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
||||||
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
||||||
@ -15,25 +15,24 @@ import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '@ghostfolio/ap
|
|||||||
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
|
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
|
||||||
import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment';
|
import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment';
|
||||||
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
|
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition
|
TimelinePosition
|
||||||
} 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 {
|
import type {
|
||||||
DateRange,
|
DateRange,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser
|
RequestWithUser
|
||||||
@ -48,6 +47,7 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
|
differenceInDays,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -59,12 +59,13 @@ import {
|
|||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty, isNumber } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
PortfolioPositionDetail
|
PortfolioPositionDetail
|
||||||
} from './interfaces/portfolio-position-detail.interface';
|
} from './interfaces/portfolio-position-detail.interface';
|
||||||
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortfolioService {
|
export class PortfolioService {
|
||||||
@ -147,14 +148,14 @@ export class PortfolioService {
|
|||||||
.map((timelineItem) => ({
|
.map((timelineItem) => ({
|
||||||
date: timelineItem.date,
|
date: timelineItem.date,
|
||||||
marketPrice: timelineItem.value,
|
marketPrice: timelineItem.value,
|
||||||
value: timelineItem.grossPerformance.toNumber()
|
value: timelineItem.netPerformance.toNumber()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDetails(
|
public async getDetails(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ [symbol: string]: PortfolioPosition }> {
|
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId);
|
||||||
|
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
@ -168,7 +169,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (transactionPoints?.length <= 0) {
|
||||||
return {};
|
return { accounts: {}, holdings: {}, hasErrors: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
@ -179,27 +180,29 @@ export class PortfolioService {
|
|||||||
startDate
|
startDate
|
||||||
);
|
);
|
||||||
|
|
||||||
if (currentPositions.hasErrors) {
|
|
||||||
throw new Error('Missing information');
|
|
||||||
}
|
|
||||||
|
|
||||||
const cashDetails = await this.accountService.getCashDetails(
|
const cashDetails = await this.accountService.getCashDetails(
|
||||||
userId,
|
userId,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
|
|
||||||
const result: { [symbol: string]: PortfolioPosition } = {};
|
const holdings: PortfolioDetails['holdings'] = {};
|
||||||
const totalInvestment = currentPositions.totalInvestment.plus(
|
const totalInvestment = currentPositions.totalInvestment.plus(
|
||||||
cashDetails.balance
|
cashDetails.balance
|
||||||
);
|
);
|
||||||
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
||||||
|
|
||||||
|
const dataGatheringItems = currentPositions.positions.map((position) => {
|
||||||
|
return {
|
||||||
|
dataSource: position.dataSource,
|
||||||
|
symbol: position.symbol
|
||||||
|
};
|
||||||
|
});
|
||||||
const symbols = currentPositions.positions.map(
|
const symbols = currentPositions.positions.map(
|
||||||
(position) => position.symbol
|
(position) => position.symbol
|
||||||
);
|
);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(symbols),
|
this.dataProviderService.get(dataGatheringItems),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -212,26 +215,33 @@ export class PortfolioService {
|
|||||||
for (const position of currentPositions.positions) {
|
for (const position of currentPositions.positions) {
|
||||||
portfolioItemsNow[position.symbol] = position;
|
portfolioItemsNow[position.symbol] = position;
|
||||||
}
|
}
|
||||||
const accounts = this.getAccounts(orders, portfolioItemsNow, userCurrency);
|
|
||||||
|
|
||||||
for (const item of currentPositions.positions) {
|
for (const item of currentPositions.positions) {
|
||||||
|
if (item.quantity.lte(0)) {
|
||||||
|
// Ignore positions without any quantity
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const value = item.quantity.mul(item.marketPrice);
|
const value = item.quantity.mul(item.marketPrice);
|
||||||
const symbolProfile = symbolProfileMap[item.symbol];
|
const symbolProfile = symbolProfileMap[item.symbol];
|
||||||
const dataProviderResponse = dataProviderResponses[item.symbol];
|
const dataProviderResponse = dataProviderResponses[item.symbol];
|
||||||
result[item.symbol] = {
|
holdings[item.symbol] = {
|
||||||
accounts,
|
|
||||||
allocationCurrent: value.div(totalValue).toNumber(),
|
allocationCurrent: value.div(totalValue).toNumber(),
|
||||||
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
||||||
assetClass: symbolProfile.assetClass,
|
assetClass: symbolProfile.assetClass,
|
||||||
|
assetSubClass: symbolProfile.assetSubClass,
|
||||||
countries: symbolProfile.countries,
|
countries: symbolProfile.countries,
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
exchange: dataProviderResponse.exchange,
|
exchange: dataProviderResponse.exchange,
|
||||||
grossPerformance: item.grossPerformance.toNumber(),
|
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
||||||
grossPerformancePercent: item.grossPerformancePercentage.toNumber(),
|
grossPerformancePercent:
|
||||||
|
item.grossPerformancePercentage?.toNumber() ?? 0,
|
||||||
investment: item.investment.toNumber(),
|
investment: item.investment.toNumber(),
|
||||||
marketPrice: item.marketPrice,
|
marketPrice: item.marketPrice,
|
||||||
marketState: dataProviderResponse.marketState,
|
marketState: dataProviderResponse.marketState,
|
||||||
name: symbolProfile.name,
|
name: symbolProfile.name,
|
||||||
|
netPerformance: item.netPerformance?.toNumber() ?? 0,
|
||||||
|
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
|
||||||
quantity: item.quantity.toNumber(),
|
quantity: item.quantity.toNumber(),
|
||||||
sectors: symbolProfile.sectors,
|
sectors: symbolProfile.sectors,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
@ -241,13 +251,20 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add a cash position for each currency
|
// TODO: Add a cash position for each currency
|
||||||
result[ghostfolioCashSymbol] = await this.getCashPosition({
|
holdings[ghostfolioCashSymbol] = await this.getCashPosition({
|
||||||
cashDetails,
|
cashDetails,
|
||||||
investment: totalInvestment,
|
investment: totalInvestment,
|
||||||
value: totalValue
|
value: totalValue
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
const accounts = await this.getAccounts(
|
||||||
|
orders,
|
||||||
|
portfolioItemsNow,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
return { accounts, holdings, hasErrors: currentPositions.hasErrors };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPosition(
|
public async getPosition(
|
||||||
@ -272,6 +289,9 @@ export class PortfolioService {
|
|||||||
marketPrice: undefined,
|
marketPrice: undefined,
|
||||||
maxPrice: undefined,
|
maxPrice: undefined,
|
||||||
minPrice: undefined,
|
minPrice: undefined,
|
||||||
|
name: undefined,
|
||||||
|
netPerformance: undefined,
|
||||||
|
netPerformancePercent: undefined,
|
||||||
quantity: undefined,
|
quantity: undefined,
|
||||||
symbol: aSymbol,
|
symbol: aSymbol,
|
||||||
transactionCount: undefined
|
transactionCount: undefined
|
||||||
@ -279,10 +299,13 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const positionCurrency = orders[0].currency;
|
const positionCurrency = orders[0].currency;
|
||||||
|
const name = orders[0].SymbolProfile?.name ?? '';
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
|
fee: new Big(order.fee),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
@ -310,13 +333,14 @@ export class PortfolioService {
|
|||||||
const {
|
const {
|
||||||
averagePrice,
|
averagePrice,
|
||||||
currency,
|
currency,
|
||||||
|
dataSource,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
quantity,
|
quantity,
|
||||||
transactionCount
|
transactionCount
|
||||||
} = position;
|
} = position;
|
||||||
|
|
||||||
// Convert investment and gross performance to currency of user
|
// Convert investment, gross and net performance to currency of user
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const investment = this.exchangeRateDataService.toCurrency(
|
const investment = this.exchangeRateDataService.toCurrency(
|
||||||
position.investment.toNumber(),
|
position.investment.toNumber(),
|
||||||
@ -328,19 +352,24 @@ export class PortfolioService {
|
|||||||
currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
|
const netPerformance = this.exchangeRateDataService.toCurrency(
|
||||||
|
position.netPerformance.toNumber(),
|
||||||
|
currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
[aSymbol],
|
[{ dataSource, symbol: aSymbol }],
|
||||||
'day',
|
'day',
|
||||||
parseISO(firstBuyDate),
|
parseISO(firstBuyDate),
|
||||||
new Date()
|
new Date()
|
||||||
);
|
);
|
||||||
|
|
||||||
const historicalDataArray: HistoricalDataItem[] = [];
|
const historicalDataArray: HistoricalDataItem[] = [];
|
||||||
let maxPrice = orders[0].unitPrice;
|
let maxPrice = Math.max(orders[0].unitPrice, marketPrice);
|
||||||
let minPrice = orders[0].unitPrice;
|
let minPrice = Math.min(orders[0].unitPrice, marketPrice);
|
||||||
|
|
||||||
if (!historicalData[aSymbol][firstBuyDate]) {
|
if (!historicalData?.[aSymbol]?.[firstBuyDate]) {
|
||||||
// Add historical entry for buy date, if no historical data available
|
// Add historical entry for buy date, if no historical data available
|
||||||
historicalDataArray.push({
|
historicalDataArray.push({
|
||||||
averagePrice: orders[0].unitPrice,
|
averagePrice: orders[0].unitPrice,
|
||||||
@ -389,19 +418,24 @@ export class PortfolioService {
|
|||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
|
name,
|
||||||
|
netPerformance,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
averagePrice: averagePrice.toNumber(),
|
averagePrice: averagePrice.toNumber(),
|
||||||
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
|
netPerformancePercent: position.netPerformancePercentage.toNumber(),
|
||||||
quantity: quantity.toNumber(),
|
quantity: quantity.toNumber(),
|
||||||
symbol: aSymbol
|
symbol: aSymbol
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const currentData = await this.dataProviderService.get([aSymbol]);
|
const currentData = await this.dataProviderService.get([
|
||||||
|
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
||||||
|
]);
|
||||||
const marketPrice = currentData[aSymbol]?.marketPrice;
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||||
|
|
||||||
let historicalData = await this.dataProviderService.getHistorical(
|
let historicalData = await this.dataProviderService.getHistorical(
|
||||||
[aSymbol],
|
[{ dataSource: DataSource.YAHOO, symbol: aSymbol }],
|
||||||
'day',
|
'day',
|
||||||
portfolioStart,
|
portfolioStart,
|
||||||
new Date()
|
new Date()
|
||||||
@ -435,6 +469,7 @@ export class PortfolioService {
|
|||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
|
name,
|
||||||
averagePrice: 0,
|
averagePrice: 0,
|
||||||
currency: currentData[aSymbol]?.currency,
|
currency: currentData[aSymbol]?.currency,
|
||||||
firstBuyDate: undefined,
|
firstBuyDate: undefined,
|
||||||
@ -442,6 +477,8 @@ export class PortfolioService {
|
|||||||
grossPerformancePercent: undefined,
|
grossPerformancePercent: undefined,
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
investment: 0,
|
investment: 0,
|
||||||
|
netPerformance: undefined,
|
||||||
|
netPerformancePercent: undefined,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
symbol: aSymbol,
|
symbol: aSymbol,
|
||||||
transactionCount: undefined
|
transactionCount: undefined
|
||||||
@ -480,10 +517,16 @@ export class PortfolioService {
|
|||||||
const positions = currentPositions.positions.filter(
|
const positions = currentPositions.positions.filter(
|
||||||
(item) => !item.quantity.eq(0)
|
(item) => !item.quantity.eq(0)
|
||||||
);
|
);
|
||||||
|
const dataGatheringItem = positions.map((position) => {
|
||||||
|
return {
|
||||||
|
dataSource: position.dataSource,
|
||||||
|
symbol: position.symbol
|
||||||
|
};
|
||||||
|
});
|
||||||
const symbols = positions.map((position) => position.symbol);
|
const symbols = positions.map((position) => position.symbol);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(symbols),
|
this.dataProviderService.get(dataGatheringItem),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -505,6 +548,9 @@ export class PortfolioService {
|
|||||||
investment: new Big(position.investment).toNumber(),
|
investment: new Big(position.investment).toNumber(),
|
||||||
marketState: dataProviderResponses[position.symbol].marketState,
|
marketState: dataProviderResponses[position.symbol].marketState,
|
||||||
name: symbolProfileMap[position.symbol].name,
|
name: symbolProfileMap[position.symbol].name,
|
||||||
|
netPerformance: position.netPerformance?.toNumber() ?? null,
|
||||||
|
netPerformancePercentage:
|
||||||
|
position.netPerformancePercentage?.toNumber() ?? null,
|
||||||
quantity: new Big(position.quantity).toNumber()
|
quantity: new Big(position.quantity).toNumber()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -528,6 +574,7 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
performance: {
|
performance: {
|
||||||
|
annualizedPerformancePercent: 0,
|
||||||
currentGrossPerformance: 0,
|
currentGrossPerformance: 0,
|
||||||
currentGrossPerformancePercent: 0,
|
currentGrossPerformancePercent: 0,
|
||||||
currentNetPerformance: 0,
|
currentNetPerformance: 0,
|
||||||
@ -546,19 +593,25 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasErrors = currentPositions.hasErrors;
|
const hasErrors = currentPositions.hasErrors;
|
||||||
|
const annualizedPerformancePercent =
|
||||||
|
currentPositions.netAnnualizedPerformance.toNumber();
|
||||||
const currentValue = currentPositions.currentValue.toNumber();
|
const currentValue = currentPositions.currentValue.toNumber();
|
||||||
const currentGrossPerformance =
|
const currentGrossPerformance =
|
||||||
currentPositions.grossPerformance.toNumber();
|
currentPositions.grossPerformance.toNumber();
|
||||||
const currentGrossPerformancePercent =
|
const currentGrossPerformancePercent =
|
||||||
currentPositions.grossPerformancePercentage.toNumber();
|
currentPositions.grossPerformancePercentage.toNumber();
|
||||||
|
const currentNetPerformance = currentPositions.netPerformance.toNumber();
|
||||||
|
const currentNetPerformancePercent =
|
||||||
|
currentPositions.netPerformancePercentage.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||||
performance: {
|
performance: {
|
||||||
|
annualizedPerformancePercent,
|
||||||
currentGrossPerformance,
|
currentGrossPerformance,
|
||||||
currentGrossPerformancePercent,
|
currentGrossPerformancePercent,
|
||||||
// TODO: the next two should include fees
|
currentNetPerformance,
|
||||||
currentNetPerformance: currentGrossPerformance,
|
currentNetPerformancePercent,
|
||||||
currentNetPerformancePercent: currentGrossPerformancePercent,
|
|
||||||
currentValue: currentValue
|
currentValue: currentValue
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -609,7 +662,12 @@ export class PortfolioService {
|
|||||||
for (const position of currentPositions.positions) {
|
for (const position of currentPositions.positions) {
|
||||||
portfolioItemsNow[position.symbol] = position;
|
portfolioItemsNow[position.symbol] = position;
|
||||||
}
|
}
|
||||||
const accounts = this.getAccounts(orders, portfolioItemsNow, baseCurrency);
|
const accounts = await this.getAccounts(
|
||||||
|
orders,
|
||||||
|
portfolioItemsNow,
|
||||||
|
baseCurrency,
|
||||||
|
userId
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
accountClusterRisk: await this.rulesService.evaluate(
|
accountClusterRisk: await this.rulesService.evaluate(
|
||||||
@ -668,7 +726,7 @@ export class PortfolioService {
|
|||||||
const currency = this.request.user.Settings.currency;
|
const currency = this.request.user.Settings.currency;
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId);
|
||||||
|
|
||||||
const performanceInformation = await this.getPerformance(userId);
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
||||||
|
|
||||||
const { balance } = await this.accountService.getCashDetails(
|
const { balance } = await this.accountService.getCashDetails(
|
||||||
userId,
|
userId,
|
||||||
@ -709,21 +767,13 @@ export class PortfolioService {
|
|||||||
investment: Big;
|
investment: Big;
|
||||||
value: Big;
|
value: Big;
|
||||||
}) {
|
}) {
|
||||||
const accounts = {};
|
|
||||||
const cashValue = new Big(cashDetails.balance);
|
const cashValue = new Big(cashDetails.balance);
|
||||||
|
|
||||||
cashDetails.accounts.forEach((account) => {
|
|
||||||
accounts[account.name] = {
|
|
||||||
current: account.balance,
|
|
||||||
original: account.balance
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
|
||||||
allocationCurrent: cashValue.div(value).toNumber(),
|
allocationCurrent: cashValue.div(value).toNumber(),
|
||||||
allocationInvestment: cashValue.div(investment).toNumber(),
|
allocationInvestment: cashValue.div(investment).toNumber(),
|
||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.CASH,
|
||||||
|
assetSubClass: AssetClass.CASH,
|
||||||
countries: [],
|
countries: [],
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
grossPerformance: 0,
|
grossPerformance: 0,
|
||||||
@ -732,6 +782,8 @@ export class PortfolioService {
|
|||||||
marketPrice: 0,
|
marketPrice: 0,
|
||||||
marketState: MarketState.open,
|
marketState: MarketState.open,
|
||||||
name: 'Cash',
|
name: 'Cash',
|
||||||
|
netPerformance: 0,
|
||||||
|
netPerformancePercent: 0,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
sectors: [],
|
sectors: [],
|
||||||
symbol: ghostfolioCashSymbol,
|
symbol: ghostfolioCashSymbol,
|
||||||
@ -777,7 +829,15 @@ export class PortfolioService {
|
|||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
|
fee: new Big(
|
||||||
|
this.exchangeRateDataService.toCurrency(
|
||||||
|
order.fee,
|
||||||
|
order.currency,
|
||||||
|
userCurrency
|
||||||
|
)
|
||||||
|
),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
@ -802,41 +862,67 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAccounts(
|
private async getAccounts(
|
||||||
orders: OrderWithAccount[],
|
orders: OrderWithAccount[],
|
||||||
portfolioItemsNow: { [p: string]: TimelinePosition },
|
portfolioItemsNow: { [p: string]: TimelinePosition },
|
||||||
userCurrency
|
userCurrency: Currency,
|
||||||
|
userId: string
|
||||||
) {
|
) {
|
||||||
const accounts: PortfolioPosition['accounts'] = {};
|
const accounts: PortfolioDetails['accounts'] = {};
|
||||||
for (const order of orders) {
|
|
||||||
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
|
||||||
order.quantity * portfolioItemsNow[order.symbol].marketPrice,
|
|
||||||
order.currency,
|
|
||||||
userCurrency
|
|
||||||
);
|
|
||||||
let originalValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
|
||||||
order.quantity * order.unitPrice,
|
|
||||||
order.currency,
|
|
||||||
userCurrency
|
|
||||||
);
|
|
||||||
|
|
||||||
if (order.type === 'SELL') {
|
const currentAccounts = await this.accountService.getAccounts(userId);
|
||||||
currentValueOfSymbol *= -1;
|
|
||||||
originalValueOfSymbol *= -1;
|
for (const account of currentAccounts) {
|
||||||
|
const ordersByAccount = orders.filter(({ accountId }) => {
|
||||||
|
return accountId === account.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ordersByAccount.length <= 0) {
|
||||||
|
// Add account without orders
|
||||||
|
const balance = this.exchangeRateDataService.toCurrency(
|
||||||
|
account.balance,
|
||||||
|
account.currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
accounts[account.name] = {
|
||||||
|
current: balance,
|
||||||
|
original: balance
|
||||||
|
};
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounts[order.Account?.name || UNKNOWN_KEY]?.current) {
|
for (const order of ordersByAccount) {
|
||||||
accounts[order.Account?.name || UNKNOWN_KEY].current +=
|
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
||||||
currentValueOfSymbol;
|
order.quantity * portfolioItemsNow[order.symbol].marketPrice,
|
||||||
accounts[order.Account?.name || UNKNOWN_KEY].original +=
|
order.currency,
|
||||||
originalValueOfSymbol;
|
userCurrency
|
||||||
} else {
|
);
|
||||||
accounts[order.Account?.name || UNKNOWN_KEY] = {
|
let originalValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
||||||
current: currentValueOfSymbol,
|
order.quantity * order.unitPrice,
|
||||||
original: originalValueOfSymbol
|
order.currency,
|
||||||
};
|
userCurrency
|
||||||
|
);
|
||||||
|
|
||||||
|
if (order.type === 'SELL') {
|
||||||
|
currentValueOfSymbol *= -1;
|
||||||
|
originalValueOfSymbol *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accounts[order.Account?.name || UNKNOWN_KEY]?.current) {
|
||||||
|
accounts[order.Account?.name || UNKNOWN_KEY].current +=
|
||||||
|
currentValueOfSymbol;
|
||||||
|
accounts[order.Account?.name || UNKNOWN_KEY].original +=
|
||||||
|
originalValueOfSymbol;
|
||||||
|
} else {
|
||||||
|
accounts[order.Account?.name || UNKNOWN_KEY] = {
|
||||||
|
current: currentValueOfSymbol,
|
||||||
|
original: originalValueOfSymbol
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
import { Rule } from '../models/rule';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RulesService {
|
export class RulesService {
|
||||||
public constructor() {}
|
public constructor() {}
|
@ -1,5 +1,5 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -8,6 +8,7 @@ import { SubscriptionService } from './subscription.service';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
controllers: [SubscriptionController],
|
controllers: [SubscriptionController],
|
||||||
providers: [ConfigurationService, PrismaService, SubscriptionService]
|
providers: [ConfigurationService, PrismaService, SubscriptionService],
|
||||||
|
exports: [SubscriptionService]
|
||||||
})
|
})
|
||||||
export class SubscriptionModule {}
|
export class SubscriptionModule {}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { addDays } from 'date-fns';
|
import { Subscription } from '@prisma/client';
|
||||||
|
import { addDays, isBefore } from 'date-fns';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -86,4 +88,23 @@ export class SubscriptionService {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSubscription(aSubscriptions: Subscription[]) {
|
||||||
|
if (aSubscriptions.length > 0) {
|
||||||
|
const latestSubscription = aSubscriptions.reduce((a, b) => {
|
||||||
|
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
expiresAt: latestSubscription.expiresAt,
|
||||||
|
type: isBefore(new Date(), latestSubscription.expiresAt)
|
||||||
|
? SubscriptionType.Premium
|
||||||
|
: SubscriptionType.Basic
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: SubscriptionType.Basic
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -10,7 +10,9 @@ 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 { DataSource } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||||
import { SymbolItem } from './interfaces/symbol-item.interface';
|
import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||||
@ -45,9 +47,28 @@ export class SymbolController {
|
|||||||
/**
|
/**
|
||||||
* Must be after /lookup
|
* Must be after /lookup
|
||||||
*/
|
*/
|
||||||
@Get(':symbol')
|
@Get(':dataSource/:symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPosition(@Param('symbol') symbol): Promise<SymbolItem> {
|
public async getSymbolData(
|
||||||
return this.symbolService.get(symbol);
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<SymbolItem> {
|
||||||
|
if (!DataSource[dataSource]) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.symbolService.get({ dataSource, symbol });
|
||||||
|
|
||||||
|
if (!result || isEmpty(result)) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,14 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { SymbolController } from './symbol.controller';
|
import { SymbolController } from './symbol.controller';
|
||||||
import { SymbolService } from './symbol.service';
|
import { SymbolService } from './symbol.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [ConfigurationModule, DataProviderModule, PrismaModule],
|
||||||
controllers: [SymbolController],
|
controllers: [SymbolController],
|
||||||
providers: [
|
providers: [SymbolService]
|
||||||
AlphaVantageService,
|
|
||||||
ConfigurationService,
|
|
||||||
DataProviderService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
SymbolService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class SymbolModule {}
|
export class SymbolModule {}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
@ -13,15 +14,19 @@ export class SymbolService {
|
|||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(aSymbol: string): Promise<SymbolItem> {
|
public async get(dataGatheringItem: IDataGatheringItem): Promise<SymbolItem> {
|
||||||
const response = await this.dataProviderService.get([aSymbol]);
|
const response = await this.dataProviderService.get([dataGatheringItem]);
|
||||||
const { currency, dataSource, marketPrice } = response[aSymbol];
|
const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {};
|
||||||
|
|
||||||
return {
|
if (dataGatheringItem.dataSource && marketPrice) {
|
||||||
dataSource,
|
return {
|
||||||
marketPrice,
|
marketPrice,
|
||||||
currency: <Currency>(<unknown>currency)
|
currency: <Currency>(<unknown>currency),
|
||||||
};
|
dataSource: dataGatheringItem.dataSource
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export interface UserSettings {
|
||||||
|
isRestrictedView?: boolean;
|
||||||
|
}
|
6
apps/api/src/app/user/update-user-setting.dto.ts
Normal file
6
apps/api/src/app/user/update-user-setting.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateUserSettingDto {
|
||||||
|
@IsBoolean()
|
||||||
|
isRestrictedView?: boolean;
|
||||||
|
}
|
@ -4,7 +4,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -26,6 +26,8 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
|||||||
|
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||||
|
import { UserSettings } from './interfaces/user-settings.interface';
|
||||||
|
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
||||||
import { UpdateUserSettingsDto } from './update-user-settings.dto';
|
import { UpdateUserSettingsDto } from './update-user-settings.dto';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
@ -78,6 +80,32 @@ export class UserController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put('setting')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.updateUserSettings
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSettings: UserSettings = {
|
||||||
|
...(<UserSettings>this.request.user.Settings.settings),
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.userService.updateUserSetting({
|
||||||
|
userSettings,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Put('settings')
|
@Put('settings')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {
|
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@ -11,9 +12,11 @@ import { UserService } from './user.service';
|
|||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET_KEY,
|
secret: process.env.JWT_SECRET_KEY,
|
||||||
signOptions: { expiresIn: '30 days' }
|
signOptions: { expiresIn: '30 days' }
|
||||||
})
|
}),
|
||||||
|
SubscriptionModule
|
||||||
],
|
],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [ConfigurationService, PrismaService, UserService]
|
providers: [ConfigurationService, PrismaService, UserService],
|
||||||
|
exports: [UserService]
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { locale } from '@ghostfolio/common/config';
|
import { locale } from '@ghostfolio/common/config';
|
||||||
@ -6,9 +7,9 @@ import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
|||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
||||||
import { isBefore } from 'date-fns';
|
|
||||||
|
|
||||||
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||||
|
import { UserSettings } from './interfaces/user-settings.interface';
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
@ -18,7 +19,8 @@ export class UserService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly subscriptionService: SubscriptionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getUser({
|
public async getUser({
|
||||||
@ -50,6 +52,7 @@ export class UserService {
|
|||||||
}),
|
}),
|
||||||
accounts: Account,
|
accounts: Account,
|
||||||
settings: {
|
settings: {
|
||||||
|
...(<UserSettings>Settings.settings),
|
||||||
locale,
|
locale,
|
||||||
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
||||||
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
|
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
|
||||||
@ -57,6 +60,10 @@ export class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isRestrictedView(aUser: UserWithSettings) {
|
||||||
|
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
public async user(
|
public async user(
|
||||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||||
): Promise<UserWithSettings | null> {
|
): Promise<UserWithSettings | null> {
|
||||||
@ -84,6 +91,7 @@ export class UserService {
|
|||||||
// Set default settings if needed
|
// Set default settings if needed
|
||||||
userFromDatabase.Settings = {
|
userFromDatabase.Settings = {
|
||||||
currency: UserService.DEFAULT_CURRENCY,
|
currency: UserService.DEFAULT_CURRENCY,
|
||||||
|
settings: null,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
userId: userFromDatabase?.id,
|
userId: userFromDatabase?.id,
|
||||||
viewMode: ViewMode.DEFAULT
|
viewMode: ViewMode.DEFAULT
|
||||||
@ -91,24 +99,9 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
if (userFromDatabase?.Subscription?.length > 0) {
|
user.subscription = this.subscriptionService.getSubscription(
|
||||||
const latestSubscription = userFromDatabase.Subscription.reduce(
|
userFromDatabase?.Subscription
|
||||||
(a, b) => {
|
);
|
||||||
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
user.subscription = {
|
|
||||||
expiresAt: latestSubscription.expiresAt,
|
|
||||||
type: isBefore(new Date(), latestSubscription.expiresAt)
|
|
||||||
? SubscriptionType.Premium
|
|
||||||
: SubscriptionType.Basic
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
user.subscription = {
|
|
||||||
type: SubscriptionType.Basic
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.subscription.type === SubscriptionType.Basic) {
|
if (user.subscription.type === SubscriptionType.Basic) {
|
||||||
user.permissions = user.permissions.filter((permission) => {
|
user.permissions = user.permissions.filter((permission) => {
|
||||||
@ -219,6 +212,35 @@ export class UserService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateUserSetting({
|
||||||
|
userId,
|
||||||
|
userSettings
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
userSettings: UserSettings;
|
||||||
|
}) {
|
||||||
|
const settings = userSettings as Prisma.JsonObject;
|
||||||
|
|
||||||
|
await this.prismaService.settings.upsert({
|
||||||
|
create: {
|
||||||
|
settings,
|
||||||
|
User: {
|
||||||
|
connect: {
|
||||||
|
id: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
settings
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
userId: userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
public async updateUserSettings({
|
public async updateUserSettings({
|
||||||
currency,
|
currency,
|
||||||
userId,
|
userId,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { groupBy } from '@ghostfolio/common/helper';
|
import { groupBy } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
|
||||||
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
||||||
import { RuleInterface } from './interfaces/rule.interface';
|
import { RuleInterface } from './interfaces/rule.interface';
|
||||||
|
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
PortfolioDetails,
|
||||||
|
PortfolioPosition
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private accounts: {
|
private accounts: PortfolioDetails['accounts']
|
||||||
[account: string]: { current: number; original: number };
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Current Investment'
|
name: 'Current Investment'
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
PortfolioDetails,
|
||||||
|
PortfolioPosition
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
@ -8,9 +11,7 @@ import { Rule } from '../../rule';
|
|||||||
export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
|
export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private accounts: {
|
private accounts: PortfolioDetails['accounts']
|
||||||
[account: string]: { current: number; original: number };
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Initial Investment'
|
name: 'Initial Investment'
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
|
import { PortfolioDetails } from '@ghostfolio/common/interfaces';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
@ -7,9 +8,7 @@ import { Rule } from '../../rule';
|
|||||||
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private accounts: {
|
private accounts: PortfolioDetails['accounts']
|
||||||
[account: string]: { current: number; original: number };
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Single Account'
|
name: 'Single Account'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
8
apps/api/src/services/configuration.module.ts
Normal file
8
apps/api/src/services/configuration.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [ConfigurationService],
|
||||||
|
exports: [ConfigurationService]
|
||||||
|
})
|
||||||
|
export class ConfigurationModule {}
|
12
apps/api/src/services/data-gathering.module.ts
Normal file
12
apps/api/src/services/data-gathering.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigurationModule, DataProviderModule, PrismaModule],
|
||||||
|
providers: [DataGatheringService],
|
||||||
|
exports: [DataGatheringService]
|
||||||
|
})
|
||||||
|
export class DataGatheringModule {}
|
@ -1,15 +1,13 @@
|
|||||||
import { benchmarks, currencyPairs } from '@ghostfolio/common/config';
|
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
benchmarks,
|
||||||
getUtc,
|
currencyPairs,
|
||||||
isGhostfolioScraperApiSymbol,
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
resetHours
|
} from '@ghostfolio/common/config';
|
||||||
} from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getUtc, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
differenceInHours,
|
differenceInHours,
|
||||||
endOfToday,
|
|
||||||
format,
|
format,
|
||||||
getDate,
|
getDate,
|
||||||
getMonth,
|
getMonth,
|
||||||
@ -19,7 +17,7 @@ import {
|
|||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
|
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { ConfigurationService } from './configuration.service';
|
||||||
import { DataProviderService } from './data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from './prisma.service';
|
||||||
@ -38,7 +36,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
if (isDataGatheringNeeded) {
|
if (isDataGatheringNeeded) {
|
||||||
console.log('7d data gathering has been started.');
|
console.log('7d data gathering has been started.');
|
||||||
console.time('7d-data-gathering');
|
console.time('data-gathering-7d');
|
||||||
|
|
||||||
await this.prismaService.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
@ -71,7 +69,7 @@ export class DataGatheringService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('7d data gathering has been completed.');
|
console.log('7d data gathering has been completed.');
|
||||||
console.timeEnd('7d-data-gathering');
|
console.timeEnd('data-gathering-7d');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +80,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
if (!isDataGatheringLocked) {
|
if (!isDataGatheringLocked) {
|
||||||
console.log('Max data gathering has been started.');
|
console.log('Max data gathering has been started.');
|
||||||
console.time('max-data-gathering');
|
console.time('data-gathering-max');
|
||||||
|
|
||||||
await this.prismaService.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
@ -115,33 +113,32 @@ export class DataGatheringService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('Max data gathering has been completed.');
|
console.log('Max data gathering has been completed.');
|
||||||
console.timeEnd('max-data-gathering');
|
console.timeEnd('data-gathering-max');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gatherProfileData(aSymbols?: string[]) {
|
public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) {
|
||||||
console.log('Profile data gathering has been started.');
|
console.log('Profile data gathering has been started.');
|
||||||
console.time('profile-data-gathering');
|
console.time('data-gathering-profile');
|
||||||
|
|
||||||
let symbols = aSymbols;
|
let dataGatheringItems = aDataGatheringItems;
|
||||||
|
|
||||||
if (!symbols) {
|
if (!dataGatheringItems) {
|
||||||
const dataGatheringItems = await this.getSymbolsProfileData();
|
dataGatheringItems = await this.getSymbolsProfileData();
|
||||||
symbols = dataGatheringItems.map((dataGatheringItem) => {
|
|
||||||
return dataGatheringItem.symbol;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentData = await this.dataProviderService.get(symbols);
|
const currentData = await this.dataProviderService.get(dataGatheringItems);
|
||||||
|
|
||||||
for (const [
|
for (const [
|
||||||
symbol,
|
symbol,
|
||||||
{ assetClass, currency, dataSource, name }
|
{ assetClass, assetSubClass, countries, currency, dataSource, name }
|
||||||
] of Object.entries(currentData)) {
|
] of Object.entries(currentData)) {
|
||||||
try {
|
try {
|
||||||
await this.prismaService.symbolProfile.upsert({
|
await this.prismaService.symbolProfile.upsert({
|
||||||
create: {
|
create: {
|
||||||
assetClass,
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
name,
|
name,
|
||||||
@ -149,6 +146,8 @@ export class DataGatheringService {
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
assetClass,
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
countries,
|
||||||
currency,
|
currency,
|
||||||
name
|
name
|
||||||
},
|
},
|
||||||
@ -165,7 +164,7 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Profile data gathering has been completed.');
|
console.log('Profile data gathering has been completed.');
|
||||||
console.timeEnd('profile-data-gathering');
|
console.timeEnd('data-gathering-profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
||||||
@ -207,6 +206,7 @@ export class DataGatheringService {
|
|||||||
try {
|
try {
|
||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: currentDate,
|
date: currentDate,
|
||||||
marketPrice: lastMarketPrice
|
marketPrice: lastMarketPrice
|
||||||
@ -291,7 +291,7 @@ export class DataGatheringService {
|
|||||||
benchmarksToGather.push({
|
benchmarksToGather.push({
|
||||||
dataSource: DataSource.RAKUTEN,
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: startDate,
|
date: startDate,
|
||||||
symbol: 'GF.FEAR_AND_GREED_INDEX'
|
symbol: ghostfolioFearAndGreedIndexSymbol
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,27 +301,20 @@ export class DataGatheringService {
|
|||||||
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
const distinctOrders = await this.prismaService.order.findMany({
|
const symbolProfilesToGather = (
|
||||||
distinct: ['symbol'],
|
await this.prismaService.symbolProfile.findMany({
|
||||||
orderBy: [{ symbol: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: { dataSource: true, symbol: true },
|
select: {
|
||||||
where: {
|
dataSource: true,
|
||||||
date: {
|
symbol: true
|
||||||
lt: endOfToday() // no draft
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const distinctOrdersWithDate: IDataGatheringItem[] = distinctOrders
|
|
||||||
.filter((distinctOrder) => {
|
|
||||||
return !isGhostfolioScraperApiSymbol(distinctOrder.symbol);
|
|
||||||
})
|
})
|
||||||
.map((distinctOrder) => {
|
).map((symbolProfile) => {
|
||||||
return {
|
return {
|
||||||
...distinctOrder,
|
...symbolProfile,
|
||||||
date: startDate
|
date: startDate
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const currencyPairsToGather = currencyPairs.map(
|
const currencyPairsToGather = currencyPairs.map(
|
||||||
({ dataSource, symbol }) => {
|
({ dataSource, symbol }) => {
|
||||||
@ -340,7 +333,7 @@ export class DataGatheringService {
|
|||||||
...this.getBenchmarksToGather(startDate),
|
...this.getBenchmarksToGather(startDate),
|
||||||
...customSymbolsToGather,
|
...customSymbolsToGather,
|
||||||
...currencyPairsToGather,
|
...currencyPairsToGather,
|
||||||
...distinctOrdersWithDate
|
...symbolProfilesToGather
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,22 +353,20 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const distinctOrders = await this.prismaService.order.findMany({
|
const symbolProfilesToGather =
|
||||||
distinct: ['symbol'],
|
await this.prismaService.symbolProfile.findMany({
|
||||||
orderBy: [{ date: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: { dataSource: true, date: true, symbol: true },
|
select: {
|
||||||
where: {
|
dataSource: true,
|
||||||
date: {
|
symbol: true
|
||||||
lt: endOfToday() // no draft
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...this.getBenchmarksToGather(startDate),
|
...this.getBenchmarksToGather(startDate),
|
||||||
...customSymbolsToGather,
|
...customSymbolsToGather,
|
||||||
...currencyPairsToGather,
|
...currencyPairsToGather,
|
||||||
...distinctOrders
|
...symbolProfilesToGather
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { isAfter, isBefore, parse } from 'date-fns';
|
import { isAfter, isBefore, parse } from 'date-fns';
|
||||||
|
|
||||||
import { ConfigurationService } from '../../configuration.service';
|
|
||||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
|
22
apps/api/src/services/data-provider/data-provider.module.ts
Normal file
22
apps/api/src/services/data-provider/data-provider.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
|
||||||
|
import { DataProviderService } from './data-provider.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigurationModule, PrismaModule],
|
||||||
|
providers: [
|
||||||
|
AlphaVantageService,
|
||||||
|
DataProviderService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
],
|
||||||
|
exports: [DataProviderService, GhostfolioScraperApiService]
|
||||||
|
})
|
||||||
|
export class DataProviderModule {}
|
@ -1,25 +1,24 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
IDataGatheringItem,
|
||||||
isGhostfolioScraperApiSymbol,
|
IDataProviderHistoricalResponse,
|
||||||
isRakutenRapidApiSymbol
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
|
||||||
import { AlphaVantageService } from './data-provider/alpha-vantage/alpha-vantage.service';
|
import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
import { RakutenRapidApiService } from './data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from './data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import {
|
import {
|
||||||
IDataGatheringItem,
|
YahooFinanceService,
|
||||||
IDataProviderHistoricalResponse,
|
convertToYahooFinanceSymbol
|
||||||
IDataProviderResponse
|
} from './yahoo-finance/yahoo-finance.service';
|
||||||
} from './interfaces/interfaces';
|
|
||||||
import { PrismaService } from './prisma.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataProviderService {
|
export class DataProviderService {
|
||||||
@ -34,49 +33,32 @@ export class DataProviderService {
|
|||||||
this.rakutenRapidApiService?.setPrisma(this.prismaService);
|
this.rakutenRapidApiService?.setPrisma(this.prismaService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async get(items: IDataGatheringItem[]): Promise<{
|
||||||
aSymbols: string[]
|
[symbol: string]: IDataProviderResponse;
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}> {
|
||||||
if (aSymbols.length === 1) {
|
const response: {
|
||||||
const symbol = aSymbols[0];
|
[symbol: string]: IDataProviderResponse;
|
||||||
|
} = {};
|
||||||
|
|
||||||
if (isGhostfolioScraperApiSymbol(symbol)) {
|
for (const item of items) {
|
||||||
return this.ghostfolioScraperApiService.get(aSymbols);
|
if (item.dataSource === DataSource.ALPHA_VANTAGE) {
|
||||||
} else if (isRakutenRapidApiSymbol(symbol)) {
|
response[item.symbol] = (
|
||||||
return this.rakutenRapidApiService.get(aSymbols);
|
await this.alphaVantageService.get([item.symbol])
|
||||||
}
|
)[item.symbol];
|
||||||
}
|
} else if (item.dataSource === DataSource.GHOSTFOLIO) {
|
||||||
|
response[item.symbol] = (
|
||||||
const yahooFinanceSymbols = aSymbols.filter((symbol) => {
|
await this.ghostfolioScraperApiService.get([item.symbol])
|
||||||
return (
|
)[item.symbol];
|
||||||
!isGhostfolioScraperApiSymbol(symbol) &&
|
} else if (item.dataSource === DataSource.RAKUTEN) {
|
||||||
!isRakutenRapidApiSymbol(symbol)
|
response[item.symbol] = (
|
||||||
);
|
await this.rakutenRapidApiService.get([item.symbol])
|
||||||
});
|
)[item.symbol];
|
||||||
|
} else if (item.dataSource === DataSource.YAHOO) {
|
||||||
const response = await this.yahooFinanceService.get(yahooFinanceSymbols);
|
response[item.symbol] = (
|
||||||
|
await this.yahooFinanceService.get([
|
||||||
const ghostfolioScraperApiSymbols = aSymbols.filter((symbol) => {
|
convertToYahooFinanceSymbol(item.symbol)
|
||||||
return isGhostfolioScraperApiSymbol(symbol);
|
])
|
||||||
});
|
)[item.symbol];
|
||||||
|
|
||||||
for (const symbol of ghostfolioScraperApiSymbols) {
|
|
||||||
if (symbol) {
|
|
||||||
const ghostfolioScraperApiResult =
|
|
||||||
await this.ghostfolioScraperApiService.get([symbol]);
|
|
||||||
response[symbol] = ghostfolioScraperApiResult[symbol];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rakutenRapidApiSymbols = aSymbols.filter((symbol) => {
|
|
||||||
return isRakutenRapidApiSymbol(symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const symbol of rakutenRapidApiSymbols) {
|
|
||||||
if (symbol) {
|
|
||||||
const rakutenRapidApiResult =
|
|
||||||
await this.ghostfolioScraperApiService.get([symbol]);
|
|
||||||
response[symbol] = rakutenRapidApiResult[symbol];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +66,7 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbols: string[],
|
aItems: IDataGatheringItem[],
|
||||||
aGranularity: Granularity = 'month',
|
aGranularity: Granularity = 'month',
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
@ -108,8 +90,17 @@ export class DataProviderService {
|
|||||||
)}'`
|
)}'`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const dataSources = aItems.map((item) => {
|
||||||
|
return item.dataSource;
|
||||||
|
});
|
||||||
|
const symbols = aItems.map((item) => {
|
||||||
|
return item.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryRaw = `SELECT * FROM "MarketData" WHERE "symbol" IN ('${aSymbols.join(
|
const queryRaw = `SELECT * FROM "MarketData" WHERE "dataSource" IN ('${dataSources.join(
|
||||||
|
`','`
|
||||||
|
)}') AND "symbol" IN ('${symbols.join(
|
||||||
`','`
|
`','`
|
||||||
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
|
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
|
||||||
|
|
||||||
@ -168,13 +159,24 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
const { items } = await this.getDataProvider(
|
const promises: Promise<{ items: LookupItem[] }>[] = [];
|
||||||
<DataSource>this.configurationService.get('DATA_SOURCES')[0]
|
let lookupItems: LookupItem[] = [];
|
||||||
).search(aSymbol);
|
|
||||||
|
|
||||||
const filteredItems = items.filter((item) => {
|
for (const dataSource of this.configurationService.get('DATA_SOURCES')) {
|
||||||
|
promises.push(
|
||||||
|
this.getDataProvider(DataSource[dataSource]).search(aSymbol)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResults = await Promise.all(promises);
|
||||||
|
|
||||||
|
searchResults.forEach((searchResult) => {
|
||||||
|
lookupItems = lookupItems.concat(searchResult.items);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredItems = lookupItems.filter((lookupItem) => {
|
||||||
// Only allow symbols with supported currency
|
// Only allow symbols with supported currency
|
||||||
return item.currency ? true : false;
|
return lookupItem.currency ? true : false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
@ -1,4 +1,5 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getYesterday,
|
getYesterday,
|
||||||
@ -18,7 +19,6 @@ import {
|
|||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState
|
MarketState
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
import { PrismaService } from '../../prisma.service';
|
|
||||||
import { ScraperConfig } from './interfaces/scraper-config.interface';
|
import { ScraperConfig } from './interfaces/scraper-config.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getToday,
|
getToday,
|
||||||
@ -11,14 +14,12 @@ import { DataSource } from '@prisma/client';
|
|||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||||
|
|
||||||
import { ConfigurationService } from '../../configuration.service';
|
|
||||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState
|
MarketState
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
import { PrismaService } from '../../prisma.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RakutenRapidApiService implements DataProviderInterface {
|
export class RakutenRapidApiService implements DataProviderInterface {
|
||||||
@ -47,11 +48,11 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = aSymbols[0];
|
const symbol = aSymbols[0];
|
||||||
|
|
||||||
if (symbol === 'GF.FEAR_AND_GREED_INDEX') {
|
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||||
const fgi = await this.getFearAndGreedIndex();
|
const fgi = await this.getFearAndGreedIndex();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'GF.FEAR_AND_GREED_INDEX': {
|
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||||
currency: undefined,
|
currency: undefined,
|
||||||
dataSource: DataSource.RAKUTEN,
|
dataSource: DataSource.RAKUTEN,
|
||||||
marketPrice: fgi.now.value,
|
marketPrice: fgi.now.value,
|
||||||
@ -82,7 +83,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = aSymbols[0];
|
const symbol = aSymbols[0];
|
||||||
|
|
||||||
if (symbol === 'GF.FEAR_AND_GREED_INDEX') {
|
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||||
const fgi = await this.getFearAndGreedIndex();
|
const fgi = await this.getFearAndGreedIndex();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -93,6 +94,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: subWeeks(getToday(), 1),
|
date: subWeeks(getToday(), 1),
|
||||||
marketPrice: fgi.oneWeekAgo.value
|
marketPrice: fgi.oneWeekAgo.value
|
||||||
}
|
}
|
||||||
@ -101,6 +103,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: subMonths(getToday(), 1),
|
date: subMonths(getToday(), 1),
|
||||||
marketPrice: fgi.oneMonthAgo.value
|
marketPrice: fgi.oneMonthAgo.value
|
||||||
}
|
}
|
||||||
@ -109,6 +112,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: subYears(getToday(), 1),
|
date: subYears(getToday(), 1),
|
||||||
marketPrice: fgi.oneYearAgo.value
|
marketPrice: fgi.oneYearAgo.value
|
||||||
}
|
}
|
||||||
@ -118,7 +122,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'GF.FEAR_AND_GREED_INDEX': {
|
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||||
[format(getYesterday(), DATE_FORMAT)]: {
|
[format(getYesterday(), DATE_FORMAT)]: {
|
||||||
marketPrice: fgi.previousClose.value
|
marketPrice: fgi.previousClose.value
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ export interface IYahooFinancePrice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IYahooFinanceSummaryProfile {
|
export interface IYahooFinanceSummaryProfile {
|
||||||
|
country?: string;
|
||||||
industry?: string;
|
industry?: string;
|
||||||
sector?: string;
|
sector?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
|
@ -8,8 +8,15 @@ import {
|
|||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AssetClass, Currency, DataSource } from '@prisma/client';
|
import {
|
||||||
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
|
Currency,
|
||||||
|
DataSource
|
||||||
|
} from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
|
import Big from 'big.js';
|
||||||
|
import { countries } from 'countries-list';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import * as yahooFinance from 'yahoo-finance';
|
import * as yahooFinance from 'yahoo-finance';
|
||||||
|
|
||||||
@ -21,6 +28,7 @@ import {
|
|||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
import {
|
import {
|
||||||
IYahooFinanceHistoricalResponse,
|
IYahooFinanceHistoricalResponse,
|
||||||
|
IYahooFinancePrice,
|
||||||
IYahooFinanceQuoteResponse
|
IYahooFinanceQuoteResponse
|
||||||
} from './interfaces/interfaces';
|
} from './interfaces/interfaces';
|
||||||
|
|
||||||
@ -35,16 +43,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async get(
|
||||||
aSymbols: string[]
|
aYahooFinanceSymbols: string[]
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
if (aSymbols.length <= 0) {
|
if (aYahooFinanceSymbols.length <= 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const yahooSymbols = aSymbols.map((symbol) => {
|
|
||||||
return this.convertToYahooSymbol(symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
|
||||||
@ -52,15 +56,18 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
[symbol: string]: IYahooFinanceQuoteResponse;
|
[symbol: string]: IYahooFinanceQuoteResponse;
|
||||||
} = await yahooFinance.quote({
|
} = await yahooFinance.quote({
|
||||||
modules: ['price', 'summaryProfile'],
|
modules: ['price', 'summaryProfile'],
|
||||||
symbols: yahooSymbols
|
symbols: aYahooFinanceSymbols
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [yahooSymbol, value] of Object.entries(data)) {
|
for (const [yahooFinanceSymbol, value] of Object.entries(data)) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = convertFromYahooSymbol(yahooSymbol);
|
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
||||||
|
|
||||||
|
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
|
||||||
|
|
||||||
response[symbol] = {
|
response[symbol] = {
|
||||||
assetClass: this.parseAssetClass(value.price?.quoteType),
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
currency: parseCurrency(value.price?.currency),
|
currency: parseCurrency(value.price?.currency),
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
exchange: this.parseExchange(value.price?.exchangeName),
|
exchange: this.parseExchange(value.price?.exchangeName),
|
||||||
@ -72,6 +79,33 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
name: value.price?.longName || value.price?.shortName || symbol
|
name: value.price?.longName || value.price?.shortName || symbol
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (value.price?.currency === 'GBp') {
|
||||||
|
// Convert GBp (pence) to GBP
|
||||||
|
response[symbol].currency = Currency.GBP;
|
||||||
|
response[symbol].marketPrice = new Big(
|
||||||
|
value.price?.regularMarketPrice ?? 0
|
||||||
|
)
|
||||||
|
.div(100)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add country if stock and available
|
||||||
|
if (
|
||||||
|
assetSubClass === AssetSubClass.STOCK &&
|
||||||
|
value.summaryProfile?.country
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const [code] = Object.entries(countries).find(([, country]) => {
|
||||||
|
return country.name === value.summaryProfile?.country;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
response[symbol].countries = [{ code, weight: 1 }];
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add url if available
|
||||||
const url = value.summaryProfile?.website;
|
const url = value.summaryProfile?.website;
|
||||||
if (url) {
|
if (url) {
|
||||||
response[symbol].url = url;
|
response[symbol].url = url;
|
||||||
@ -98,15 +132,15 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const yahooSymbols = aSymbols.map((symbol) => {
|
const yahooFinanceSymbols = aSymbols.map((symbol) => {
|
||||||
return this.convertToYahooSymbol(symbol);
|
return convertToYahooFinanceSymbol(symbol);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historicalData: {
|
const historicalData: {
|
||||||
[symbol: string]: IYahooFinanceHistoricalResponse[];
|
[symbol: string]: IYahooFinanceHistoricalResponse[];
|
||||||
} = await yahooFinance.historical({
|
} = await yahooFinance.historical({
|
||||||
symbols: yahooSymbols,
|
symbols: yahooFinanceSymbols,
|
||||||
from: format(from, DATE_FORMAT),
|
from: format(from, DATE_FORMAT),
|
||||||
to: format(to, DATE_FORMAT)
|
to: format(to, DATE_FORMAT)
|
||||||
});
|
});
|
||||||
@ -115,9 +149,11 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
for (const [yahooSymbol, timeSeries] of Object.entries(historicalData)) {
|
for (const [yahooFinanceSymbol, timeSeries] of Object.entries(
|
||||||
|
historicalData
|
||||||
|
)) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = convertFromYahooSymbol(yahooSymbol);
|
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
||||||
response[symbol] = {};
|
response[symbol] = {};
|
||||||
|
|
||||||
timeSeries.forEach((timeSerie) => {
|
timeSeries.forEach((timeSerie) => {
|
||||||
@ -137,7 +173,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
let items: LookupItem[] = [];
|
const items: LookupItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const get = bent(
|
||||||
@ -154,19 +190,6 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
// filter out undefined symbols
|
// filter out undefined symbols
|
||||||
return quote.symbol;
|
return quote.symbol;
|
||||||
})
|
})
|
||||||
.filter(({ quoteType }) => {
|
|
||||||
return quoteType === 'EQUITY' || quoteType === 'ETF';
|
|
||||||
})
|
|
||||||
.map(({ symbol }) => {
|
|
||||||
return symbol;
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketData = await this.get(symbols);
|
|
||||||
|
|
||||||
items = searchResult.quotes
|
|
||||||
.filter((quote) => {
|
|
||||||
return quote.isYahooFinance;
|
|
||||||
})
|
|
||||||
.filter(({ quoteType }) => {
|
.filter(({ quoteType }) => {
|
||||||
return (
|
return (
|
||||||
quoteType === 'CRYPTOCURRENCY' ||
|
quoteType === 'CRYPTOCURRENCY' ||
|
||||||
@ -182,56 +205,48 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map(({ longname, shortname, symbol }) => {
|
.map(({ symbol }) => {
|
||||||
return {
|
return symbol;
|
||||||
currency: marketData[symbol]?.currency,
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
name: longname || shortname,
|
|
||||||
symbol: convertFromYahooSymbol(symbol)
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const marketData = await this.get(symbols);
|
||||||
|
|
||||||
|
for (const [symbol, value] of Object.entries(marketData)) {
|
||||||
|
items.push({
|
||||||
|
symbol,
|
||||||
|
currency: value.currency,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
|
name: value.name
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return { items };
|
return { items };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private parseAssetClass(aPrice: IYahooFinancePrice): {
|
||||||
* Converts a symbol to a Yahoo symbol
|
assetClass: AssetClass;
|
||||||
*
|
assetSubClass: AssetSubClass;
|
||||||
* Currency: USDCHF=X
|
} {
|
||||||
* Cryptocurrency: BTC-USD
|
|
||||||
*/
|
|
||||||
private convertToYahooSymbol(aSymbol: string) {
|
|
||||||
if (isCurrency(aSymbol)) {
|
|
||||||
if (isCrypto(aSymbol)) {
|
|
||||||
// Add a dash before the last three characters
|
|
||||||
// BTCUSD -> BTC-USD
|
|
||||||
// DOGEUSD -> DOGE-USD
|
|
||||||
return `${aSymbol.substring(0, aSymbol.length - 3)}-${aSymbol.substring(
|
|
||||||
aSymbol.length - 3
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${aSymbol}=X`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return aSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseAssetClass(aString: string): AssetClass {
|
|
||||||
let assetClass: AssetClass;
|
let assetClass: AssetClass;
|
||||||
|
let assetSubClass: AssetSubClass;
|
||||||
|
|
||||||
switch (aString?.toLowerCase()) {
|
switch (aPrice?.quoteType?.toLowerCase()) {
|
||||||
case 'cryptocurrency':
|
case 'cryptocurrency':
|
||||||
assetClass = AssetClass.CASH;
|
assetClass = AssetClass.CASH;
|
||||||
|
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||||
break;
|
break;
|
||||||
case 'equity':
|
case 'equity':
|
||||||
|
assetClass = AssetClass.EQUITY;
|
||||||
|
assetSubClass = AssetSubClass.STOCK;
|
||||||
|
break;
|
||||||
case 'etf':
|
case 'etf':
|
||||||
assetClass = AssetClass.EQUITY;
|
assetClass = AssetClass.EQUITY;
|
||||||
|
assetSubClass = AssetSubClass.ETF;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return assetClass;
|
return { assetClass, assetSubClass };
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseExchange(aString: string): string {
|
private parseExchange(aString: string): string {
|
||||||
@ -243,7 +258,30 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertFromYahooSymbol = (aSymbol: string) => {
|
export const convertFromYahooFinanceSymbol = (aYahooFinanceSymbol: string) => {
|
||||||
const symbol = aSymbol.replace('-', '');
|
const symbol = aYahooFinanceSymbol.replace('-', '');
|
||||||
return symbol.replace('=X', '');
|
return symbol.replace('=X', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a symbol to a Yahoo Finance symbol
|
||||||
|
*
|
||||||
|
* Currency: USDCHF=X
|
||||||
|
* Cryptocurrency: BTC-USD
|
||||||
|
*/
|
||||||
|
export const convertToYahooFinanceSymbol = (aSymbol: string) => {
|
||||||
|
if (isCurrency(aSymbol)) {
|
||||||
|
if (isCrypto(aSymbol)) {
|
||||||
|
// Add a dash before the last three characters
|
||||||
|
// BTCUSD -> BTC-USD
|
||||||
|
// DOGEUSD -> DOGE-USD
|
||||||
|
return `${aSymbol.substring(0, aSymbol.length - 3)}-${aSymbol.substring(
|
||||||
|
aSymbol.length - 3
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${aSymbol}=X`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return aSymbol;
|
||||||
|
};
|
||||||
|
10
apps/api/src/services/exchange-rate-data.module.ts
Normal file
10
apps/api/src/services/exchange-rate-data.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [DataProviderModule],
|
||||||
|
providers: [ExchangeRateDataService],
|
||||||
|
exports: [ExchangeRateDataService]
|
||||||
|
})
|
||||||
|
export class ExchangeRateDataModule {}
|
@ -1,15 +1,16 @@
|
|||||||
import { currencyPairs } from '@ghostfolio/common/config';
|
import { currencyPairs } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { isNumber } from 'lodash';
|
import { isEmpty, isNumber } from 'lodash';
|
||||||
|
|
||||||
import { DataProviderService } from './data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExchangeRateDataService {
|
export class ExchangeRateDataService {
|
||||||
private currencyPairs: string[] = [];
|
private currencyPairs: IDataGatheringItem[] = [];
|
||||||
private exchangeRates: { [currencyPair: string]: number } = {};
|
private exchangeRates: { [currencyPair: string]: number } = {};
|
||||||
|
|
||||||
public constructor(private dataProviderService: DataProviderService) {
|
public constructor(private dataProviderService: DataProviderService) {
|
||||||
@ -20,8 +21,8 @@ export class ExchangeRateDataService {
|
|||||||
this.currencyPairs = [];
|
this.currencyPairs = [];
|
||||||
this.exchangeRates = {};
|
this.exchangeRates = {};
|
||||||
|
|
||||||
for (const { currency1, currency2 } of currencyPairs) {
|
for (const { currency1, currency2, dataSource } of currencyPairs) {
|
||||||
this.addCurrencyPairs(currency1, currency2);
|
this.addCurrencyPairs({ currency1, currency2, dataSource });
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadCurrencies();
|
await this.loadCurrencies();
|
||||||
@ -35,6 +36,24 @@ export class ExchangeRateDataService {
|
|||||||
getYesterday()
|
getYesterday()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isEmpty(result)) {
|
||||||
|
// Load currencies directly from data provider as a fallback
|
||||||
|
// if historical data is not yet available
|
||||||
|
const historicalData = await this.dataProviderService.get(
|
||||||
|
this.currencyPairs.map(({ dataSource, symbol }) => {
|
||||||
|
return { dataSource, symbol };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.keys(historicalData).forEach((key) => {
|
||||||
|
result[key] = {
|
||||||
|
[format(getYesterday(), DATE_FORMAT)]: {
|
||||||
|
marketPrice: historicalData[key].marketPrice
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const resultExtended = result;
|
const resultExtended = result;
|
||||||
|
|
||||||
Object.keys(result).forEach((pair) => {
|
Object.keys(result).forEach((pair) => {
|
||||||
@ -49,21 +68,21 @@ export class ExchangeRateDataService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currencyPairs.forEach((pair) => {
|
this.currencyPairs.forEach(({ symbol }) => {
|
||||||
const [currency1, currency2] = pair.match(/.{1,3}/g);
|
const [currency1, currency2] = symbol.match(/.{1,3}/g);
|
||||||
const date = format(getYesterday(), DATE_FORMAT);
|
const date = format(getYesterday(), DATE_FORMAT);
|
||||||
|
|
||||||
this.exchangeRates[pair] = resultExtended[pair]?.[date]?.marketPrice;
|
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
||||||
|
|
||||||
if (!this.exchangeRates[pair]) {
|
if (!this.exchangeRates[symbol]) {
|
||||||
// Not found, calculate indirectly via USD
|
// Not found, calculate indirectly via USD
|
||||||
this.exchangeRates[pair] =
|
this.exchangeRates[symbol] =
|
||||||
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
|
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
|
||||||
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
|
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
|
||||||
|
|
||||||
// Calculate the opposite direction
|
// Calculate the opposite direction
|
||||||
this.exchangeRates[`${currency2}${currency1}`] =
|
this.exchangeRates[`${currency2}${currency1}`] =
|
||||||
1 / this.exchangeRates[pair];
|
1 / this.exchangeRates[symbol];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -105,8 +124,22 @@ export class ExchangeRateDataService {
|
|||||||
return aValue;
|
return aValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addCurrencyPairs(aCurrency1: Currency, aCurrency2: Currency) {
|
private addCurrencyPairs({
|
||||||
this.currencyPairs.push(`${aCurrency1}${aCurrency2}`);
|
currency1,
|
||||||
this.currencyPairs.push(`${aCurrency2}${aCurrency1}`);
|
currency2,
|
||||||
|
dataSource
|
||||||
|
}: {
|
||||||
|
currency1: Currency;
|
||||||
|
currency2: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
|
}) {
|
||||||
|
this.currencyPairs.push({
|
||||||
|
dataSource,
|
||||||
|
symbol: `${currency1}${currency2}`
|
||||||
|
});
|
||||||
|
this.currencyPairs.push({
|
||||||
|
dataSource,
|
||||||
|
symbol: `${currency2}${currency1}`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
apps/api/src/services/impersonation.module.ts
Normal file
10
apps/api/src/services/impersonation.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [ImpersonationService],
|
||||||
|
exports: [ImpersonationService]
|
||||||
|
})
|
||||||
|
export class ImpersonationModule {}
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
AssetClass,
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
Currency,
|
Currency,
|
||||||
DataSource,
|
DataSource,
|
||||||
SymbolProfile
|
SymbolProfile
|
||||||
@ -35,6 +36,8 @@ export interface IDataProviderHistoricalResponse {
|
|||||||
|
|
||||||
export interface IDataProviderResponse {
|
export interface IDataProviderResponse {
|
||||||
assetClass?: AssetClass;
|
assetClass?: AssetClass;
|
||||||
|
assetSubClass?: AssetSubClass;
|
||||||
|
countries?: { code: string; weight: number }[];
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { AssetClass, Currency, DataSource } from '@prisma/client';
|
import {
|
||||||
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
|
Currency,
|
||||||
|
DataSource
|
||||||
|
} from '@prisma/client';
|
||||||
|
|
||||||
export interface EnhancedSymbolProfile {
|
export interface EnhancedSymbolProfile {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
|
assetSubClass: AssetSubClass;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
currency: Currency | null;
|
currency: Currency | null;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
8
apps/api/src/services/prisma.module.ts
Normal file
8
apps/api/src/services/prisma.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService]
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
@ -10,7 +10,7 @@
|
|||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
|
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
|
||||||
Restricted Access
|
Restricted View
|
||||||
</td></ng-container
|
</td></ng-container
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
[tooltip]="element.Platform?.name"
|
[tooltip]="element.Platform?.name"
|
||||||
[url]="element.Platform?.url"
|
[url]="element.Platform?.url"
|
||||||
></gf-symbol-icon>
|
></gf-symbol-icon>
|
||||||
<span>{{ element.name }}</span>
|
<span>{{ element.name }} </span>
|
||||||
<span
|
<span
|
||||||
*ngIf="element.isDefault"
|
*ngIf="element.isDefault"
|
||||||
class="d-lg-inline-block d-none text-muted"
|
class="d-lg-inline-block d-none text-muted"
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<span class="d-none d-sm-block" i18n>Transactions</span>
|
<span class="d-none d-sm-block" i18n>Transactions</span>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
{{ element.Order?.length }}
|
{{ element.transactionCount }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { AccountsTableComponent } from './accounts-table.component';
|
import { AccountsTableComponent } from './accounts-table.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -5,7 +5,7 @@ import { MatMenuModule } from '@angular/material/menu';
|
|||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@ import { NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
||||||
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';
|
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -16,6 +16,8 @@ import { LinearScale } from 'chart.js';
|
|||||||
import { ArcElement } from 'chart.js';
|
import { ArcElement } from 'chart.js';
|
||||||
import { DoughnutController } from 'chart.js';
|
import { DoughnutController } from 'chart.js';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
|
import * as Color from 'color';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-portfolio-proportion-chart',
|
selector: 'gf-portfolio-proportion-chart',
|
||||||
@ -28,9 +30,10 @@ export class PortfolioProportionChartComponent
|
|||||||
{
|
{
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: Currency;
|
||||||
@Input() isInPercent: boolean;
|
@Input() isInPercent: boolean;
|
||||||
@Input() key: string;
|
@Input() keys: string[];
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() maxItems?: number;
|
@Input() maxItems?: number;
|
||||||
|
@Input() showLabels = false;
|
||||||
@Input() positions: {
|
@Input() positions: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'type'> & { value: number };
|
[symbol: string]: Pick<PortfolioPosition, 'type'> & { value: number };
|
||||||
};
|
};
|
||||||
@ -47,7 +50,13 @@ export class PortfolioProportionChartComponent
|
|||||||
};
|
};
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
ChartDataLabels,
|
||||||
|
DoughnutController,
|
||||||
|
LinearScale,
|
||||||
|
Tooltip
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
@ -65,24 +74,54 @@ export class PortfolioProportionChartComponent
|
|||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const chartData: {
|
const chartData: {
|
||||||
[symbol: string]: { color?: string; value: number };
|
[symbol: string]: {
|
||||||
|
color?: string;
|
||||||
|
subCategory: { [symbol: string]: { value: number } };
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
Object.keys(this.positions).forEach((symbol) => {
|
Object.keys(this.positions).forEach((symbol) => {
|
||||||
if (this.positions[symbol][this.key]) {
|
if (this.positions[symbol][this.keys[0]]) {
|
||||||
if (chartData[this.positions[symbol][this.key]]) {
|
if (chartData[this.positions[symbol][this.keys[0]]]) {
|
||||||
chartData[this.positions[symbol][this.key]].value +=
|
chartData[this.positions[symbol][this.keys[0]]].value +=
|
||||||
this.positions[symbol].value;
|
this.positions[symbol].value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||||
|
this.positions[symbol][this.keys[1]]
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||||
|
this.positions[symbol][this.keys[1]]
|
||||||
|
].value += this.positions[symbol].value;
|
||||||
|
} else {
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||||
|
this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY
|
||||||
|
] = { value: this.positions[symbol].value };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
chartData[this.positions[symbol][this.key]] = {
|
chartData[this.positions[symbol][this.keys[0]]] = {
|
||||||
|
subCategory: {},
|
||||||
value: this.positions[symbol].value
|
value: this.positions[symbol].value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.positions[symbol][this.keys[1]]) {
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory = {
|
||||||
|
[this.positions[symbol][this.keys[1]]]: {
|
||||||
|
value: this.positions[symbol].value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (chartData[UNKNOWN_KEY]) {
|
if (chartData[UNKNOWN_KEY]) {
|
||||||
chartData[UNKNOWN_KEY].value += this.positions[symbol].value;
|
chartData[UNKNOWN_KEY].value += this.positions[symbol].value;
|
||||||
} else {
|
} else {
|
||||||
chartData[UNKNOWN_KEY] = {
|
chartData[UNKNOWN_KEY] = {
|
||||||
|
subCategory: this.keys[1]
|
||||||
|
? { [this.keys[1]]: { value: 0 } }
|
||||||
|
: undefined,
|
||||||
value: this.positions[symbol].value
|
value: this.positions[symbol].value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -107,13 +146,17 @@ export class PortfolioProportionChartComponent
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!unknownItem) {
|
if (!unknownItem) {
|
||||||
const index = chartDataSorted.push([UNKNOWN_KEY, { value: 0 }]);
|
const index = chartDataSorted.push([
|
||||||
|
UNKNOWN_KEY,
|
||||||
|
{ subCategory: {}, value: 0 }
|
||||||
|
]);
|
||||||
unknownItem = chartDataSorted[index];
|
unknownItem = chartDataSorted[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
rest.forEach((restItem) => {
|
rest.forEach((restItem) => {
|
||||||
if (unknownItem?.[1]) {
|
if (unknownItem?.[1]) {
|
||||||
unknownItem[1] = {
|
unknownItem[1] = {
|
||||||
|
subCategory: {},
|
||||||
value: unknownItem[1].value + restItem[1].value
|
value: unknownItem[1].value + restItem[1].value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -132,7 +175,8 @@ export class PortfolioProportionChartComponent
|
|||||||
// Reuse color
|
// Reuse color
|
||||||
item.color = this.colorMap[symbol];
|
item.color = this.colorMap[symbol];
|
||||||
} else {
|
} else {
|
||||||
const color = this.getColorPalette()[index];
|
const color =
|
||||||
|
this.getColorPalette()[index % this.getColorPalette().length];
|
||||||
|
|
||||||
// Store color for reuse
|
// Store color for reuse
|
||||||
this.colorMap[symbol] = color;
|
this.colorMap[symbol] = color;
|
||||||
@ -141,21 +185,53 @@ export class PortfolioProportionChartComponent
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const backgroundColorSubCategory: string[] = [];
|
||||||
|
const dataSubCategory: number[] = [];
|
||||||
|
const labelSubCategory: string[] = [];
|
||||||
|
|
||||||
|
chartDataSorted.forEach(([, item]) => {
|
||||||
|
let lightnessRatio = 0.2;
|
||||||
|
|
||||||
|
Object.keys(item.subCategory).forEach((subCategory) => {
|
||||||
|
backgroundColorSubCategory.push(
|
||||||
|
Color(item.color).lighten(lightnessRatio).hex()
|
||||||
|
);
|
||||||
|
dataSubCategory.push(item.subCategory[subCategory].value);
|
||||||
|
labelSubCategory.push(subCategory);
|
||||||
|
|
||||||
|
lightnessRatio += 0.1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const datasets = [
|
||||||
|
{
|
||||||
|
backgroundColor: chartDataSorted.map(([, item]) => {
|
||||||
|
return item.color;
|
||||||
|
}),
|
||||||
|
borderWidth: 0,
|
||||||
|
data: chartDataSorted.map(([, item]) => {
|
||||||
|
return item.value;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let labels = chartDataSorted.map(([label]) => {
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.keys[1]) {
|
||||||
|
datasets.unshift({
|
||||||
|
backgroundColor: backgroundColorSubCategory,
|
||||||
|
borderWidth: 0,
|
||||||
|
data: dataSubCategory
|
||||||
|
});
|
||||||
|
|
||||||
|
labels = labelSubCategory.concat(labels);
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
datasets: [
|
datasets,
|
||||||
{
|
labels
|
||||||
backgroundColor: chartDataSorted.map(([, item]) => {
|
|
||||||
return item.color;
|
|
||||||
}),
|
|
||||||
borderWidth: 0,
|
|
||||||
data: chartDataSorted.map(([, item]) => {
|
|
||||||
return item.value;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
],
|
|
||||||
labels: chartDataSorted.map(([label]) => {
|
|
||||||
return label;
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.chartCanvas) {
|
if (this.chartCanvas) {
|
||||||
@ -166,13 +242,39 @@ export class PortfolioProportionChartComponent
|
|||||||
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||||
data,
|
data,
|
||||||
options: {
|
options: {
|
||||||
|
cutout: '70%',
|
||||||
|
layout: {
|
||||||
|
padding: this.showLabels === true ? 100 : 0
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
color: (context) => {
|
||||||
|
return this.getColorPalette()[
|
||||||
|
context.dataIndex % this.getColorPalette().length
|
||||||
|
];
|
||||||
|
},
|
||||||
|
display: this.showLabels === true ? 'auto' : false,
|
||||||
|
labels: {
|
||||||
|
index: {
|
||||||
|
align: 'end',
|
||||||
|
anchor: 'end',
|
||||||
|
formatter: (value, context) => {
|
||||||
|
return value > 0
|
||||||
|
? context.chart.data.labels[context.dataIndex]
|
||||||
|
: '';
|
||||||
|
},
|
||||||
|
offset: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
const label =
|
const labelIndex =
|
||||||
context.label === UNKNOWN_KEY ? 'Other' : context.label;
|
(data.datasets[context.datasetIndex - 1]?.data?.length ??
|
||||||
|
0) + context.dataIndex;
|
||||||
|
const label = context.chart.data.labels[labelIndex];
|
||||||
|
|
||||||
if (this.isInPercent) {
|
if (this.isInPercent) {
|
||||||
const value = 100 * <number>context.raw;
|
const value = 100 * <number>context.raw;
|
||||||
|
@ -9,23 +9,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>
|
|
||||||
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
|
||||||
{order} other {orders}}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : summary?.fees"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
@ -66,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
|
<div class="d-flex flex-grow-1" i18n>Absolute Gross Performance</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -77,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1 ml-3" i18n>Performance (TWR)</div>
|
<div class="d-flex flex-grow-1 ml-3" i18n>Gross Performance (TWR)</div>
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -91,6 +74,48 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>
|
||||||
|
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
||||||
|
{order} other {orders}}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.fees"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Absolute Net Performance</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.currentNetPerformance"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1 ml-3" i18n>Net Performance (TWR)</div>
|
||||||
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
@ -121,7 +146,7 @@
|
|||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Net Worth</div>
|
<div class="d-flex flex-grow-1 font-weight-bold" i18n>Net Worth</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -131,4 +156,17 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1 ml-3" i18n>Annualized Performance</div>
|
||||||
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user