Compare commits
106 Commits
Author | SHA1 | Date | |
---|---|---|---|
9a3db91982 | |||
d23cb5f190 | |||
7a364472c8 | |||
59c064e3c8 | |||
e792924606 | |||
d32dd5e860 | |||
bb86f85203 | |||
0bca8897d6 | |||
ba73f6de2e | |||
eb75be8535 | |||
6d2a897366 | |||
d8bfb23f20 | |||
d9d71e7827 | |||
b642ce08e5 | |||
bc8d8309d4 | |||
1f2f9f22f2 | |||
7a3237f1ff | |||
07661d9262 | |||
77358eed65 | |||
c641c28b12 | |||
c54392b7bb | |||
f3a8822a77 | |||
f1dc075c36 | |||
144d831954 | |||
c37ad9bad4 | |||
4ab3f81384 | |||
b932bac9aa | |||
bcdd873222 | |||
25b3de5828 | |||
40b454d2f3 | |||
5596e5f03b | |||
66992ef915 | |||
7f67430685 | |||
8a49a04324 | |||
5d7c19b0ed | |||
cde74b6c62 | |||
633c65e33c | |||
d1617f2d87 | |||
68e558f198 | |||
12ca01c862 | |||
2115745471 | |||
2cabd21315 | |||
3615e2f057 | |||
d3679d41b3 | |||
f2d431a6b8 | |||
2bc8bebfb8 | |||
5b20ba3382 | |||
15cc294581 | |||
b060b81204 | |||
a8d557eb1b | |||
6ae3a47b54 | |||
88c19eb45e | |||
7728706bc8 | |||
2e9d40c201 | |||
c002e37285 | |||
6be38a1c19 | |||
a3178fb213 | |||
e7158f6e16 | |||
dbea0456bc | |||
fefee11301 | |||
40836b745b | |||
07eabac059 | |||
48b412cfb8 | |||
b62488628c | |||
982c71c728 | |||
5aa16a3779 | |||
93de25e5b6 | |||
9acdb41aa2 | |||
ffbdfb86ec | |||
be7f6bb657 | |||
6f7cbc93b9 | |||
0b5c71130d | |||
0578c645d1 | |||
67ae86763e | |||
266c0a9a2c | |||
a3cdb23776 | |||
e1371a8d2b | |||
448cea0b69 | |||
ad42c0bf28 | |||
f50670c7fe | |||
c0029d3b1d | |||
2518a8fd9d | |||
572dcf075a | |||
29cb83d469 | |||
cac73ac111 | |||
02cf4295a9 | |||
78b3328bf7 | |||
e0d6d9e8ca | |||
54310f2214 | |||
1fec49fbc2 | |||
d00489b547 | |||
2985dd67c5 | |||
5eba764c04 | |||
cc0ce18627 | |||
b758654158 | |||
d5d40c0ea1 | |||
fd294d4d2b | |||
e82cf2e7d0 | |||
446c7cb517 | |||
e921ed7f52 | |||
865402be3a | |||
6eb659d7e6 | |||
37430b7bdc | |||
ef9d77312e | |||
ccaf06360a | |||
f83e75df44 |
@ -1,3 +1,4 @@
|
|||||||
/.nx/cache
|
/.nx/cache
|
||||||
|
/apps/client/src/polyfills.ts
|
||||||
/dist
|
/dist
|
||||||
/test/import
|
/test/import
|
||||||
|
21
.prettierrc
21
.prettierrc
@ -9,7 +9,26 @@
|
|||||||
],
|
],
|
||||||
"attributeSort": "ASC",
|
"attributeSort": "ASC",
|
||||||
"endOfLine": "auto",
|
"endOfLine": "auto",
|
||||||
"plugins": ["prettier-plugin-organize-attributes"],
|
"importOrder": ["^@ghostfolio/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.ts",
|
||||||
|
"options": {
|
||||||
|
"importOrderParserPlugins": ["decorators-legacy", "typescript"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-organize-attributes",
|
||||||
|
"@trivago/prettier-plugin-sort-imports"
|
||||||
|
],
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
|
211
CHANGELOG.md
211
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).
|
||||||
|
|
||||||
|
## 2.63.0 - 2024-03-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the content of the _Self-Hosting_ section by available home server systems on the Frequently Asked Questions (FAQ) page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `8.3` to `9.0`
|
||||||
|
- Upgraded `countries-list` from version `2.6.1` to `3.1.0`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.9.1` to `2.10.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the performance calculation caused by multiple `SELL` activities on the same day
|
||||||
|
- Fixed an issue in the calculation on the allocations page caused by liabilities
|
||||||
|
|
||||||
|
## 2.62.0 - 2024-03-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the calculation of the accounts table
|
||||||
|
- Optimized the calculation of the portfolio holdings
|
||||||
|
- Integrated dividend into the transaction point concept in the portfolio service
|
||||||
|
- Removed the environment variable `WEB_AUTH_RP_ID`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the calculation of the portfolio summary caused by future liabilities
|
||||||
|
- Fixed an issue with removing a linked account from a (wealth) item activity
|
||||||
|
|
||||||
|
## 2.61.1 - 2024-03-06
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the account value calculation caused by liabilities
|
||||||
|
|
||||||
|
## 2.61.0 - 2024-03-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the calculation of the portfolio summary
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the activities import (query parameter handling)
|
||||||
|
|
||||||
|
## 2.60.0 - 2024-03-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for the cryptocurrency _Uniswap_ (`UNI7083-USD`)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the benchmarks in the markets overview
|
||||||
|
- Integrated (wealth) items into the transaction point concept in the portfolio service
|
||||||
|
- Refreshed the cryptocurrencies list
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a missing value in the activities table on mobile
|
||||||
|
- Fixed a missing value on the public page
|
||||||
|
- Displayed the button to fetch the current market price only if the activity is from today
|
||||||
|
|
||||||
|
## 2.59.0 - 2024-02-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an index for `isExcluded` to the account database table
|
||||||
|
- Extended the content of the _Self-Hosting_ section on the Frequently Asked Questions (FAQ) page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the activities import by `isin` in the _Yahoo Finance_ service
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the exchange rate calculation of (wealth) items in accounts
|
||||||
|
|
||||||
|
## 2.58.0 - 2024-02-27
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the handling of activities without account
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the query to filter activities of excluded accounts
|
||||||
|
- Improved the asset profile validation in the activities import
|
||||||
|
|
||||||
|
## 2.57.0 - 2024-02-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the break down of the performance into asset and currency on the analysis page from experimental to general availability
|
||||||
|
- Restructured the `copy-assets` `Nx` target
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Changed the performances of the _Top 3_ and _Bottom 3_ performers on the analysis page to take the currency effects into account
|
||||||
|
|
||||||
|
## 2.56.0 - 2024-02-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Switched the performance calculations to take the currency effects into account
|
||||||
|
- Removed the `isDefault` flag from the `Account` database schema
|
||||||
|
- Exposed the database index of _Redis_ as an environment variable (`REDIS_DB`)
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `prisma` from version `5.9.1` to `5.10.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Added the missing default currency to the prepare currencies function in the exchange rate data service
|
||||||
|
|
||||||
|
## 2.55.0 - 2024-02-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added indexes for `alias`, `granteeUserId` and `userId` to the access database table
|
||||||
|
- Added indexes for `currency`, `name` and `userId` to the account database table
|
||||||
|
- Added indexes for `accountId`, `date` and `updatedAt` to the account balance database table
|
||||||
|
- Added an index for `userId` to the auth device database table
|
||||||
|
- Added indexes for `marketPrice` and `state` to the market data database table
|
||||||
|
- Added indexes for `date`, `isDraft` and `userId` to the order database table
|
||||||
|
- Added an index for `name` to the platform database table
|
||||||
|
- Added indexes for `assetClass`, `currency`, `dataSource`, `isin`, `name` and `symbol` to the symbol profile database table
|
||||||
|
- Added an index for `userId` to the subscription database table
|
||||||
|
- Added an index for `name` to the tag database table
|
||||||
|
- Added indexes for `accessToken`, `createdAt`, `provider`, `role` and `thirdPartyId` to the user database table
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the validation for `currency` in various endpoints
|
||||||
|
- Harmonized the setting of a default locale in various components
|
||||||
|
- Set the parser to `angular` in the `prettier` options
|
||||||
|
|
||||||
|
## 2.54.0 - 2024-02-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an index for `id` to the account database table
|
||||||
|
- Added indexes for `dataSource` and `date` to the market data database table
|
||||||
|
- Added an index for `accountId` to the order database table
|
||||||
|
|
||||||
|
## 2.53.1 - 2024-02-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an accounts tab to the position detail dialog
|
||||||
|
- Added `INACTIVE` as a new user role
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the holdings table
|
||||||
|
- Refactored the query to filter activities of excluded accounts
|
||||||
|
- Eliminated the search request to get quotes in the _EOD Historical Data_ service
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `ng-extract-i18n-merge` from version `2.9.1` to `2.10.0`
|
||||||
|
|
||||||
|
## 2.52.0 - 2024-02-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a loading indicator to the dividend timeline on the analysis page
|
||||||
|
- Added a loading indicator to the investment timeline on the analysis page
|
||||||
|
- Added support for the cryptocurrency _Jupiter_ (`JUP29210-USD`)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Divided the content of the Frequently Asked Questions (FAQ) page into three sections: _General_, _Cloud (SaaS)_ and _Self-Hosting_
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the X-axis scale of the dividend timeline on the analysis page
|
||||||
|
- Fixed an issue with the X-axis scale of the investment timeline on the analysis page
|
||||||
|
|
||||||
|
## 2.51.0 - 2024-02-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the ordered list of the _Top 3_ and _Bottom 3_ performers on the analysis page in Safari
|
||||||
|
- Replaced `import-sort` with `prettier-plugin-sort-imports`
|
||||||
|
- Upgraded `eslint` dependencies
|
||||||
|
- Upgraded `Nx` from version `17.2.8` to `18.0.4`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the date conversion of the import of historical market data in the admin control panel
|
||||||
|
|
||||||
|
## 2.50.0 - 2024-02-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduced a setting to disable the data gathering in the admin control
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Harmonized the environment variables of various API keys
|
||||||
|
- Upgraded `prisma` from version `5.8.1` to `5.9.1`
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Rename the environment variable from `ALPHA_VANTAGE_API_KEY` to `API_KEY_ALPHA_VANTAGE`
|
||||||
|
- Rename the environment variable from `BETTER_UPTIME_API_KEY` to `API_KEY_BETTER_UPTIME`
|
||||||
|
- Rename the environment variable from `EOD_HISTORICAL_DATA_API_KEY` to `API_KEY_EOD_HISTORICAL_DATA`
|
||||||
|
- Rename the environment variable from `FINANCIAL_MODELING_PREP_API_KEY` to `API_KEY_FINANCIAL_MODELING_PREP`
|
||||||
|
- Rename the environment variable from `OPEN_FIGI_API_KEY` to `API_KEY_OPEN_FIGI`
|
||||||
|
- Rename the environment variable from `RAPID_API_API_KEY` to `API_KEY_RAPID_API`
|
||||||
|
|
||||||
## 2.49.0 - 2024-02-09
|
## 2.49.0 - 2024-02-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -99,6 +99,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
|
|||||||
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
||||||
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
||||||
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
||||||
|
| `REDIS_DB` | `0` | The database index of _Redis_ |
|
||||||
| `REDIS_HOST` | | The host where _Redis_ is running |
|
| `REDIS_HOST` | | The host where _Redis_ is running |
|
||||||
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
||||||
| `REDIS_PORT` | | The port where _Redis_ is running |
|
| `REDIS_PORT` | | The port where _Redis_ is running |
|
||||||
@ -143,7 +144,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
|||||||
|
|
||||||
### Home Server Systems (Community)
|
### Home Server Systems (Community)
|
||||||
|
|
||||||
Ghostfolio is available for various home server systems, including [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ export default {
|
|||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||||
coverageDirectory: '../../coverage/apps/api',
|
coverageDirectory: '../../coverage/apps/api',
|
||||||
testTimeout: 10000,
|
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
preset: '../../jest.preset.js'
|
preset: '../../jest.preset.js'
|
||||||
};
|
};
|
||||||
|
@ -9,12 +9,13 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"executor": "@nx/webpack:webpack",
|
"executor": "@nx/webpack:webpack",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/apps/api",
|
|
||||||
"main": "apps/api/src/main.ts",
|
|
||||||
"tsConfig": "apps/api/tsconfig.app.json",
|
|
||||||
"assets": ["apps/api/src/assets"],
|
|
||||||
"target": "node",
|
|
||||||
"compiler": "tsc",
|
"compiler": "tsc",
|
||||||
|
"deleteOutputPath": false,
|
||||||
|
"main": "apps/api/src/main.ts",
|
||||||
|
"outputPath": "dist/apps/api",
|
||||||
|
"sourceMap": true,
|
||||||
|
"target": "node",
|
||||||
|
"tsConfig": "apps/api/tsconfig.app.json",
|
||||||
"webpackConfig": "apps/api/webpack.config.js"
|
"webpackConfig": "apps/api/webpack.config.js"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@ -33,6 +34,26 @@
|
|||||||
},
|
},
|
||||||
"outputs": ["{options.outputPath}"]
|
"outputs": ["{options.outputPath}"]
|
||||||
},
|
},
|
||||||
|
"copy-assets": {
|
||||||
|
"executor": "nx:run-commands",
|
||||||
|
"options": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "shx rm -rf dist/apps/api"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "shx mkdir -p dist/apps/api/assets/locales"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "shx cp -r apps/api/src/assets/* dist/apps/api/assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "shx cp -r apps/client/src/locales/* dist/apps/api/assets/locales"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parallel": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"executor": "@nx/js:node",
|
"executor": "@nx/js:node",
|
||||||
"options": {
|
"options": {
|
||||||
|
@ -4,6 +4,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
|
|||||||
import { Access } from '@ghostfolio/common/interfaces';
|
import { Access } from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AccessController } from './access.controller';
|
import { AccessController } from './access.controller';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { AccessWithGranteeUser } from '@ghostfolio/common/types';
|
import { AccessWithGranteeUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Access, Prisma } from '@prisma/client';
|
import { Access, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AccountBalanceController } from './account-balance.controller';
|
import { AccountBalanceController } from './account-balance.controller';
|
||||||
|
@ -2,6 +2,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
|
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
|
||||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AccountBalance, Prisma } from '@prisma/client';
|
import { AccountBalance, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import type {
|
|||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
RequestWithUser
|
RequestWithUser
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -62,7 +63,7 @@ export class AccountController {
|
|||||||
{ Order: true }
|
{ Order: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (account?.isDefault || account?.Order.length > 0) {
|
if (!account || account?.Order.length > 0) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
StatusCodes.FORBIDDEN
|
StatusCodes.FORBIDDEN
|
||||||
|
@ -7,6 +7,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AccountController } from './account.controller';
|
import { AccountController } from './account.controller';
|
||||||
|
@ -2,6 +2,7 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { Filter } from '@ghostfolio/common/interfaces';
|
import { Filter } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
@ -20,10 +21,8 @@ export class AccountService {
|
|||||||
public async account({
|
public async account({
|
||||||
id_userId
|
id_userId
|
||||||
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
|
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
|
||||||
const { id, userId } = id_userId;
|
|
||||||
|
|
||||||
const [account] = await this.accounts({
|
const [account] = await this.accounts({
|
||||||
where: { id, userId }
|
where: id_userId
|
||||||
});
|
});
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Transform, TransformFnParams } from 'class-transformer';
|
import { Transform, TransformFnParams } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
|
IsISO4217CurrencyCode,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@ -19,7 +20,7 @@ export class CreateAccountDto {
|
|||||||
)
|
)
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsISO4217CurrencyCode()
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Transform, TransformFnParams } from 'class-transformer';
|
import { Transform, TransformFnParams } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
|
IsISO4217CurrencyCode,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@ -19,7 +20,7 @@ export class UpdateAccountDto {
|
|||||||
)
|
)
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsISO4217CurrencyCode()
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@ -25,6 +25,7 @@ import type {
|
|||||||
MarketDataPreset,
|
MarketDataPreset,
|
||||||
RequestWithUser
|
RequestWithUser
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -255,7 +256,7 @@ export class AdminController {
|
|||||||
dataSource,
|
dataSource,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
symbol,
|
symbol,
|
||||||
date: resetHours(parseISO(date)),
|
date: parseISO(date),
|
||||||
state: 'CLOSE'
|
state: 'CLOSE'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
|
|||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { MarketDataPreset } from '@ghostfolio/common/types';
|
import { MarketDataPreset } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
AssetSubClass,
|
AssetSubClass,
|
||||||
@ -225,7 +226,7 @@ export class AdminService {
|
|||||||
this.prismaService.symbolProfile.count({ where })
|
this.prismaService.symbolProfile.count({ where })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let marketData = assetProfiles.map(
|
let marketData: AdminMarketDataItem[] = assetProfiles.map(
|
||||||
({
|
({
|
||||||
_count,
|
_count,
|
||||||
assetClass,
|
assetClass,
|
||||||
@ -439,13 +440,14 @@ export class AdminService {
|
|||||||
},
|
},
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
id: true,
|
id: true,
|
||||||
|
role: true,
|
||||||
Subscription: true
|
Subscription: true
|
||||||
},
|
},
|
||||||
take: 30
|
take: 30
|
||||||
});
|
});
|
||||||
|
|
||||||
return usersWithAnalytics.map(
|
return usersWithAnalytics.map(
|
||||||
({ _count, Analytics, createdAt, id, Subscription }) => {
|
({ _count, Analytics, createdAt, id, role, Subscription }) => {
|
||||||
const daysSinceRegistration =
|
const daysSinceRegistration =
|
||||||
differenceInDays(new Date(), createdAt) + 1;
|
differenceInDays(new Date(), createdAt) + 1;
|
||||||
const engagement = Analytics
|
const engagement = Analytics
|
||||||
@ -455,13 +457,17 @@ export class AdminService {
|
|||||||
const subscription = this.configurationService.get(
|
const subscription = this.configurationService.get(
|
||||||
'ENABLE_FEATURE_SUBSCRIPTION'
|
'ENABLE_FEATURE_SUBSCRIPTION'
|
||||||
)
|
)
|
||||||
? this.subscriptionService.getSubscription(Subscription)
|
? this.subscriptionService.getSubscription({
|
||||||
|
createdAt,
|
||||||
|
subscriptions: Subscription
|
||||||
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createdAt,
|
createdAt,
|
||||||
engagement,
|
engagement,
|
||||||
id,
|
id,
|
||||||
|
role,
|
||||||
subscription,
|
subscription,
|
||||||
accountCount: _count.Account || 0,
|
accountCount: _count.Account || 0,
|
||||||
country: Analytics?.country,
|
country: Analytics?.country,
|
||||||
|
@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { AdminJobs } from '@ghostfolio/common/interfaces';
|
import { AdminJobs } from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { QueueController } from './queue.controller';
|
import { QueueController } from './queue.controller';
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
QUEUE_JOB_STATUS_LIST
|
QUEUE_JOB_STATUS_LIST
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { AdminJobs } from '@ghostfolio/common/interfaces';
|
import { AdminJobs } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { JobStatus, Queue } from 'bull';
|
import { JobStatus, Queue } from 'bull';
|
||||||
|
@ -2,6 +2,7 @@ import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
|
|||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
|
IsISO4217CurrencyCode,
|
||||||
IsObject,
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString
|
IsString
|
||||||
@ -24,7 +25,7 @@ export class UpdateAssetProfileDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
countries?: Prisma.InputJsonArray;
|
countries?: Prisma.InputJsonArray;
|
||||||
|
|
||||||
@IsString()
|
@IsISO4217CurrencyCode()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
currency?: string;
|
currency?: string;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Controller } from '@nestjs/common';
|
import { Controller } from '@nestjs/common';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { CronService } from '@ghostfolio/api/services/cron.service';
|
import { CronService } from '@ghostfolio/api/services/cron.service';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
|
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
SUPPORTED_LANGUAGE_CODES
|
SUPPORTED_LANGUAGE_CODES
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
|
|
||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
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 { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
import { AccessModule } from './access/access.module';
|
import { AccessModule } from './access/access.module';
|
||||||
import { AccountModule } from './account/account.module';
|
import { AccountModule } from './account/account.module';
|
||||||
@ -52,6 +53,7 @@ import { UserModule } from './user/user.module';
|
|||||||
BenchmarkModule,
|
BenchmarkModule,
|
||||||
BullModule.forRoot({
|
BullModule.forRoot({
|
||||||
redis: {
|
redis: {
|
||||||
|
db: parseInt(process.env.REDIS_DB ?? '0', 10),
|
||||||
host: process.env.REDIS_HOST,
|
host: process.env.REDIS_HOST,
|
||||||
port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
|
port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
|
||||||
password: process.env.REDIS_PASSWORD
|
password: process.env.REDIS_PASSWORD
|
||||||
@ -73,6 +75,7 @@ import { UserModule } from './user/user.module';
|
|||||||
PlatformModule,
|
PlatformModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
PropertyModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
|
@ -2,6 +2,7 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.s
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { Controller, Delete, Param, UseGuards } from '@nestjs/common';
|
import { Controller, Delete, Param, UseGuards } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-devic
|
|||||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AuthDevice, Prisma } from '@prisma/client';
|
import { AuthDevice, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||||
import { OAuthResponse } from '@ghostfolio/common/interfaces';
|
import { OAuthResponse } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -5,6 +5,7 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
|
|
||||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { Provider } from '@prisma/client';
|
import { Provider } from '@prisma/client';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Provider } from '@prisma/client';
|
import { Provider } from '@prisma/client';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
|
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
|
||||||
|
|
||||||
import { Provider } from '@prisma/client';
|
import { Provider } from '@prisma/client';
|
||||||
|
|
||||||
export interface AuthDeviceDialogParams {
|
export interface AuthDeviceDialogParams {
|
||||||
|
@ -2,6 +2,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
|
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
|
||||||
|
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import * as countriesAndTimezones from 'countries-and-timezones';
|
import * as countriesAndTimezones from 'countries-and-timezones';
|
||||||
|
@ -3,6 +3,7 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.s
|
|||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
@ -40,7 +41,7 @@ export class WebAuthService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
get rpID() {
|
get rpID() {
|
||||||
return this.configurationService.get('WEB_AUTH_RP_ID');
|
return new URL(this.configurationService.get('ROOT_URL')).hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
get expectedOrigin() {
|
get expectedOrigin() {
|
||||||
|
@ -9,6 +9,7 @@ import type {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -7,6 +7,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
|
|||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { BenchmarkController } from './benchmark.controller';
|
import { BenchmarkController } from './benchmark.controller';
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { BenchmarkTrend } from '@ghostfolio/common/types';
|
import { BenchmarkTrend } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { SymbolProfile } from '@prisma/client';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
1
apps/api/src/app/cache/cache.controller.ts
vendored
1
apps/api/src/app/cache/cache.controller.ts
vendored
@ -2,6 +2,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { Controller, Post, UseGuards } from '@nestjs/common';
|
import { Controller, Post, UseGuards } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
1
apps/api/src/app/cache/cache.module.ts
vendored
1
apps/api/src/app/cache/cache.module.ts
vendored
@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CacheController } from './cache.controller';
|
import { CacheController } from './cache.controller';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ExchangeRateController } from './exchange-rate.controller';
|
import { ExchangeRateController } from './exchange-rate.controller';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -2,6 +2,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
|
|||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { Export } from '@ghostfolio/common/interfaces';
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
@ -5,6 +5,7 @@ import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ExportController } from './export.controller';
|
import { ExportController } from './export.controller';
|
||||||
|
@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { environment } from '@ghostfolio/api/environments/environment';
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
import { Filter, Export } from '@ghostfolio/common/interfaces';
|
import { Filter, Export } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
|
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
|
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsOptional, ValidateNested } from 'class-validator';
|
import { IsArray, IsOptional, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
|
|||||||
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -42,8 +43,10 @@ export class ImportController {
|
|||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async import(
|
public async import(
|
||||||
@Body() importData: ImportDataDto,
|
@Body() importData: ImportDataDto,
|
||||||
@Query('dryRun') isDryRun?: boolean
|
@Query('dryRun') isDryRunParam = 'false'
|
||||||
): Promise<ImportResponse> {
|
): Promise<ImportResponse> {
|
||||||
|
const isDryRun = isDryRunParam === 'true';
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!hasPermission(this.request.user.permissions, permissions.createAccount)
|
!hasPermission(this.request.user.permissions, permissions.createAccount)
|
||||||
) {
|
) {
|
||||||
|
@ -10,6 +10,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ImportController } from './import.controller';
|
import { ImportController } from './import.controller';
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
UserWithSettings
|
UserWithSettings
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
@ -569,17 +570,10 @@ export class ImportService {
|
|||||||
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
|
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const uniqueActivitiesDto = uniqBy(
|
|
||||||
activitiesDto,
|
|
||||||
({ dataSource, symbol }) => {
|
|
||||||
return getAssetProfileIdentifier({ dataSource, symbol });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [
|
for (const [
|
||||||
index,
|
index,
|
||||||
{ currency, dataSource, symbol, type }
|
{ currency, dataSource, symbol, type }
|
||||||
] of uniqueActivitiesDto.entries()) {
|
] of activitiesDto.entries()) {
|
||||||
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
|
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`activities.${index}.dataSource ("${dataSource}") is not valid`
|
`activities.${index}.dataSource ("${dataSource}") is not valid`
|
||||||
@ -601,37 +595,33 @@ export class ImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetProfile = {
|
if (!assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })]) {
|
||||||
currency,
|
const assetProfile = {
|
||||||
...(
|
currency,
|
||||||
await this.dataProviderService.getAssetProfiles([
|
...(
|
||||||
{ dataSource, symbol }
|
await this.dataProviderService.getAssetProfiles([
|
||||||
])
|
{ dataSource, symbol }
|
||||||
)?.[symbol]
|
])
|
||||||
};
|
)?.[symbol]
|
||||||
|
};
|
||||||
|
|
||||||
if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
|
if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
|
||||||
if (!assetProfile?.name) {
|
if (!assetProfile?.name) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetProfile.currency !== currency) {
|
||||||
|
throw new Error(
|
||||||
|
`activities.${index}.currency ("${currency}") does not match with currency of ${assetProfile.symbol} ("${assetProfile.currency}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
||||||
assetProfile.currency !== currency &&
|
assetProfile;
|
||||||
!this.exchangeRateDataService.hasCurrencyPair(
|
|
||||||
currency,
|
|
||||||
assetProfile.currency
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
|
||||||
assetProfile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return assetProfiles;
|
return assetProfiles;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||||
|
|
||||||
import { InfoService } from './info.service';
|
import { InfoService } from './info.service';
|
||||||
|
@ -10,6 +10,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
|||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
|
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
@ -59,10 +60,6 @@ export class InfoService {
|
|||||||
|
|
||||||
const globalPermissions: string[] = [];
|
const globalPermissions: string[] = [];
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_BLOG')) {
|
|
||||||
globalPermissions.push(permissions.enableBlog);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
info.fearAndGreedDataSource = encodeDataSource(
|
info.fearAndGreedDataSource = encodeDataSource(
|
||||||
@ -351,7 +348,7 @@ export class InfoService {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.configurationService.get(
|
Authorization: `Bearer ${this.configurationService.get(
|
||||||
'BETTER_UPTIME_API_KEY'
|
'API_KEY_BETTER_UPTIME'
|
||||||
)}`
|
)}`
|
||||||
},
|
},
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { LogoController } from './logo.controller';
|
import { LogoController } from './logo.controller';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { HttpException, Injectable } from '@nestjs/common';
|
import { HttpException, Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
IsArray,
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
|
IsISO4217CurrencyCode,
|
||||||
IsISO8601,
|
IsISO8601,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@ -38,7 +39,7 @@ export class CreateOrderDto {
|
|||||||
)
|
)
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsISO4217CurrencyCode()
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -9,6 +9,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation/imp
|
|||||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -11,6 +11,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
|
|||||||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { OrderController } from './order.controller';
|
import { OrderController } from './order.controller';
|
||||||
|
@ -8,8 +8,9 @@ import {
|
|||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import { Filter } from '@ghostfolio/common/interfaces';
|
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
AssetClass,
|
AssetClass,
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
Order,
|
Order,
|
||||||
Prisma,
|
Prisma,
|
||||||
Tag,
|
Tag,
|
||||||
Type as TypeOfOrder
|
Type as ActivityType
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { endOfToday, isAfter } from 'date-fns';
|
import { endOfToday, isAfter } from 'date-fns';
|
||||||
@ -69,12 +70,7 @@ export class OrderService {
|
|||||||
const updateAccountBalance = data.updateAccountBalance ?? false;
|
const updateAccountBalance = data.updateAccountBalance ?? false;
|
||||||
const userId = data.userId;
|
const userId = data.userId;
|
||||||
|
|
||||||
if (
|
if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) {
|
||||||
data.type === 'FEE' ||
|
|
||||||
data.type === 'INTEREST' ||
|
|
||||||
data.type === 'ITEM' ||
|
|
||||||
data.type === 'LIABILITY'
|
|
||||||
) {
|
|
||||||
const assetClass = data.assetClass;
|
const assetClass = data.assetClass;
|
||||||
const assetSubClass = data.assetSubClass;
|
const assetSubClass = data.assetSubClass;
|
||||||
currency = data.SymbolProfile.connectOrCreate.create.currency;
|
currency = data.SymbolProfile.connectOrCreate.create.currency;
|
||||||
@ -129,13 +125,9 @@ export class OrderService {
|
|||||||
|
|
||||||
const orderData: Prisma.OrderCreateInput = data;
|
const orderData: Prisma.OrderCreateInput = data;
|
||||||
|
|
||||||
const isDraft =
|
const isDraft = ['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)
|
||||||
data.type === 'FEE' ||
|
? false
|
||||||
data.type === 'INTEREST' ||
|
: isAfter(data.date as Date, endOfToday());
|
||||||
data.type === 'ITEM' ||
|
|
||||||
data.type === 'LIABILITY'
|
|
||||||
? false
|
|
||||||
: isAfter(data.date as Date, endOfToday());
|
|
||||||
|
|
||||||
const order = await this.prismaService.order.create({
|
const order = await this.prismaService.order.create({
|
||||||
data: {
|
data: {
|
||||||
@ -179,12 +171,7 @@ export class OrderService {
|
|||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type)) {
|
||||||
order.type === 'FEE' ||
|
|
||||||
order.type === 'INTEREST' ||
|
|
||||||
order.type === 'ITEM' ||
|
|
||||||
order.type === 'LIABILITY'
|
|
||||||
) {
|
|
||||||
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,6 +186,17 @@ export class OrderService {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getLatestOrder({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
return this.prismaService.order.findFirst({
|
||||||
|
orderBy: {
|
||||||
|
date: 'desc'
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
SymbolProfile: { dataSource, symbol }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getOrders({
|
public async getOrders({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
@ -217,7 +215,7 @@ export class OrderService {
|
|||||||
sortColumn?: string;
|
sortColumn?: string;
|
||||||
sortDirection?: Prisma.SortOrder;
|
sortDirection?: Prisma.SortOrder;
|
||||||
take?: number;
|
take?: number;
|
||||||
types?: TypeOfOrder[];
|
types?: ActivityType[];
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
@ -291,13 +289,14 @@ export class OrderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (types) {
|
if (types) {
|
||||||
where.OR = types.map((type) => {
|
where.type = { in: types };
|
||||||
return {
|
}
|
||||||
type: {
|
|
||||||
equals: type
|
if (withExcludedAccounts === false) {
|
||||||
}
|
where.OR = [
|
||||||
};
|
{ Account: null },
|
||||||
});
|
{ Account: { NOT: { isExcluded: true } } }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const [orders, count] = await Promise.all([
|
const [orders, count] = await Promise.all([
|
||||||
@ -321,32 +320,26 @@ export class OrderService {
|
|||||||
this.prismaService.order.count({ where })
|
this.prismaService.order.count({ where })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const activities = orders
|
const activities = orders.map((order) => {
|
||||||
.filter((order) => {
|
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||||
return (
|
|
||||||
withExcludedAccounts ||
|
|
||||||
!order.Account ||
|
|
||||||
order.Account?.isExcluded === false
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((order) => {
|
|
||||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
|
value,
|
||||||
|
// TODO: Use exchange rate of date
|
||||||
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
|
order.fee,
|
||||||
|
order.SymbolProfile.currency,
|
||||||
|
userCurrency
|
||||||
|
),
|
||||||
|
// TODO: Use exchange rate of date
|
||||||
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
value,
|
||||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
order.SymbolProfile.currency,
|
||||||
order.fee,
|
userCurrency
|
||||||
order.SymbolProfile.currency,
|
)
|
||||||
userCurrency
|
};
|
||||||
),
|
});
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
||||||
value,
|
|
||||||
order.SymbolProfile.currency,
|
|
||||||
userCurrency
|
|
||||||
)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { activities, count };
|
return { activities, count };
|
||||||
}
|
}
|
||||||
@ -370,13 +363,10 @@ export class OrderService {
|
|||||||
dataSource?: DataSource;
|
dataSource?: DataSource;
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
|
type?: ActivityType;
|
||||||
};
|
};
|
||||||
where: Prisma.OrderWhereUniqueInput;
|
where: Prisma.OrderWhereUniqueInput;
|
||||||
}): Promise<Order> {
|
}): Promise<Order> {
|
||||||
if (data.Account.connect.id_userId.id === null) {
|
|
||||||
delete data.Account;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.comment) {
|
if (!data.comment) {
|
||||||
data.comment = null;
|
data.comment = null;
|
||||||
}
|
}
|
||||||
@ -385,13 +375,12 @@ export class OrderService {
|
|||||||
|
|
||||||
let isDraft = false;
|
let isDraft = false;
|
||||||
|
|
||||||
if (
|
if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) {
|
||||||
data.type === 'FEE' ||
|
|
||||||
data.type === 'INTEREST' ||
|
|
||||||
data.type === 'ITEM' ||
|
|
||||||
data.type === 'LIABILITY'
|
|
||||||
) {
|
|
||||||
delete data.SymbolProfile.connect;
|
delete data.SymbolProfile.connect;
|
||||||
|
|
||||||
|
if (data.Account?.connect?.id_userId?.id === null) {
|
||||||
|
data.Account = { disconnect: true };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete data.SymbolProfile.update;
|
delete data.SymbolProfile.update;
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import { Transform, TransformFnParams } from 'class-transformer';
|
|||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
|
IsISO4217CurrencyCode,
|
||||||
IsISO8601,
|
IsISO8601,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@ -37,7 +38,7 @@ export class UpdateOrderDto {
|
|||||||
)
|
)
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsISO4217CurrencyCode()
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { PlatformController } from './platform.controller';
|
import { PlatformController } from './platform.controller';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Platform, Prisma } from '@prisma/client';
|
import { Platform, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns';
|
import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns';
|
||||||
|
|
||||||
import { GetValueObject } from './interfaces/get-value-object.interface';
|
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||||
@ -42,6 +43,17 @@ function mockGetValue(symbol: string, date: Date) {
|
|||||||
|
|
||||||
return { marketPrice: 0 };
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
|
case 'MSFT':
|
||||||
|
if (isSameDay(parseDate('2021-09-16'), date)) {
|
||||||
|
return { marketPrice: 89.12 };
|
||||||
|
} else if (isSameDay(parseDate('2021-11-16'), date)) {
|
||||||
|
return { marketPrice: 339.51 };
|
||||||
|
} else if (isSameDay(parseDate('2023-07-10'), date)) {
|
||||||
|
return { marketPrice: 331.83 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
case 'NOVN.SW':
|
case 'NOVN.SW':
|
||||||
if (isSameDay(parseDate('2022-04-11'), date)) {
|
if (isSameDay(parseDate('2022-04-11'), date)) {
|
||||||
return { marketPrice: 87.8 };
|
return { marketPrice: 87.8 };
|
||||||
|
@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
@ -106,7 +107,9 @@ describe('CurrentRateService', () => {
|
|||||||
|
|
||||||
currentRateService = new CurrentRateService(
|
currentRateService = new CurrentRateService(
|
||||||
dataProviderService,
|
dataProviderService,
|
||||||
marketDataService
|
marketDataService,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
@ -6,7 +7,10 @@ import {
|
|||||||
ResponseError,
|
ResponseError,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { isBefore, isToday } from 'date-fns';
|
import { isBefore, isToday } from 'date-fns';
|
||||||
import { flatten, isEmpty, uniqBy } from 'lodash';
|
import { flatten, isEmpty, uniqBy } from 'lodash';
|
||||||
|
|
||||||
@ -18,9 +22,12 @@ import { GetValuesParams } from './interfaces/get-values-params.interface';
|
|||||||
export class CurrentRateService {
|
export class CurrentRateService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly marketDataService: MarketDataService
|
private readonly marketDataService: MarketDataService,
|
||||||
|
private readonly orderService: OrderService,
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
// TODO: Pass user instead of using this.request.user
|
||||||
public async getValues({
|
public async getValues({
|
||||||
dataGatheringItems,
|
dataGatheringItems,
|
||||||
dateQuery
|
dateQuery
|
||||||
@ -39,7 +46,7 @@ export class CurrentRateService {
|
|||||||
if (includeToday) {
|
if (includeToday) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.dataProviderService
|
this.dataProviderService
|
||||||
.getQuotes({ items: dataGatheringItems })
|
.getQuotes({ items: dataGatheringItems, user: this.request?.user })
|
||||||
.then((dataResultProvider) => {
|
.then((dataResultProvider) => {
|
||||||
const result: GetValueObject[] = [];
|
const result: GetValueObject[] = [];
|
||||||
|
|
||||||
@ -116,11 +123,17 @@ export class CurrentRateService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
// Fallback to unit price of latest activity
|
||||||
|
const latestActivity = await this.orderService.getLatestOrder({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
|
||||||
value = {
|
value = {
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: today,
|
date: today,
|
||||||
marketPrice: 0
|
marketPrice: latestActivity?.unitPrice ?? 0
|
||||||
};
|
};
|
||||||
|
|
||||||
response.values.push(value);
|
response.values.push(value);
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface CurrentPositions extends ResponseError {
|
export interface CurrentPositions extends ResponseError {
|
||||||
positions: TimelinePosition[];
|
currentValueInBaseCurrency: Big;
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformanceWithCurrencyEffect: Big;
|
grossPerformanceWithCurrencyEffect: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
@ -13,6 +14,6 @@ export interface CurrentPositions extends ResponseError {
|
|||||||
netPerformanceWithCurrencyEffect: Big;
|
netPerformanceWithCurrencyEffect: Big;
|
||||||
netPerformancePercentage: Big;
|
netPerformancePercentage: Big;
|
||||||
netPerformancePercentageWithCurrencyEffect: Big;
|
netPerformancePercentageWithCurrencyEffect: Big;
|
||||||
currentValue: Big;
|
positions: TimelinePosition[];
|
||||||
totalInvestment: Big;
|
totalInvestment: Big;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { PortfolioOrder } from './portfolio-order.interface';
|
|||||||
export interface PortfolioOrderItem extends PortfolioOrder {
|
export interface PortfolioOrderItem extends PortfolioOrder {
|
||||||
feeInBaseCurrency?: Big;
|
feeInBaseCurrency?: Big;
|
||||||
feeInBaseCurrencyWithCurrencyEffect?: Big;
|
feeInBaseCurrencyWithCurrencyEffect?: Big;
|
||||||
itemType?: '' | 'start' | 'end';
|
itemType?: 'end' | 'start';
|
||||||
unitPriceInBaseCurrency?: Big;
|
unitPriceInBaseCurrency?: Big;
|
||||||
unitPriceInBaseCurrencyWithCurrencyEffect?: Big;
|
unitPriceInBaseCurrencyWithCurrencyEffect?: Big;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DataSource, Tag, Type as TypeOfOrder } from '@prisma/client';
|
import { DataSource, Tag, Type as ActivityType } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface PortfolioOrder {
|
export interface PortfolioOrder {
|
||||||
@ -10,6 +10,6 @@ export interface PortfolioOrder {
|
|||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
type: TypeOfOrder;
|
type: ActivityType;
|
||||||
unitPrice: Big;
|
unitPrice: Big;
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ import {
|
|||||||
HistoricalDataItem
|
HistoricalDataItem
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { Tag } from '@prisma/client';
|
|
||||||
|
import { Account, Tag } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioPositionDetail {
|
||||||
|
accounts: Account[];
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
dataProviderInfo: DataProviderInfo;
|
dataProviderInfo: DataProviderInfo;
|
||||||
dividendInBaseCurrency: number;
|
dividendInBaseCurrency: number;
|
||||||
|
@ -2,8 +2,10 @@ import { DataSource, Tag } from '@prisma/client';
|
|||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface TransactionPointSymbol {
|
export interface TransactionPointSymbol {
|
||||||
|
averagePrice: Big;
|
||||||
currency: string;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
dividend: Big;
|
||||||
fee: Big;
|
fee: Big;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get current positions', () => {
|
||||||
|
it.only('with BALN.SW buy and sell in two activities', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService,
|
||||||
|
currency: 'CHF',
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2021-11-22',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(1.55),
|
||||||
|
name: 'Bâloise Holding AG',
|
||||||
|
quantity: new Big(2),
|
||||||
|
symbol: 'BALN.SW',
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: new Big(142.9)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2021-11-30',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(1.65),
|
||||||
|
name: 'Bâloise Holding AG',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'BALN.SW',
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: new Big(136.6)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2021-11-30',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Bâloise Holding AG',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'BALN.SW',
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: new Big(136.6)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
|
start: parseDate('2021-11-22')
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2021-11-22')
|
||||||
|
);
|
||||||
|
|
||||||
|
const investments = portfolioCalculator.getInvestments();
|
||||||
|
|
||||||
|
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||||
|
data: chartData,
|
||||||
|
groupBy: 'month'
|
||||||
|
});
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('-12.6'),
|
||||||
|
grossPerformancePercentage: new Big('-0.04408677396780965649'),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: new Big(
|
||||||
|
'-0.04408677396780965649'
|
||||||
|
),
|
||||||
|
grossPerformanceWithCurrencyEffect: new Big('-12.6'),
|
||||||
|
hasErrors: false,
|
||||||
|
netPerformance: new Big('-15.8'),
|
||||||
|
netPerformancePercentage: new Big('-0.05528341497550734703'),
|
||||||
|
netPerformancePercentageWithCurrencyEffect: new Big(
|
||||||
|
'-0.05528341497550734703'
|
||||||
|
),
|
||||||
|
netPerformanceWithCurrencyEffect: new Big('-15.8'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('0'),
|
||||||
|
currency: 'CHF',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
|
fee: new Big('3.2'),
|
||||||
|
firstBuyDate: '2021-11-22',
|
||||||
|
grossPerformance: new Big('-12.6'),
|
||||||
|
grossPerformancePercentage: new Big('-0.04408677396780965649'),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: new Big(
|
||||||
|
'-0.04408677396780965649'
|
||||||
|
),
|
||||||
|
grossPerformanceWithCurrencyEffect: new Big('-12.6'),
|
||||||
|
investment: new Big('0'),
|
||||||
|
investmentWithCurrencyEffect: new Big('0'),
|
||||||
|
netPerformance: new Big('-15.8'),
|
||||||
|
netPerformancePercentage: new Big('-0.05528341497550734703'),
|
||||||
|
netPerformancePercentageWithCurrencyEffect: new Big(
|
||||||
|
'-0.05528341497550734703'
|
||||||
|
),
|
||||||
|
netPerformanceWithCurrencyEffect: new Big('-15.8'),
|
||||||
|
marketPrice: 148.9,
|
||||||
|
marketPriceInBaseCurrency: 148.9,
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'BALN.SW',
|
||||||
|
timeWeightedInvestment: new Big('285.80000000000000396627'),
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big(
|
||||||
|
'285.80000000000000396627'
|
||||||
|
),
|
||||||
|
transactionCount: 3,
|
||||||
|
valueInBaseCurrency: new Big('0')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalInvestment: new Big('0'),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(investments).toEqual([
|
||||||
|
{ date: '2021-11-22', investment: new Big('285.8') },
|
||||||
|
{ date: '2021-11-30', investment: new Big('0') }
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(investmentsByMonth).toEqual([
|
||||||
|
{ date: '2021-11-01', investment: 0 },
|
||||||
|
{ date: '2021-12-01', investment: 0 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
@ -20,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
@ -86,7 +87,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
grossPerformancePercentage: new Big('-0.0440867739678096571'),
|
grossPerformancePercentage: new Big('-0.0440867739678096571'),
|
||||||
@ -106,6 +107,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
averagePrice: new Big('0'),
|
averagePrice: new Big('0'),
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
fee: new Big('3.2'),
|
fee: new Big('3.2'),
|
||||||
firstBuyDate: '2021-11-22',
|
firstBuyDate: '2021-11-22',
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
@ -128,7 +131,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'BALN.SW',
|
symbol: 'BALN.SW',
|
||||||
timeWeightedInvestment: new Big('285.8'),
|
timeWeightedInvestment: new Big('285.8'),
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'),
|
timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'),
|
||||||
transactionCount: 2
|
transactionCount: 2,
|
||||||
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
@ -20,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
@ -75,7 +76,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('297.8'),
|
currentValueInBaseCurrency: new Big('297.8'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('24.6'),
|
grossPerformance: new Big('24.6'),
|
||||||
grossPerformancePercentage: new Big('0.09004392386530014641'),
|
grossPerformancePercentage: new Big('0.09004392386530014641'),
|
||||||
@ -95,6 +96,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
averagePrice: new Big('136.6'),
|
averagePrice: new Big('136.6'),
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
fee: new Big('1.55'),
|
fee: new Big('1.55'),
|
||||||
firstBuyDate: '2021-11-30',
|
firstBuyDate: '2021-11-30',
|
||||||
grossPerformance: new Big('24.6'),
|
grossPerformance: new Big('24.6'),
|
||||||
@ -117,7 +120,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'BALN.SW',
|
symbol: 'BALN.SW',
|
||||||
timeWeightedInvestment: new Big('273.2'),
|
timeWeightedInvestment: new Big('273.2'),
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'),
|
timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'),
|
||||||
transactionCount: 1
|
transactionCount: 1,
|
||||||
|
valueInBaseCurrency: new Big('297.8')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
totalInvestment: new Big('273.2'),
|
totalInvestment: new Big('273.2'),
|
||||||
|
@ -2,6 +2,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
@ -33,7 +34,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
@ -99,7 +100,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('13298.425356'),
|
currentValueInBaseCurrency: new Big('13298.425356'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('27172.74'),
|
grossPerformance: new Big('27172.74'),
|
||||||
grossPerformancePercentage: new Big('42.41978276196153750666'),
|
grossPerformancePercentage: new Big('42.41978276196153750666'),
|
||||||
@ -119,6 +120,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
averagePrice: new Big('320.43'),
|
averagePrice: new Big('320.43'),
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
fee: new Big('0'),
|
fee: new Big('0'),
|
||||||
firstBuyDate: '2015-01-01',
|
firstBuyDate: '2015-01-01',
|
||||||
grossPerformance: new Big('27172.74'),
|
grossPerformance: new Big('27172.74'),
|
||||||
@ -148,7 +151,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(
|
timeWeightedInvestmentWithCurrencyEffect: new Big(
|
||||||
'636.79469348020066587024'
|
'636.79469348020066587024'
|
||||||
),
|
),
|
||||||
transactionCount: 2
|
transactionCount: 2,
|
||||||
|
valueInBaseCurrency: new Big('13298.425356')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
totalInvestment: new Big('320.43'),
|
totalInvestment: new Big('320.43'),
|
||||||
|
@ -2,6 +2,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
@ -33,7 +34,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
@ -88,7 +89,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('103.10483'),
|
currentValueInBaseCurrency: new Big('103.10483'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('27.33'),
|
grossPerformance: new Big('27.33'),
|
||||||
grossPerformancePercentage: new Big('0.3066651705565529623'),
|
grossPerformancePercentage: new Big('0.3066651705565529623'),
|
||||||
@ -108,6 +109,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
averagePrice: new Big('89.12'),
|
averagePrice: new Big('89.12'),
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
fee: new Big('1'),
|
fee: new Big('1'),
|
||||||
firstBuyDate: '2023-01-03',
|
firstBuyDate: '2023-01-03',
|
||||||
grossPerformance: new Big('27.33'),
|
grossPerformance: new Big('27.33'),
|
||||||
@ -131,7 +134,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
tags: undefined,
|
tags: undefined,
|
||||||
timeWeightedInvestment: new Big('89.12'),
|
timeWeightedInvestment: new Big('89.12'),
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'),
|
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'),
|
||||||
transactionCount: 1
|
transactionCount: 1,
|
||||||
|
valueInBaseCurrency: new Big('103.10483')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
totalInvestment: new Big('89.12'),
|
totalInvestment: new Big('89.12'),
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
ExchangeRateDataService: jest.fn().mockImplementation(() => {
|
||||||
|
return ExchangeRateDataServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get current positions', () => {
|
||||||
|
it.only('with MSFT buy', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService,
|
||||||
|
currency: 'USD',
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
date: '2021-09-16',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(19),
|
||||||
|
name: 'Microsoft Inc.',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'MSFT',
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: new Big(298.58)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
date: '2021-11-16',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Microsoft Inc.',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'MSFT',
|
||||||
|
type: 'DIVIDEND',
|
||||||
|
unitPrice: new Big(0.62)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||||
|
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2023-07-10')
|
||||||
|
);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toMatchObject({
|
||||||
|
errors: [],
|
||||||
|
hasErrors: false,
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('298.58'),
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0.62'),
|
||||||
|
dividendInBaseCurrency: new Big('0.62'),
|
||||||
|
fee: new Big('19'),
|
||||||
|
firstBuyDate: '2021-09-16',
|
||||||
|
investment: new Big('298.58'),
|
||||||
|
investmentWithCurrencyEffect: new Big('298.58'),
|
||||||
|
marketPrice: 331.83,
|
||||||
|
marketPriceInBaseCurrency: 331.83,
|
||||||
|
quantity: new Big('1'),
|
||||||
|
symbol: 'MSFT',
|
||||||
|
tags: undefined,
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalInvestment: new Big('298.58'),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big('298.58')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
@ -20,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
@ -63,7 +64,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big(0),
|
currentValueInBaseCurrency: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
@ -20,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
@ -86,7 +87,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('87.8'),
|
currentValueInBaseCurrency: new Big('87.8'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('21.93'),
|
grossPerformance: new Big('21.93'),
|
||||||
grossPerformancePercentage: new Big('0.15113417083448194384'),
|
grossPerformancePercentage: new Big('0.15113417083448194384'),
|
||||||
@ -106,6 +107,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
averagePrice: new Big('75.80'),
|
averagePrice: new Big('75.80'),
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
fee: new Big('4.25'),
|
fee: new Big('4.25'),
|
||||||
firstBuyDate: '2022-03-07',
|
firstBuyDate: '2022-03-07',
|
||||||
grossPerformance: new Big('21.93'),
|
grossPerformance: new Big('21.93'),
|
||||||
@ -130,7 +133,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(
|
timeWeightedInvestmentWithCurrencyEffect: new Big(
|
||||||
'145.10285714285714285714'
|
'145.10285714285714285714'
|
||||||
),
|
),
|
||||||
transactionCount: 2
|
transactionCount: 2,
|
||||||
|
valueInBaseCurrency: new Big('87.8')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
totalInvestment: new Big('75.80'),
|
totalInvestment: new Big('75.80'),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
@ -20,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
@ -112,7 +113,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('19.86'),
|
grossPerformance: new Big('19.86'),
|
||||||
grossPerformancePercentage: new Big('0.13100263852242744063'),
|
grossPerformancePercentage: new Big('0.13100263852242744063'),
|
||||||
@ -132,6 +133,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
averagePrice: new Big('0'),
|
averagePrice: new Big('0'),
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
fee: new Big('0'),
|
fee: new Big('0'),
|
||||||
firstBuyDate: '2022-03-07',
|
firstBuyDate: '2022-03-07',
|
||||||
grossPerformance: new Big('19.86'),
|
grossPerformance: new Big('19.86'),
|
||||||
@ -154,7 +157,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'NOVN.SW',
|
symbol: 'NOVN.SW',
|
||||||
timeWeightedInvestment: new Big('151.6'),
|
timeWeightedInvestment: new Big('151.6'),
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'),
|
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'),
|
||||||
transactionCount: 2
|
transactionCount: 2,
|
||||||
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
@ -9,7 +10,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
null,
|
null,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
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';
|
||||||
@ -10,8 +11,8 @@ import {
|
|||||||
TimelinePosition
|
TimelinePosition
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { GroupBy } from '@ghostfolio/common/types';
|
import { GroupBy } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { Type as TypeOfOrder } from '@prisma/client';
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
@ -21,6 +22,7 @@ import {
|
|||||||
format,
|
format,
|
||||||
isBefore,
|
isBefore,
|
||||||
isSameDay,
|
isSameDay,
|
||||||
|
max,
|
||||||
subDays
|
subDays
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
|
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
|
||||||
@ -69,40 +71,40 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
let lastDate: string = null;
|
let lastDate: string = null;
|
||||||
let lastTransactionPoint: TransactionPoint = null;
|
let lastTransactionPoint: TransactionPoint = null;
|
||||||
|
|
||||||
for (const order of this.orders) {
|
for (const order of this.orders) {
|
||||||
const currentDate = order.date;
|
const currentDate = order.date;
|
||||||
|
|
||||||
let currentTransactionPointItem: TransactionPointSymbol;
|
let currentTransactionPointItem: TransactionPointSymbol;
|
||||||
const oldAccumulatedSymbol = symbols[order.symbol];
|
const oldAccumulatedSymbol = symbols[order.symbol];
|
||||||
|
|
||||||
const factor = this.getFactor(order.type);
|
const factor = getFactor(order.type);
|
||||||
const unitPrice = new Big(order.unitPrice);
|
|
||||||
if (oldAccumulatedSymbol) {
|
if (oldAccumulatedSymbol) {
|
||||||
|
let investment = oldAccumulatedSymbol.investment;
|
||||||
|
|
||||||
const newQuantity = order.quantity
|
const newQuantity = order.quantity
|
||||||
.mul(factor)
|
.mul(factor)
|
||||||
.plus(oldAccumulatedSymbol.quantity);
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
|
|
||||||
let investment = new Big(0);
|
if (order.type === 'BUY') {
|
||||||
|
investment = oldAccumulatedSymbol.investment.plus(
|
||||||
if (newQuantity.gt(0)) {
|
order.quantity.mul(order.unitPrice)
|
||||||
if (order.type === 'BUY') {
|
);
|
||||||
investment = oldAccumulatedSymbol.investment.plus(
|
} else if (order.type === 'SELL') {
|
||||||
order.quantity.mul(unitPrice)
|
investment = oldAccumulatedSymbol.investment.minus(
|
||||||
);
|
order.quantity.mul(oldAccumulatedSymbol.averagePrice)
|
||||||
} else if (order.type === 'SELL') {
|
);
|
||||||
const averagePrice = oldAccumulatedSymbol.investment.div(
|
|
||||||
oldAccumulatedSymbol.quantity
|
|
||||||
);
|
|
||||||
investment = oldAccumulatedSymbol.investment.minus(
|
|
||||||
order.quantity.mul(averagePrice)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
investment,
|
investment,
|
||||||
|
averagePrice: newQuantity.gt(0)
|
||||||
|
? investment.div(newQuantity)
|
||||||
|
: new Big(0),
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
dataSource: order.dataSource,
|
dataSource: order.dataSource,
|
||||||
|
dividend: new Big(0),
|
||||||
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
@ -112,11 +114,13 @@ export class PortfolioCalculator {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
|
averagePrice: order.unitPrice,
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
dataSource: order.dataSource,
|
dataSource: order.dataSource,
|
||||||
|
dividend: new Big(0),
|
||||||
fee: order.fee,
|
fee: order.fee,
|
||||||
firstBuyDate: order.date,
|
firstBuyDate: order.date,
|
||||||
investment: unitPrice.mul(order.quantity).mul(factor),
|
investment: order.unitPrice.mul(order.quantity).mul(factor),
|
||||||
quantity: order.quantity.mul(factor),
|
quantity: order.quantity.mul(factor),
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
tags: order.tags,
|
tags: order.tags,
|
||||||
@ -127,22 +131,28 @@ export class PortfolioCalculator {
|
|||||||
symbols[order.symbol] = currentTransactionPointItem;
|
symbols[order.symbol] = currentTransactionPointItem;
|
||||||
|
|
||||||
const items = lastTransactionPoint?.items ?? [];
|
const items = lastTransactionPoint?.items ?? [];
|
||||||
|
|
||||||
const newItems = items.filter(
|
const newItems = items.filter(
|
||||||
(transactionPointItem) => transactionPointItem.symbol !== order.symbol
|
(transactionPointItem) => transactionPointItem.symbol !== order.symbol
|
||||||
);
|
);
|
||||||
|
|
||||||
newItems.push(currentTransactionPointItem);
|
newItems.push(currentTransactionPointItem);
|
||||||
|
|
||||||
newItems.sort((a, b) => {
|
newItems.sort((a, b) => {
|
||||||
return a.symbol?.localeCompare(b.symbol);
|
return a.symbol?.localeCompare(b.symbol);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (lastDate !== currentDate || lastTransactionPoint === null) {
|
if (lastDate !== currentDate || lastTransactionPoint === null) {
|
||||||
lastTransactionPoint = {
|
lastTransactionPoint = {
|
||||||
date: currentDate,
|
date: currentDate,
|
||||||
items: newItems
|
items: newItems
|
||||||
};
|
};
|
||||||
|
|
||||||
this.transactionPoints.push(lastTransactionPoint);
|
this.transactionPoints.push(lastTransactionPoint);
|
||||||
} else {
|
} else {
|
||||||
lastTransactionPoint.items = newItems;
|
lastTransactionPoint.items = newItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDate = currentDate;
|
lastDate = currentDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -440,16 +450,27 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
public async getCurrentPositions(
|
public async getCurrentPositions(
|
||||||
start: Date,
|
start: Date,
|
||||||
end = new Date(Date.now())
|
end?: Date
|
||||||
): Promise<CurrentPositions> {
|
): Promise<CurrentPositions> {
|
||||||
const transactionPointsBeforeEndDate =
|
const lastTransactionPoint = last(this.transactionPoints);
|
||||||
this.transactionPoints?.filter((transactionPoint) => {
|
|
||||||
return isBefore(parseDate(transactionPoint.date), end);
|
|
||||||
}) ?? [];
|
|
||||||
|
|
||||||
if (!transactionPointsBeforeEndDate.length) {
|
let endDate = end;
|
||||||
|
|
||||||
|
if (!endDate) {
|
||||||
|
endDate = new Date(Date.now());
|
||||||
|
|
||||||
|
if (lastTransactionPoint) {
|
||||||
|
endDate = max([endDate, parseDate(lastTransactionPoint.date)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactionPoints = this.transactionPoints?.filter(({ date }) => {
|
||||||
|
return isBefore(parseDate(date), endDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!transactionPoints.length) {
|
||||||
return {
|
return {
|
||||||
currentValue: new Big(0),
|
currentValueInBaseCurrency: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
@ -464,41 +485,40 @@ export class PortfolioCalculator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastTransactionPoint =
|
|
||||||
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
|
|
||||||
|
|
||||||
const currencies: { [symbol: string]: string } = {};
|
const currencies: { [symbol: string]: string } = {};
|
||||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
let dates: Date[] = [];
|
let dates: Date[] = [];
|
||||||
let firstIndex = transactionPointsBeforeEndDate.length;
|
let firstIndex = transactionPoints.length;
|
||||||
let firstTransactionPoint: TransactionPoint = null;
|
let firstTransactionPoint: TransactionPoint = null;
|
||||||
|
|
||||||
dates.push(resetHours(start));
|
dates.push(resetHours(start));
|
||||||
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
|
||||||
|
for (const { currency, dataSource, symbol } of transactionPoints[
|
||||||
|
firstIndex - 1
|
||||||
|
].items) {
|
||||||
dataGatheringItems.push({
|
dataGatheringItems.push({
|
||||||
dataSource: item.dataSource,
|
dataSource,
|
||||||
symbol: item.symbol
|
symbol
|
||||||
});
|
});
|
||||||
|
|
||||||
currencies[item.symbol] = item.currency;
|
currencies[symbol] = currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
|
for (let i = 0; i < transactionPoints.length; i++) {
|
||||||
if (
|
if (
|
||||||
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
|
!isBefore(parseDate(transactionPoints[i].date), start) &&
|
||||||
firstTransactionPoint === null
|
firstTransactionPoint === null
|
||||||
) {
|
) {
|
||||||
firstTransactionPoint = transactionPointsBeforeEndDate[i];
|
firstTransactionPoint = transactionPoints[i];
|
||||||
firstIndex = i;
|
firstIndex = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstTransactionPoint !== null) {
|
if (firstTransactionPoint !== null) {
|
||||||
dates.push(
|
dates.push(resetHours(parseDate(transactionPoints[i].date)));
|
||||||
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dates.push(resetHours(end));
|
dates.push(resetHours(endDate));
|
||||||
|
|
||||||
// Add dates of last week for fallback
|
// Add dates of last week for fallback
|
||||||
dates.push(subDays(resetHours(new Date()), 7));
|
dates.push(subDays(resetHours(new Date()), 7));
|
||||||
@ -525,7 +545,7 @@ export class PortfolioCalculator {
|
|||||||
let exchangeRatesByCurrency =
|
let exchangeRatesByCurrency =
|
||||||
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
||||||
currencies: uniq(Object.values(currencies)),
|
currencies: uniq(Object.values(currencies)),
|
||||||
endDate: endOfDay(end),
|
endDate: endOfDay(endDate),
|
||||||
startDate: parseDate(this.transactionPoints?.[0]?.date),
|
startDate: parseDate(this.transactionPoints?.[0]?.date),
|
||||||
targetCurrency: this.currency
|
targetCurrency: this.currency
|
||||||
});
|
});
|
||||||
@ -561,7 +581,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const endDateString = format(end, DATE_FORMAT);
|
const endDateString = format(endDate, DATE_FORMAT);
|
||||||
|
|
||||||
if (firstIndex > 0) {
|
if (firstIndex > 0) {
|
||||||
firstIndex--;
|
firstIndex--;
|
||||||
@ -573,15 +593,17 @@ export class PortfolioCalculator {
|
|||||||
const errors: ResponseError['errors'] = [];
|
const errors: ResponseError['errors'] = [];
|
||||||
|
|
||||||
for (const item of lastTransactionPoint.items) {
|
for (const item of lastTransactionPoint.items) {
|
||||||
const marketPriceInBaseCurrency = marketSymbolMap[endDateString]?.[
|
const marketPriceInBaseCurrency = (
|
||||||
item.symbol
|
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
|
||||||
]?.mul(
|
).mul(
|
||||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
|
||||||
endDateString
|
endDateString
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
dividend,
|
||||||
|
dividendInBaseCurrency,
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
grossPerformancePercentageWithCurrencyEffect,
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
@ -596,9 +618,9 @@ export class PortfolioCalculator {
|
|||||||
totalInvestment,
|
totalInvestment,
|
||||||
totalInvestmentWithCurrencyEffect
|
totalInvestmentWithCurrencyEffect
|
||||||
} = this.getSymbolMetrics({
|
} = this.getSymbolMetrics({
|
||||||
end,
|
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
start,
|
start,
|
||||||
|
end: endDate,
|
||||||
exchangeRates:
|
exchangeRates:
|
||||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
||||||
symbol: item.symbol
|
symbol: item.symbol
|
||||||
@ -607,11 +629,11 @@ export class PortfolioCalculator {
|
|||||||
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
||||||
|
|
||||||
positions.push({
|
positions.push({
|
||||||
|
dividend,
|
||||||
|
dividendInBaseCurrency,
|
||||||
timeWeightedInvestment,
|
timeWeightedInvestment,
|
||||||
timeWeightedInvestmentWithCurrencyEffect,
|
timeWeightedInvestmentWithCurrencyEffect,
|
||||||
averagePrice: item.quantity.eq(0)
|
averagePrice: item.averagePrice,
|
||||||
? new Big(0)
|
|
||||||
: item.investment.div(item.quantity),
|
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
dataSource: item.dataSource,
|
dataSource: item.dataSource,
|
||||||
fee: item.fee,
|
fee: item.fee,
|
||||||
@ -645,7 +667,10 @@ export class PortfolioCalculator {
|
|||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
tags: item.tags,
|
tags: item.tags,
|
||||||
transactionCount: item.transactionCount
|
transactionCount: item.transactionCount,
|
||||||
|
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
|
||||||
|
item.quantity
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -714,7 +739,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private calculateOverallPerformance(positions: TimelinePosition[]) {
|
private calculateOverallPerformance(positions: TimelinePosition[]) {
|
||||||
let currentValue = new Big(0);
|
let currentValueInBaseCurrency = new Big(0);
|
||||||
let grossPerformance = new Big(0);
|
let grossPerformance = new Big(0);
|
||||||
let grossPerformanceWithCurrencyEffect = new Big(0);
|
let grossPerformanceWithCurrencyEffect = new Big(0);
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
@ -726,14 +751,9 @@ export class PortfolioCalculator {
|
|||||||
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
|
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
|
||||||
|
|
||||||
for (const currentPosition of positions) {
|
for (const currentPosition of positions) {
|
||||||
if (
|
if (currentPosition.valueInBaseCurrency) {
|
||||||
currentPosition.investment &&
|
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
||||||
currentPosition.marketPriceInBaseCurrency
|
currentPosition.valueInBaseCurrency
|
||||||
) {
|
|
||||||
currentValue = currentValue.plus(
|
|
||||||
new Big(currentPosition.marketPriceInBaseCurrency).mul(
|
|
||||||
currentPosition.quantity
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
@ -790,7 +810,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentValue,
|
currentValueInBaseCurrency,
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformanceWithCurrencyEffect,
|
grossPerformanceWithCurrencyEffect,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
@ -819,24 +839,6 @@ export class PortfolioCalculator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFactor(type: TypeOfOrder) {
|
|
||||||
let factor: number;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'BUY':
|
|
||||||
factor = 1;
|
|
||||||
break;
|
|
||||||
case 'SELL':
|
|
||||||
factor = -1;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
factor = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSymbolMetrics({
|
private getSymbolMetrics({
|
||||||
end,
|
end,
|
||||||
exchangeRates,
|
exchangeRates,
|
||||||
@ -859,6 +861,8 @@ export class PortfolioCalculator {
|
|||||||
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
|
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
|
||||||
const currentValues: { [date: string]: Big } = {};
|
const currentValues: { [date: string]: Big } = {};
|
||||||
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
|
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
|
||||||
|
let dividend = new Big(0);
|
||||||
|
let dividendInBaseCurrency = new Big(0);
|
||||||
let fees = new Big(0);
|
let fees = new Big(0);
|
||||||
let feesAtStartDate = new Big(0);
|
let feesAtStartDate = new Big(0);
|
||||||
let feesAtStartDateWithCurrencyEffect = new Big(0);
|
let feesAtStartDateWithCurrencyEffect = new Big(0);
|
||||||
@ -889,13 +893,10 @@ export class PortfolioCalculator {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
|
let totalInvestmentFromBuyTransactions = new Big(0);
|
||||||
|
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
|
||||||
let totalInvestmentWithCurrencyEffect = new Big(0);
|
let totalInvestmentWithCurrencyEffect = new Big(0);
|
||||||
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
|
let totalQuantityFromBuyTransactions = new Big(0);
|
||||||
|
|
||||||
let totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect = new Big(
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
let totalUnits = new Big(0);
|
let totalUnits = new Big(0);
|
||||||
let valueAtStartDate: Big;
|
let valueAtStartDate: Big;
|
||||||
let valueAtStartDateWithCurrencyEffect: Big;
|
let valueAtStartDateWithCurrencyEffect: Big;
|
||||||
@ -911,6 +912,8 @@ export class PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues: {},
|
currentValues: {},
|
||||||
currentValuesWithCurrencyEffect: {},
|
currentValuesWithCurrencyEffect: {},
|
||||||
|
dividend: new Big(0),
|
||||||
|
dividendInBaseCurrency: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
@ -951,6 +954,8 @@ export class PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues: {},
|
currentValues: {},
|
||||||
currentValuesWithCurrencyEffect: {},
|
currentValuesWithCurrencyEffect: {},
|
||||||
|
dividend: new Big(0),
|
||||||
|
dividendInBaseCurrency: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
@ -987,7 +992,7 @@ export class PortfolioCalculator {
|
|||||||
itemType: 'start',
|
itemType: 'start',
|
||||||
name: '',
|
name: '',
|
||||||
quantity: new Big(0),
|
quantity: new Big(0),
|
||||||
type: TypeOfOrder.BUY,
|
type: 'BUY',
|
||||||
unitPrice: unitPriceAtStartDate
|
unitPrice: unitPriceAtStartDate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1001,7 +1006,7 @@ export class PortfolioCalculator {
|
|||||||
itemType: 'end',
|
itemType: 'end',
|
||||||
name: '',
|
name: '',
|
||||||
quantity: new Big(0),
|
quantity: new Big(0),
|
||||||
type: TypeOfOrder.BUY,
|
type: 'BUY',
|
||||||
unitPrice: unitPriceAtEndDate
|
unitPrice: unitPriceAtEndDate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1028,7 +1033,7 @@ export class PortfolioCalculator {
|
|||||||
feeInBaseCurrency: new Big(0),
|
feeInBaseCurrency: new Big(0),
|
||||||
name: '',
|
name: '',
|
||||||
quantity: new Big(0),
|
quantity: new Big(0),
|
||||||
type: TypeOfOrder.BUY,
|
type: 'BUY',
|
||||||
unitPrice:
|
unitPrice:
|
||||||
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
|
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
|
||||||
lastUnitPrice
|
lastUnitPrice
|
||||||
@ -1041,28 +1046,26 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort orders so that the start and end placeholder order are at the right
|
// Sort orders so that the start and end placeholder order are at the correct
|
||||||
// position
|
// position
|
||||||
orders = sortBy(orders, (order) => {
|
orders = sortBy(orders, ({ date, itemType }) => {
|
||||||
let sortIndex = new Date(order.date);
|
let sortIndex = new Date(date);
|
||||||
|
|
||||||
if (order.itemType === 'start') {
|
if (itemType === 'end') {
|
||||||
sortIndex = addMilliseconds(sortIndex, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (order.itemType === 'end') {
|
|
||||||
sortIndex = addMilliseconds(sortIndex, 1);
|
sortIndex = addMilliseconds(sortIndex, 1);
|
||||||
|
} else if (itemType === 'start') {
|
||||||
|
sortIndex = addMilliseconds(sortIndex, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortIndex.getTime();
|
return sortIndex.getTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
const indexOfStartOrder = orders.findIndex((order) => {
|
const indexOfStartOrder = orders.findIndex(({ itemType }) => {
|
||||||
return order.itemType === 'start';
|
return itemType === 'start';
|
||||||
});
|
});
|
||||||
|
|
||||||
const indexOfEndOrder = orders.findIndex((order) => {
|
const indexOfEndOrder = orders.findIndex(({ itemType }) => {
|
||||||
return order.itemType === 'end';
|
return itemType === 'end';
|
||||||
});
|
});
|
||||||
|
|
||||||
let totalInvestmentDays = 0;
|
let totalInvestmentDays = 0;
|
||||||
@ -1125,29 +1128,41 @@ export class PortfolioCalculator {
|
|||||||
valueOfInvestmentBeforeTransactionWithCurrencyEffect;
|
valueOfInvestmentBeforeTransactionWithCurrencyEffect;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transactionInvestment =
|
let transactionInvestment = new Big(0);
|
||||||
order.type === 'BUY'
|
let transactionInvestmentWithCurrencyEffect = new Big(0);
|
||||||
? order.quantity
|
|
||||||
.mul(order.unitPriceInBaseCurrency)
|
|
||||||
.mul(this.getFactor(order.type))
|
|
||||||
: totalUnits.gt(0)
|
|
||||||
? totalInvestment
|
|
||||||
.div(totalUnits)
|
|
||||||
.mul(order.quantity)
|
|
||||||
.mul(this.getFactor(order.type))
|
|
||||||
: new Big(0);
|
|
||||||
|
|
||||||
const transactionInvestmentWithCurrencyEffect =
|
if (order.type === 'BUY') {
|
||||||
order.type === 'BUY'
|
transactionInvestment = order.quantity
|
||||||
? order.quantity
|
.mul(order.unitPriceInBaseCurrency)
|
||||||
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
|
.mul(getFactor(order.type));
|
||||||
.mul(this.getFactor(order.type))
|
|
||||||
: totalUnits.gt(0)
|
transactionInvestmentWithCurrencyEffect = order.quantity
|
||||||
? totalInvestmentWithCurrencyEffect
|
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
|
||||||
.div(totalUnits)
|
.mul(getFactor(order.type));
|
||||||
.mul(order.quantity)
|
|
||||||
.mul(this.getFactor(order.type))
|
totalQuantityFromBuyTransactions =
|
||||||
: new Big(0);
|
totalQuantityFromBuyTransactions.plus(order.quantity);
|
||||||
|
|
||||||
|
totalInvestmentFromBuyTransactions =
|
||||||
|
totalInvestmentFromBuyTransactions.plus(transactionInvestment);
|
||||||
|
|
||||||
|
totalInvestmentFromBuyTransactionsWithCurrencyEffect =
|
||||||
|
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
|
||||||
|
transactionInvestmentWithCurrencyEffect
|
||||||
|
);
|
||||||
|
} else if (order.type === 'SELL') {
|
||||||
|
if (totalUnits.gt(0)) {
|
||||||
|
transactionInvestment = totalInvestment
|
||||||
|
.div(totalUnits)
|
||||||
|
.mul(order.quantity)
|
||||||
|
.mul(getFactor(order.type));
|
||||||
|
transactionInvestmentWithCurrencyEffect =
|
||||||
|
totalInvestmentWithCurrencyEffect
|
||||||
|
.div(totalUnits)
|
||||||
|
.mul(order.quantity)
|
||||||
|
.mul(getFactor(order.type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
console.log('totalInvestment', totalInvestment.toNumber());
|
console.log('totalInvestment', totalInvestment.toNumber());
|
||||||
@ -1201,9 +1216,14 @@ export class PortfolioCalculator {
|
|||||||
order.feeInBaseCurrencyWithCurrencyEffect ?? 0
|
order.feeInBaseCurrencyWithCurrencyEffect ?? 0
|
||||||
);
|
);
|
||||||
|
|
||||||
totalUnits = totalUnits.plus(
|
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
|
||||||
order.quantity.mul(this.getFactor(order.type))
|
|
||||||
);
|
if (order.type === 'DIVIDEND') {
|
||||||
|
dividend = dividend.plus(order.quantity.mul(order.unitPrice));
|
||||||
|
dividendInBaseCurrency = dividendInBaseCurrency.plus(
|
||||||
|
dividend.mul(exchangeRateAtOrderDate ?? 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
|
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
|
||||||
|
|
||||||
@ -1212,14 +1232,14 @@ export class PortfolioCalculator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const grossPerformanceFromSell =
|
const grossPerformanceFromSell =
|
||||||
order.type === TypeOfOrder.SELL
|
order.type === 'SELL'
|
||||||
? order.unitPriceInBaseCurrency
|
? order.unitPriceInBaseCurrency
|
||||||
.minus(lastAveragePrice)
|
.minus(lastAveragePrice)
|
||||||
.mul(order.quantity)
|
.mul(order.quantity)
|
||||||
: new Big(0);
|
: new Big(0);
|
||||||
|
|
||||||
const grossPerformanceFromSellWithCurrencyEffect =
|
const grossPerformanceFromSellWithCurrencyEffect =
|
||||||
order.type === TypeOfOrder.SELL
|
order.type === 'SELL'
|
||||||
? order.unitPriceInBaseCurrencyWithCurrencyEffect
|
? order.unitPriceInBaseCurrencyWithCurrencyEffect
|
||||||
.minus(lastAveragePriceWithCurrencyEffect)
|
.minus(lastAveragePriceWithCurrencyEffect)
|
||||||
.mul(order.quantity)
|
.mul(order.quantity)
|
||||||
@ -1234,35 +1254,21 @@ export class PortfolioCalculator {
|
|||||||
grossPerformanceFromSellWithCurrencyEffect
|
grossPerformanceFromSellWithCurrencyEffect
|
||||||
);
|
);
|
||||||
|
|
||||||
totalInvestmentWithGrossPerformanceFromSell =
|
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0)
|
||||||
totalInvestmentWithGrossPerformanceFromSell
|
|
||||||
.plus(transactionInvestment)
|
|
||||||
.plus(grossPerformanceFromSell);
|
|
||||||
|
|
||||||
totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect =
|
|
||||||
totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect
|
|
||||||
.plus(transactionInvestmentWithCurrencyEffect)
|
|
||||||
.plus(grossPerformanceFromSellWithCurrencyEffect);
|
|
||||||
|
|
||||||
lastAveragePrice = totalUnits.eq(0)
|
|
||||||
? new Big(0)
|
? new Big(0)
|
||||||
: totalInvestmentWithGrossPerformanceFromSell.div(totalUnits);
|
: totalInvestmentFromBuyTransactions.div(
|
||||||
|
totalQuantityFromBuyTransactions
|
||||||
|
);
|
||||||
|
|
||||||
lastAveragePriceWithCurrencyEffect = totalUnits.eq(0)
|
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq(
|
||||||
|
0
|
||||||
|
)
|
||||||
? new Big(0)
|
? new Big(0)
|
||||||
: totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.div(
|
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div(
|
||||||
totalUnits
|
totalQuantityFromBuyTransactions
|
||||||
);
|
);
|
||||||
|
|
||||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
console.log(
|
|
||||||
'totalInvestmentWithGrossPerformanceFromSell',
|
|
||||||
totalInvestmentWithGrossPerformanceFromSell.toNumber()
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect',
|
|
||||||
totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.toNumber()
|
|
||||||
);
|
|
||||||
console.log(
|
console.log(
|
||||||
'grossPerformanceFromSells',
|
'grossPerformanceFromSells',
|
||||||
grossPerformanceFromSells.toNumber()
|
grossPerformanceFromSells.toNumber()
|
||||||
@ -1296,7 +1302,7 @@ export class PortfolioCalculator {
|
|||||||
grossPerformanceWithCurrencyEffect;
|
grossPerformanceWithCurrencyEffect;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i > indexOfStartOrder) {
|
if (i > indexOfStartOrder && ['BUY', 'SELL'].includes(order.type)) {
|
||||||
// Only consider periods with an investment for the calculation of
|
// Only consider periods with an investment for the calculation of
|
||||||
// the time weighted investment
|
// the time weighted investment
|
||||||
if (valueOfInvestmentBeforeTransaction.gt(0)) {
|
if (valueOfInvestmentBeforeTransaction.gt(0)) {
|
||||||
@ -1308,11 +1314,10 @@ export class PortfolioCalculator {
|
|||||||
orderDate,
|
orderDate,
|
||||||
previousOrderDate
|
previousOrderDate
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set to at least 1 day, otherwise the transactions on the same day
|
|
||||||
// would not be considered in the time weighted calculation
|
|
||||||
if (daysSinceLastOrder <= 0) {
|
if (daysSinceLastOrder <= 0) {
|
||||||
daysSinceLastOrder = 1;
|
// The time between two activities on the same day is unknown
|
||||||
|
// -> Set it to the smallest floating point number greater than 0
|
||||||
|
daysSinceLastOrder = Number.EPSILON;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum up the total investment days since the start date to calculate
|
// Sum up the total investment days since the start date to calculate
|
||||||
@ -1490,6 +1495,7 @@ export class PortfolioCalculator {
|
|||||||
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
|
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
|
||||||
2
|
2
|
||||||
)}
|
)}
|
||||||
|
Total dividend: ${dividend.toFixed(2)}
|
||||||
Gross performance: ${totalGrossPerformance.toFixed(
|
Gross performance: ${totalGrossPerformance.toFixed(
|
||||||
2
|
2
|
||||||
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
|
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
|
||||||
@ -1514,6 +1520,8 @@ export class PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues,
|
currentValues,
|
||||||
currentValuesWithCurrencyEffect,
|
currentValuesWithCurrencyEffect,
|
||||||
|
dividend,
|
||||||
|
dividendInBaseCurrency,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
grossPerformancePercentageWithCurrencyEffect,
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
initialValue,
|
initialValue,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AccessService } from '@ghostfolio/api/app/access/access.service';
|
import { AccessService } from '@ghostfolio/api/app/access/access.service';
|
||||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import {
|
import {
|
||||||
@ -11,6 +12,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
|
|||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CURRENCY,
|
DEFAULT_CURRENCY,
|
||||||
HEADER_KEY_IMPERSONATION
|
HEADER_KEY_IMPERSONATION
|
||||||
@ -18,6 +20,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioDividends,
|
PortfolioDividends,
|
||||||
|
PortfolioHoldingsResponse,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
@ -28,6 +31,7 @@ import type {
|
|||||||
GroupBy,
|
GroupBy,
|
||||||
RequestWithUser
|
RequestWithUser
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -56,6 +60,8 @@ export class PortfolioController {
|
|||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
|
private readonly impersonationService: ImpersonationService,
|
||||||
|
private readonly orderService: OrderService,
|
||||||
private readonly portfolioService: PortfolioService,
|
private readonly portfolioService: PortfolioService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
@ -70,8 +76,11 @@ export class PortfolioController {
|
|||||||
@Query('accounts') filterByAccounts?: string,
|
@Query('accounts') filterByAccounts?: string,
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string,
|
||||||
|
@Query('withLiabilities') withLiabilitiesParam = 'false'
|
||||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||||
|
const withLiabilities = withLiabilitiesParam === 'true';
|
||||||
|
|
||||||
let hasDetails = true;
|
let hasDetails = true;
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
const hasReadRestrictedAccessPermission =
|
const hasReadRestrictedAccessPermission =
|
||||||
@ -90,21 +99,15 @@ export class PortfolioController {
|
|||||||
filterByTags
|
filterByTags
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { accounts, hasErrors, holdings, platforms, summary } =
|
||||||
accounts,
|
await this.portfolioService.getDetails({
|
||||||
filteredValueInBaseCurrency,
|
dateRange,
|
||||||
filteredValueInPercentage,
|
filters,
|
||||||
hasErrors,
|
impersonationId,
|
||||||
holdings,
|
withLiabilities,
|
||||||
platforms,
|
userId: this.request.user.id,
|
||||||
summary,
|
withSummary: true
|
||||||
totalValueInBaseCurrency
|
});
|
||||||
} = await this.portfolioService.getDetails({
|
|
||||||
dateRange,
|
|
||||||
filters,
|
|
||||||
impersonationId,
|
|
||||||
userId: this.request.user.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||||
hasError = true;
|
hasError = true;
|
||||||
@ -117,27 +120,23 @@ export class PortfolioController {
|
|||||||
this.userService.isRestrictedView(this.request.user)
|
this.userService.isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
const totalInvestment = Object.values(holdings)
|
const totalInvestment = Object.values(holdings)
|
||||||
.map((portfolioPosition) => {
|
.map(({ investment }) => {
|
||||||
return portfolioPosition.investment;
|
return investment;
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const totalValue = Object.values(holdings)
|
const totalValue = Object.values(holdings)
|
||||||
.map((portfolioPosition) => {
|
.filter(({ assetClass, assetSubClass }) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return assetClass !== 'CASH' && assetSubClass !== 'CASH';
|
||||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
})
|
||||||
portfolioPosition.currency,
|
.map(({ valueInBaseCurrency }) => {
|
||||||
this.request.user.Settings.settings.baseCurrency
|
return valueInBaseCurrency;
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||||
portfolioPosition.grossPerformance = null;
|
|
||||||
portfolioPosition.investment =
|
portfolioPosition.investment =
|
||||||
portfolioPosition.investment / totalInvestment;
|
portfolioPosition.investment / totalInvestment;
|
||||||
portfolioPosition.netPerformance = null;
|
|
||||||
portfolioPosition.quantity = null;
|
|
||||||
portfolioPosition.valueInPercentage =
|
portfolioPosition.valueInPercentage =
|
||||||
portfolioPosition.valueInBaseCurrency / totalValue;
|
portfolioPosition.valueInBaseCurrency / totalValue;
|
||||||
}
|
}
|
||||||
@ -163,19 +162,21 @@ export class PortfolioController {
|
|||||||
'currentGrossPerformanceWithCurrencyEffect',
|
'currentGrossPerformanceWithCurrencyEffect',
|
||||||
'currentNetPerformance',
|
'currentNetPerformance',
|
||||||
'currentNetPerformanceWithCurrencyEffect',
|
'currentNetPerformanceWithCurrencyEffect',
|
||||||
|
'currentNetWorth',
|
||||||
'currentValue',
|
'currentValue',
|
||||||
'dividend',
|
'dividendInBaseCurrency',
|
||||||
'emergencyFund',
|
'emergencyFund',
|
||||||
'excludedAccountsAndActivities',
|
'excludedAccountsAndActivities',
|
||||||
'fees',
|
'fees',
|
||||||
|
'filteredValueInBaseCurrency',
|
||||||
'fireWealth',
|
'fireWealth',
|
||||||
'interest',
|
'interest',
|
||||||
'items',
|
'items',
|
||||||
'liabilities',
|
'liabilities',
|
||||||
'netWorth',
|
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
'totalInvestment',
|
'totalInvestment',
|
||||||
'totalSell'
|
'totalSell',
|
||||||
|
'totalValueInBaseCurrency'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,12 +203,9 @@ export class PortfolioController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
filteredValueInBaseCurrency,
|
|
||||||
filteredValueInPercentage,
|
|
||||||
hasError,
|
hasError,
|
||||||
holdings,
|
holdings,
|
||||||
platforms,
|
platforms,
|
||||||
totalValueInBaseCurrency,
|
|
||||||
summary: portfolioSummary
|
summary: portfolioSummary
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -234,11 +232,21 @@ export class PortfolioController {
|
|||||||
filterByTags
|
filterByTags
|
||||||
});
|
});
|
||||||
|
|
||||||
let dividends = await this.portfolioService.getDividends({
|
const impersonationUserId =
|
||||||
dateRange,
|
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||||
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
|
||||||
|
const { activities } = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
groupBy,
|
userCurrency,
|
||||||
impersonationId
|
userId: impersonationUserId || this.request.user.id,
|
||||||
|
types: ['DIVIDEND']
|
||||||
|
});
|
||||||
|
|
||||||
|
let dividends = await this.portfolioService.getDividends({
|
||||||
|
activities,
|
||||||
|
dateRange,
|
||||||
|
groupBy
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -268,6 +276,33 @@ export class PortfolioController {
|
|||||||
return { dividends };
|
return { dividends };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('holdings')
|
||||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async getHoldings(
|
||||||
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||||
|
@Query('accounts') filterByAccounts?: string,
|
||||||
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('query') filterBySearchQuery?: string,
|
||||||
|
@Query('tags') filterByTags?: string
|
||||||
|
): Promise<PortfolioHoldingsResponse> {
|
||||||
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
|
filterByAccounts,
|
||||||
|
filterByAssetClasses,
|
||||||
|
filterBySearchQuery,
|
||||||
|
filterByTags
|
||||||
|
});
|
||||||
|
|
||||||
|
const { holdings } = await this.portfolioService.getDetails({
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { holdings: Object.values(holdings) };
|
||||||
|
}
|
||||||
|
|
||||||
@Get('investments')
|
@Get('investments')
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
public async getInvestments(
|
public async getInvestments(
|
||||||
@ -345,8 +380,12 @@ export class PortfolioController {
|
|||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string,
|
@Query('tags') filterByTags?: string,
|
||||||
@Query('withExcludedAccounts') withExcludedAccounts = false
|
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false',
|
||||||
|
@Query('withItems') withItemsParam = 'false'
|
||||||
): Promise<PortfolioPerformanceResponse> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
|
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||||
|
const withItems = withItemsParam === 'true';
|
||||||
|
|
||||||
const hasReadRestrictedAccessPermission =
|
const hasReadRestrictedAccessPermission =
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
this.userService.hasReadRestrictedAccessPermission({
|
||||||
impersonationId,
|
impersonationId,
|
||||||
@ -364,6 +403,7 @@ export class PortfolioController {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts,
|
||||||
|
withItems,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -428,6 +468,10 @@ export class PortfolioController {
|
|||||||
return nullifyValuesInObject(item, ['totalInvestment', 'value']);
|
return nullifyValuesInObject(item, ['totalInvestment', 'value']);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
performanceInformation.performance = nullifyValuesInObject(
|
||||||
|
performanceInformation.performance,
|
||||||
|
['currentNetPerformance', 'currentNetPerformancePercent']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return performanceInformation;
|
return performanceInformation;
|
||||||
@ -482,7 +526,6 @@ export class PortfolioController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { holdings } = await this.portfolioService.getDetails({
|
const { holdings } = await this.portfolioService.getDetails({
|
||||||
dateRange: 'max',
|
|
||||||
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
||||||
impersonationId: access.userId,
|
impersonationId: access.userId,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
@ -514,7 +557,8 @@ export class PortfolioController {
|
|||||||
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
|
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
|
||||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||||
name: portfolioPosition.name,
|
name: portfolioPosition.name,
|
||||||
netPerformancePercent: portfolioPosition.netPerformancePercent,
|
netPerformancePercentWithCurrencyEffect:
|
||||||
|
portfolioPosition.netPerformancePercentWithCurrencyEffect,
|
||||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||||
symbol: portfolioPosition.symbol,
|
symbol: portfolioPosition.symbol,
|
||||||
url: portfolioPosition.url,
|
url: portfolioPosition.url,
|
||||||
|
@ -12,6 +12,7 @@ import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impe
|
|||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
|
@ -7,6 +7,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
|
|||||||
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
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 { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
||||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
|
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
|
||||||
@ -23,7 +24,12 @@ import {
|
|||||||
MAX_CHART_ITEMS,
|
MAX_CHART_ITEMS,
|
||||||
UNKNOWN_KEY
|
UNKNOWN_KEY
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
|
getAllActivityTypes,
|
||||||
|
getSum,
|
||||||
|
parseDate
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
@ -48,6 +54,7 @@ import type {
|
|||||||
RequestWithUser,
|
RequestWithUser,
|
||||||
UserWithSettings
|
UserWithSettings
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import {
|
import {
|
||||||
@ -61,6 +68,7 @@ import {
|
|||||||
Tag
|
Tag
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
import { isUUID } from 'class-validator';
|
||||||
import {
|
import {
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
format,
|
format,
|
||||||
@ -118,7 +126,7 @@ export class PortfolioService {
|
|||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<AccountWithValue[]> {
|
}): Promise<AccountWithValue[]> {
|
||||||
const where: Prisma.AccountWhereInput = { userId: userId };
|
const where: Prisma.AccountWhereInput = { userId };
|
||||||
|
|
||||||
const accountFilter = filters?.find(({ type }) => {
|
const accountFilter = filters?.find(({ type }) => {
|
||||||
return type === 'ACCOUNT';
|
return type === 'ACCOUNT';
|
||||||
@ -215,29 +223,18 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getDividends({
|
public async getDividends({
|
||||||
dateRange,
|
activities,
|
||||||
filters,
|
dateRange = 'max',
|
||||||
groupBy,
|
groupBy
|
||||||
impersonationId
|
|
||||||
}: {
|
}: {
|
||||||
dateRange: DateRange;
|
activities: Activity[];
|
||||||
filters?: Filter[];
|
dateRange?: DateRange;
|
||||||
groupBy?: GroupBy;
|
groupBy?: GroupBy;
|
||||||
impersonationId: string;
|
|
||||||
}): Promise<InvestmentItem[]> {
|
}): Promise<InvestmentItem[]> {
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
let dividends = activities.map(({ date, valueInBaseCurrency }) => {
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
|
||||||
filters,
|
|
||||||
userId,
|
|
||||||
types: ['DIVIDEND'],
|
|
||||||
userCurrency: this.request.user.Settings.settings.baseCurrency
|
|
||||||
});
|
|
||||||
|
|
||||||
let dividends = activities.map((dividend) => {
|
|
||||||
return {
|
return {
|
||||||
date: format(dividend.date, DATE_FORMAT),
|
date: format(date, DATE_FORMAT),
|
||||||
investment: dividend.valueInBaseCurrency
|
investment: valueInBaseCurrency
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -274,7 +271,8 @@ export class PortfolioService {
|
|||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
includeDrafts: true
|
includeDrafts: true,
|
||||||
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints.length === 0) {
|
if (transactionPoints.length === 0) {
|
||||||
@ -339,13 +337,17 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false
|
withExcludedAccounts = false,
|
||||||
|
withLiabilities = false,
|
||||||
|
withSummary = false
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
|
withLiabilities?: boolean;
|
||||||
|
withSummary?: boolean;
|
||||||
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
@ -355,11 +357,16 @@ export class PortfolioService {
|
|||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { activities, portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts
|
withExcludedAccounts,
|
||||||
|
types: withLiabilities
|
||||||
|
? undefined
|
||||||
|
: getAllActivityTypes().filter((activityType) => {
|
||||||
|
return activityType !== 'LIABILITY';
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
@ -385,9 +392,11 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const holdings: PortfolioDetails['holdings'] = {};
|
const holdings: PortfolioDetails['holdings'] = {};
|
||||||
const totalValueInBaseCurrency = currentPositions.currentValue.plus(
|
|
||||||
cashDetails.balanceInBaseCurrency
|
const totalValueInBaseCurrency =
|
||||||
);
|
currentPositions.currentValueInBaseCurrency.plus(
|
||||||
|
cashDetails.balanceInBaseCurrency
|
||||||
|
);
|
||||||
|
|
||||||
const isFilteredByAccount =
|
const isFilteredByAccount =
|
||||||
filters?.some((filter) => {
|
filters?.some((filter) => {
|
||||||
@ -396,7 +405,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
let filteredValueInBaseCurrency = isFilteredByAccount
|
let filteredValueInBaseCurrency = isFilteredByAccount
|
||||||
? totalValueInBaseCurrency
|
? totalValueInBaseCurrency
|
||||||
: currentPositions.currentValue;
|
: currentPositions.currentValueInBaseCurrency;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
filters?.length === 0 ||
|
filters?.length === 0 ||
|
||||||
@ -419,7 +428,7 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes({ items: dataGatheringItems }),
|
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
||||||
this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
|
this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -433,15 +442,34 @@ export class PortfolioService {
|
|||||||
portfolioItemsNow[position.symbol] = position;
|
portfolioItemsNow[position.symbol] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of currentPositions.positions) {
|
for (const {
|
||||||
if (item.quantity.lte(0)) {
|
currency,
|
||||||
|
dividend,
|
||||||
|
firstBuyDate,
|
||||||
|
grossPerformance,
|
||||||
|
grossPerformanceWithCurrencyEffect,
|
||||||
|
grossPerformancePercentage,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
|
investment,
|
||||||
|
marketPrice,
|
||||||
|
marketPriceInBaseCurrency,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercentage,
|
||||||
|
netPerformancePercentageWithCurrencyEffect,
|
||||||
|
netPerformanceWithCurrencyEffect,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
tags,
|
||||||
|
transactionCount,
|
||||||
|
valueInBaseCurrency
|
||||||
|
} of currentPositions.positions) {
|
||||||
|
if (quantity.eq(0)) {
|
||||||
// Ignore positions without any quantity
|
// Ignore positions without any quantity
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = item.quantity.mul(item.marketPriceInBaseCurrency ?? 0);
|
const symbolProfile = symbolProfileMap[symbol];
|
||||||
const symbolProfile = symbolProfileMap[item.symbol];
|
const dataProviderResponse = dataProviderResponses[symbol];
|
||||||
const dataProviderResponse = dataProviderResponses[item.symbol];
|
|
||||||
|
|
||||||
const markets: PortfolioPosition['markets'] = {
|
const markets: PortfolioPosition['markets'] = {
|
||||||
[UNKNOWN_KEY]: 0,
|
[UNKNOWN_KEY]: 0,
|
||||||
@ -505,42 +533,50 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
|
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
|
||||||
.plus(value)
|
.plus(valueInBaseCurrency)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
|
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
|
||||||
.plus(value)
|
.plus(valueInBaseCurrency)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
holdings[item.symbol] = {
|
holdings[symbol] = {
|
||||||
|
currency,
|
||||||
markets,
|
markets,
|
||||||
marketsAdvanced,
|
marketsAdvanced,
|
||||||
|
marketPrice,
|
||||||
|
symbol,
|
||||||
|
tags,
|
||||||
|
transactionCount,
|
||||||
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
|
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
|
||||||
? 0
|
? 0
|
||||||
: value.div(filteredValueInBaseCurrency).toNumber(),
|
: valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(),
|
||||||
assetClass: symbolProfile.assetClass,
|
assetClass: symbolProfile.assetClass,
|
||||||
assetSubClass: symbolProfile.assetSubClass,
|
assetSubClass: symbolProfile.assetSubClass,
|
||||||
countries: symbolProfile.countries,
|
countries: symbolProfile.countries,
|
||||||
currency: item.currency,
|
|
||||||
dataSource: symbolProfile.dataSource,
|
dataSource: symbolProfile.dataSource,
|
||||||
dateOfFirstActivity: parseDate(item.firstBuyDate),
|
dateOfFirstActivity: parseDate(firstBuyDate),
|
||||||
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
dividend: dividend?.toNumber() ?? 0,
|
||||||
grossPerformancePercent:
|
grossPerformance: grossPerformance?.toNumber() ?? 0,
|
||||||
item.grossPerformancePercentage?.toNumber() ?? 0,
|
grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0,
|
||||||
investment: item.investment.toNumber(),
|
grossPerformancePercentWithCurrencyEffect:
|
||||||
marketPrice: item.marketPrice,
|
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
|
grossPerformanceWithCurrencyEffect:
|
||||||
|
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
|
investment: investment.toNumber(),
|
||||||
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
||||||
name: symbolProfile.name,
|
name: symbolProfile.name,
|
||||||
netPerformance: item.netPerformance?.toNumber() ?? 0,
|
netPerformance: netPerformance?.toNumber() ?? 0,
|
||||||
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
|
netPerformancePercent: netPerformancePercentage?.toNumber() ?? 0,
|
||||||
quantity: item.quantity.toNumber(),
|
netPerformancePercentWithCurrencyEffect:
|
||||||
|
netPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
|
netPerformanceWithCurrencyEffect:
|
||||||
|
netPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
|
quantity: quantity.toNumber(),
|
||||||
sectors: symbolProfile.sectors,
|
sectors: symbolProfile.sectors,
|
||||||
symbol: item.symbol,
|
|
||||||
tags: item.tags,
|
|
||||||
transactionCount: item.transactionCount,
|
|
||||||
url: symbolProfile.url,
|
url: symbolProfile.url,
|
||||||
valueInBaseCurrency: value.toNumber()
|
valueInBaseCurrency: valueInBaseCurrency.toNumber()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,8 +597,8 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
|
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
|
||||||
|
activities,
|
||||||
filters,
|
filters,
|
||||||
orders,
|
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
@ -604,28 +640,29 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = await this.getSummary({
|
let summary: PortfolioSummary;
|
||||||
impersonationId,
|
|
||||||
userCurrency,
|
if (withSummary) {
|
||||||
userId,
|
summary = await this.getSummary({
|
||||||
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
filteredValueInBaseCurrency,
|
||||||
emergencyFundPositionsValueInBaseCurrency:
|
holdings,
|
||||||
this.getEmergencyFundPositionsValueInBaseCurrency({
|
impersonationId,
|
||||||
holdings
|
userCurrency,
|
||||||
})
|
userId,
|
||||||
});
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
||||||
|
emergencyFundPositionsValueInBaseCurrency:
|
||||||
|
this.getEmergencyFundPositionsValueInBaseCurrency({
|
||||||
|
holdings
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
holdings,
|
holdings,
|
||||||
platforms,
|
platforms,
|
||||||
summary,
|
summary,
|
||||||
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
hasErrors: currentPositions.hasErrors
|
||||||
filteredValueInPercentage: summary.netWorth
|
|
||||||
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
|
|
||||||
: 0,
|
|
||||||
hasErrors: currentPositions.hasErrors,
|
|
||||||
totalValueInBaseCurrency: summary.netWorth
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,6 +693,7 @@ export class PortfolioService {
|
|||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
return {
|
return {
|
||||||
tags,
|
tags,
|
||||||
|
accounts: [],
|
||||||
averagePrice: undefined,
|
averagePrice: undefined,
|
||||||
dataProviderInfo: undefined,
|
dataProviderInfo: undefined,
|
||||||
dividendInBaseCurrency: undefined,
|
dividendInBaseCurrency: undefined,
|
||||||
@ -690,7 +728,7 @@ export class PortfolioService {
|
|||||||
.filter((order) => {
|
.filter((order) => {
|
||||||
tags = tags.concat(order.tags);
|
tags = tags.concat(order.tags);
|
||||||
|
|
||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
|
||||||
})
|
})
|
||||||
.map((order) => ({
|
.map((order) => ({
|
||||||
currency: order.SymbolProfile.currency,
|
currency: order.SymbolProfile.currency,
|
||||||
@ -731,6 +769,7 @@ export class PortfolioService {
|
|||||||
averagePrice,
|
averagePrice,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
dividendInBaseCurrency,
|
||||||
fee,
|
fee,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
@ -738,15 +777,14 @@ export class PortfolioService {
|
|||||||
transactionCount
|
transactionCount
|
||||||
} = position;
|
} = position;
|
||||||
|
|
||||||
const dividendInBaseCurrency = getSum(
|
const accounts: PortfolioPositionDetail['accounts'] = uniqBy(
|
||||||
orders
|
orders.filter(({ Account }) => {
|
||||||
.filter(({ type }) => {
|
return Account;
|
||||||
return type === 'DIVIDEND';
|
}),
|
||||||
})
|
'Account.id'
|
||||||
.map(({ valueInBaseCurrency }) => {
|
).map(({ Account }) => {
|
||||||
return new Big(valueInBaseCurrency);
|
return Account;
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
[{ dataSource, symbol: aSymbol }],
|
[{ dataSource, symbol: aSymbol }],
|
||||||
@ -781,9 +819,7 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (currentSymbol) {
|
if (currentSymbol) {
|
||||||
currentAveragePrice = currentSymbol.quantity.eq(0)
|
currentAveragePrice = currentSymbol.averagePrice.toNumber();
|
||||||
? 0
|
|
||||||
: currentSymbol.investment.div(currentSymbol.quantity).toNumber();
|
|
||||||
currentQuantity = currentSymbol.quantity.toNumber();
|
currentQuantity = currentSymbol.quantity.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -811,6 +847,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
accounts,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
@ -851,6 +888,7 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const currentData = await this.dataProviderService.getQuotes({
|
const currentData = await this.dataProviderService.getQuotes({
|
||||||
|
user,
|
||||||
items: [{ dataSource: DataSource.YAHOO, symbol: aSymbol }]
|
items: [{ dataSource: DataSource.YAHOO, symbol: aSymbol }]
|
||||||
});
|
});
|
||||||
const marketPrice = currentData[aSymbol]?.marketPrice;
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||||
@ -893,6 +931,7 @@ export class PortfolioService {
|
|||||||
orders,
|
orders,
|
||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
tags,
|
tags,
|
||||||
|
accounts: [],
|
||||||
averagePrice: 0,
|
averagePrice: 0,
|
||||||
dataProviderInfo: undefined,
|
dataProviderInfo: undefined,
|
||||||
dividendInBaseCurrency: 0,
|
dividendInBaseCurrency: 0,
|
||||||
@ -928,11 +967,13 @@ export class PortfolioService {
|
|||||||
return type === 'SEARCH_QUERY';
|
return type === 'SEARCH_QUERY';
|
||||||
})?.id;
|
})?.id;
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
|
const user = await this.userService.user({ id: userId });
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
userId
|
userId,
|
||||||
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (transactionPoints?.length <= 0) {
|
||||||
@ -968,7 +1009,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes({ items: dataGatheringItems }),
|
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
||||||
this.symbolProfileService.getSymbolProfiles(
|
this.symbolProfileService.getSymbolProfiles(
|
||||||
positions.map(({ dataSource, symbol }) => {
|
positions.map(({ dataSource, symbol }) => {
|
||||||
return { dataSource, symbol };
|
return { dataSource, symbol };
|
||||||
@ -1062,13 +1103,15 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false
|
withExcludedAccounts = false,
|
||||||
|
withItems = false
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
|
withItems?: boolean;
|
||||||
}): Promise<PortfolioPerformanceResponse> {
|
}): Promise<PortfolioPerformanceResponse> {
|
||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
@ -1103,7 +1146,8 @@ export class PortfolioService {
|
|||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts
|
withExcludedAccounts,
|
||||||
|
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
@ -1147,7 +1191,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||||
const {
|
const {
|
||||||
currentValue,
|
currentValueInBaseCurrency,
|
||||||
errors,
|
errors,
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
@ -1242,7 +1286,7 @@ export class PortfolioService {
|
|||||||
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
|
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
|
||||||
currentNetPerformanceWithCurrencyEffect:
|
currentNetPerformanceWithCurrencyEffect:
|
||||||
currentNetPerformanceWithCurrencyEffect.toNumber(),
|
currentNetPerformanceWithCurrencyEffect.toNumber(),
|
||||||
currentValue: currentValue.toNumber(),
|
currentValue: currentValueInBaseCurrency.toNumber(),
|
||||||
totalInvestment: totalInvestment.toNumber()
|
totalInvestment: totalInvestment.toNumber()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1253,9 +1297,10 @@ export class PortfolioService {
|
|||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
const userCurrency = this.getUserCurrency(user);
|
const userCurrency = this.getUserCurrency(user);
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { activities, portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
userId
|
userId,
|
||||||
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
@ -1284,7 +1329,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { accounts } = await this.getValueOfAccountsAndPlatforms({
|
const { accounts } = await this.getValueOfAccountsAndPlatforms({
|
||||||
orders,
|
activities,
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
@ -1294,7 +1339,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
accountClusterRisk: isEmpty(orders)
|
accountClusterRisk: isEmpty(activities)
|
||||||
? undefined
|
? undefined
|
||||||
: await this.rulesService.evaluate(
|
: await this.rulesService.evaluate(
|
||||||
[
|
[
|
||||||
@ -1309,7 +1354,7 @@ export class PortfolioService {
|
|||||||
],
|
],
|
||||||
userSettings
|
userSettings
|
||||||
),
|
),
|
||||||
currencyClusterRisk: isEmpty(orders)
|
currencyClusterRisk: isEmpty(activities)
|
||||||
? undefined
|
? undefined
|
||||||
: await this.rulesService.evaluate(
|
: await this.rulesService.evaluate(
|
||||||
[
|
[
|
||||||
@ -1338,7 +1383,7 @@ export class PortfolioService {
|
|||||||
new FeeRatioInitialInvestment(
|
new FeeRatioInitialInvestment(
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
currentPositions.totalInvestment.toNumber(),
|
currentPositions.totalInvestment.toNumber(),
|
||||||
this.getFees({ userCurrency, activities: orders }).toNumber()
|
this.getFees({ activities, userCurrency }).toNumber()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
userSettings
|
userSettings
|
||||||
@ -1545,29 +1590,26 @@ export class PortfolioService {
|
|||||||
|
|
||||||
private getFees({
|
private getFees({
|
||||||
activities,
|
activities,
|
||||||
date = new Date(0),
|
|
||||||
userCurrency
|
userCurrency
|
||||||
}: {
|
}: {
|
||||||
activities: OrderWithAccount[];
|
activities: OrderWithAccount[];
|
||||||
date?: Date;
|
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
}) {
|
}) {
|
||||||
return activities
|
return getSum(
|
||||||
.filter((activity) => {
|
activities
|
||||||
// Filter out all activities before given date (drafts)
|
.filter(({ isDraft }) => {
|
||||||
return isBefore(date, new Date(activity.date));
|
return isDraft === false;
|
||||||
})
|
})
|
||||||
.map(({ fee, SymbolProfile }) => {
|
.map(({ fee, SymbolProfile }) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return new Big(
|
||||||
fee,
|
this.exchangeRateDataService.toCurrency(
|
||||||
SymbolProfile.currency,
|
fee,
|
||||||
userCurrency
|
SymbolProfile.currency,
|
||||||
);
|
userCurrency
|
||||||
})
|
)
|
||||||
.reduce(
|
);
|
||||||
(previous, current) => new Big(previous).plus(current),
|
})
|
||||||
new Big(0)
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInitialCashPosition({
|
private getInitialCashPosition({
|
||||||
@ -1585,14 +1627,19 @@ export class PortfolioService {
|
|||||||
countries: [],
|
countries: [],
|
||||||
dataSource: undefined,
|
dataSource: undefined,
|
||||||
dateOfFirstActivity: undefined,
|
dateOfFirstActivity: undefined,
|
||||||
|
dividend: 0,
|
||||||
grossPerformance: 0,
|
grossPerformance: 0,
|
||||||
grossPerformancePercent: 0,
|
grossPerformancePercent: 0,
|
||||||
|
grossPerformancePercentWithCurrencyEffect: 0,
|
||||||
|
grossPerformanceWithCurrencyEffect: 0,
|
||||||
investment: balance,
|
investment: balance,
|
||||||
marketPrice: 0,
|
marketPrice: 0,
|
||||||
marketState: 'open',
|
marketState: 'open',
|
||||||
name: currency,
|
name: currency,
|
||||||
netPerformance: 0,
|
netPerformance: 0,
|
||||||
netPerformancePercent: 0,
|
netPerformancePercent: 0,
|
||||||
|
netPerformancePercentWithCurrencyEffect: 0,
|
||||||
|
netPerformanceWithCurrencyEffect: 0,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
sectors: [],
|
sectors: [],
|
||||||
symbol: currency,
|
symbol: currency,
|
||||||
@ -1673,12 +1720,16 @@ export class PortfolioService {
|
|||||||
private async getSummary({
|
private async getSummary({
|
||||||
balanceInBaseCurrency,
|
balanceInBaseCurrency,
|
||||||
emergencyFundPositionsValueInBaseCurrency,
|
emergencyFundPositionsValueInBaseCurrency,
|
||||||
|
filteredValueInBaseCurrency,
|
||||||
|
holdings,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
balanceInBaseCurrency: number;
|
balanceInBaseCurrency: number;
|
||||||
emergencyFundPositionsValueInBaseCurrency: number;
|
emergencyFundPositionsValueInBaseCurrency: number;
|
||||||
|
filteredValueInBaseCurrency: Big;
|
||||||
|
holdings: PortfolioDetails['holdings'];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -1692,57 +1743,87 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
userCurrency,
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
let { activities: excludedActivities } = await this.orderService.getOrders({
|
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts: true
|
withExcludedAccounts: true
|
||||||
});
|
});
|
||||||
|
|
||||||
excludedActivities = excludedActivities.filter(({ Account: account }) => {
|
const excludedActivities: Activity[] = [];
|
||||||
return account?.isExcluded ?? false;
|
const nonExcludedActivities: Activity[] = [];
|
||||||
});
|
|
||||||
|
for (const activity of activities) {
|
||||||
|
if (activity.Account?.isExcluded) {
|
||||||
|
excludedActivities.push(activity);
|
||||||
|
} else {
|
||||||
|
nonExcludedActivities.push(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dividendInBaseCurrency = getSum(
|
||||||
|
(
|
||||||
|
await this.getDividends({
|
||||||
|
activities: activities.filter(({ type }) => {
|
||||||
|
return type === 'DIVIDEND';
|
||||||
|
})
|
||||||
|
})
|
||||||
|
).map(({ investment }) => {
|
||||||
|
return new Big(investment);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const dividend = this.getSumOfActivityType({
|
|
||||||
activities,
|
|
||||||
userCurrency,
|
|
||||||
activityType: 'DIVIDEND'
|
|
||||||
}).toNumber();
|
|
||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
Math.max(
|
Math.max(
|
||||||
emergencyFundPositionsValueInBaseCurrency,
|
emergencyFundPositionsValueInBaseCurrency,
|
||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
||||||
const firstOrderDate = activities[0]?.date;
|
const firstOrderDate = activities[0]?.date;
|
||||||
|
|
||||||
const interest = this.getSumOfActivityType({
|
const interest = this.getSumOfActivityType({
|
||||||
activities,
|
activities,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
activityType: 'INTEREST'
|
activityType: 'INTEREST'
|
||||||
}).toNumber();
|
}).toNumber();
|
||||||
const items = this.getSumOfActivityType({
|
|
||||||
activities,
|
const items = getSum(
|
||||||
userCurrency,
|
Object.keys(holdings)
|
||||||
activityType: 'ITEM'
|
.filter((symbol) => {
|
||||||
}).toNumber();
|
return (
|
||||||
const liabilities = this.getSumOfActivityType({
|
isUUID(symbol) &&
|
||||||
activities,
|
holdings[symbol].dataSource === 'MANUAL' &&
|
||||||
userCurrency,
|
holdings[symbol].valueInBaseCurrency > 0
|
||||||
activityType: 'LIABILITY'
|
);
|
||||||
}).toNumber();
|
})
|
||||||
|
.map((symbol) => {
|
||||||
|
return new Big(holdings[symbol].valueInBaseCurrency).abs();
|
||||||
|
})
|
||||||
|
).toNumber();
|
||||||
|
|
||||||
|
const liabilities = getSum(
|
||||||
|
Object.keys(holdings)
|
||||||
|
.filter((symbol) => {
|
||||||
|
return (
|
||||||
|
isUUID(symbol) &&
|
||||||
|
holdings[symbol].dataSource === 'MANUAL' &&
|
||||||
|
holdings[symbol].valueInBaseCurrency < 0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((symbol) => {
|
||||||
|
return new Big(holdings[symbol].valueInBaseCurrency).abs();
|
||||||
|
})
|
||||||
|
).toNumber();
|
||||||
|
|
||||||
const totalBuy = this.getSumOfActivityType({
|
const totalBuy = this.getSumOfActivityType({
|
||||||
activities,
|
|
||||||
userCurrency,
|
userCurrency,
|
||||||
|
activities: nonExcludedActivities,
|
||||||
activityType: 'BUY'
|
activityType: 'BUY'
|
||||||
}).toNumber();
|
}).toNumber();
|
||||||
|
|
||||||
const totalSell = this.getSumOfActivityType({
|
const totalSell = this.getSumOfActivityType({
|
||||||
activities,
|
|
||||||
userCurrency,
|
userCurrency,
|
||||||
|
activities: nonExcludedActivities,
|
||||||
activityType: 'SELL'
|
activityType: 'SELL'
|
||||||
}).toNumber();
|
}).toNumber();
|
||||||
|
|
||||||
@ -1750,7 +1831,9 @@ export class PortfolioService {
|
|||||||
.minus(emergencyFund)
|
.minus(emergencyFund)
|
||||||
.plus(emergencyFundPositionsValueInBaseCurrency)
|
.plus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
const committedFunds = new Big(totalBuy).minus(totalSell);
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
||||||
|
|
||||||
const totalOfExcludedActivities = this.getSumOfActivityType({
|
const totalOfExcludedActivities = this.getSumOfActivityType({
|
||||||
userCurrency,
|
userCurrency,
|
||||||
activities: excludedActivities,
|
activities: excludedActivities,
|
||||||
@ -1801,21 +1884,36 @@ export class PortfolioService {
|
|||||||
})
|
})
|
||||||
?.toNumber();
|
?.toNumber();
|
||||||
|
|
||||||
|
const annualizedPerformancePercentWithCurrencyEffect =
|
||||||
|
new PortfolioCalculator({
|
||||||
|
currency: userCurrency,
|
||||||
|
currentRateService: this.currentRateService,
|
||||||
|
exchangeRateDataService: this.exchangeRateDataService,
|
||||||
|
orders: []
|
||||||
|
})
|
||||||
|
.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket,
|
||||||
|
netPerformancePercent: new Big(
|
||||||
|
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
|
||||||
|
)
|
||||||
|
})
|
||||||
|
?.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
...performanceInformation.performance,
|
||||||
annualizedPerformancePercent,
|
annualizedPerformancePercent,
|
||||||
|
annualizedPerformancePercentWithCurrencyEffect,
|
||||||
cash,
|
cash,
|
||||||
dividend,
|
|
||||||
excludedAccountsAndActivities,
|
excludedAccountsAndActivities,
|
||||||
fees,
|
fees,
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
interest,
|
interest,
|
||||||
items,
|
items,
|
||||||
liabilities,
|
liabilities,
|
||||||
netWorth,
|
|
||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
|
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
||||||
emergencyFund: {
|
emergencyFund: {
|
||||||
assets: emergencyFundPositionsValueInBaseCurrency,
|
assets: emergencyFundPositionsValueInBaseCurrency,
|
||||||
cash: emergencyFund
|
cash: emergencyFund
|
||||||
@ -1823,61 +1921,61 @@ export class PortfolioService {
|
|||||||
.toNumber(),
|
.toNumber(),
|
||||||
total: emergencyFund.toNumber()
|
total: emergencyFund.toNumber()
|
||||||
},
|
},
|
||||||
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
||||||
|
filteredValueInPercentage: netWorth
|
||||||
|
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||||
|
: undefined,
|
||||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
fireWealth: new Big(performanceInformation.performance.currentValue)
|
||||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
ordersCount: activities.filter(({ type }) => {
|
ordersCount: activities.filter(({ type }) => {
|
||||||
return type === 'BUY' || type === 'SELL';
|
return type === 'BUY' || type === 'SELL';
|
||||||
}).length
|
}).length,
|
||||||
|
totalValueInBaseCurrency: netWorth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSumOfActivityType({
|
private getSumOfActivityType({
|
||||||
activities,
|
activities,
|
||||||
activityType,
|
activityType,
|
||||||
date = new Date(0),
|
|
||||||
userCurrency
|
userCurrency
|
||||||
}: {
|
}: {
|
||||||
activities: OrderWithAccount[];
|
activities: OrderWithAccount[];
|
||||||
activityType: ActivityType;
|
activityType: ActivityType;
|
||||||
date?: Date;
|
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
}) {
|
}) {
|
||||||
return activities
|
return getSum(
|
||||||
.filter((activity) => {
|
activities
|
||||||
// Filter out all activities before given date (drafts) and
|
.filter(({ isDraft, type }) => {
|
||||||
// activity type
|
return isDraft === false && type === activityType;
|
||||||
return (
|
})
|
||||||
isBefore(date, new Date(activity.date)) &&
|
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
||||||
activity.type === activityType
|
return new Big(
|
||||||
);
|
this.exchangeRateDataService.toCurrency(
|
||||||
})
|
new Big(quantity).mul(unitPrice).toNumber(),
|
||||||
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
SymbolProfile.currency,
|
||||||
return this.exchangeRateDataService.toCurrency(
|
userCurrency
|
||||||
new Big(quantity).mul(unitPrice).toNumber(),
|
)
|
||||||
SymbolProfile.currency,
|
);
|
||||||
userCurrency
|
})
|
||||||
);
|
);
|
||||||
})
|
|
||||||
.reduce(
|
|
||||||
(previous, current) => new Big(previous).plus(current),
|
|
||||||
new Big(0)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTransactionPoints({
|
private async getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
|
types = getAllActivityTypes(),
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
|
types?: ActivityType[];
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
|
activities: Activity[];
|
||||||
transactionPoints: TransactionPoint[];
|
transactionPoints: TransactionPoint[];
|
||||||
orders: Activity[];
|
|
||||||
portfolioOrders: PortfolioOrder[];
|
portfolioOrders: PortfolioOrder[];
|
||||||
}> {
|
}> {
|
||||||
const userCurrency =
|
const userCurrency =
|
||||||
@ -1886,14 +1984,14 @@ export class PortfolioService {
|
|||||||
const { activities, count } = await this.orderService.getOrders({
|
const { activities, count } = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts,
|
includeDrafts,
|
||||||
|
types,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts
|
||||||
types: ['BUY', 'SELL']
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
return { transactionPoints: [], orders: [], portfolioOrders: [] };
|
return { activities: [], transactionPoints: [], portfolioOrders: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({
|
||||||
@ -1919,8 +2017,8 @@ export class PortfolioService {
|
|||||||
portfolioCalculator.computeTransactionPoints();
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
activities,
|
||||||
portfolioOrders,
|
portfolioOrders,
|
||||||
orders: activities,
|
|
||||||
transactionPoints: portfolioCalculator.getTransactionPoints()
|
transactionPoints: portfolioCalculator.getTransactionPoints()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1941,29 +2039,20 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getValueOfAccountsAndPlatforms({
|
private async getValueOfAccountsAndPlatforms({
|
||||||
|
activities,
|
||||||
filters = [],
|
filters = [],
|
||||||
orders,
|
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
|
activities: Activity[];
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
orders: OrderWithAccount[];
|
|
||||||
portfolioItemsNow: { [p: string]: TimelinePosition };
|
portfolioItemsNow: { [p: string]: TimelinePosition };
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { activities: ordersOfTypeItemOrLiability } =
|
|
||||||
await this.orderService.getOrders({
|
|
||||||
filters,
|
|
||||||
userCurrency,
|
|
||||||
userId,
|
|
||||||
withExcludedAccounts,
|
|
||||||
types: ['ITEM', 'LIABILITY']
|
|
||||||
});
|
|
||||||
|
|
||||||
const accounts: PortfolioDetails['accounts'] = {};
|
const accounts: PortfolioDetails['accounts'] = {};
|
||||||
const platforms: PortfolioDetails['platforms'] = {};
|
const platforms: PortfolioDetails['platforms'] = {};
|
||||||
|
|
||||||
@ -1981,7 +2070,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const accountIds = uniq(
|
const accountIds = uniq(
|
||||||
orders
|
activities
|
||||||
.filter(({ accountId }) => {
|
.filter(({ accountId }) => {
|
||||||
return accountId;
|
return accountId;
|
||||||
})
|
})
|
||||||
@ -2001,19 +2090,10 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const account of currentAccounts) {
|
for (const account of currentAccounts) {
|
||||||
let ordersByAccount = orders.filter(({ accountId }) => {
|
const ordersByAccount = activities.filter(({ accountId }) => {
|
||||||
return accountId === account.id;
|
return accountId === account.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ordersOfTypeItemOrLiabilityByAccount =
|
|
||||||
ordersOfTypeItemOrLiability.filter(({ accountId }) => {
|
|
||||||
return accountId === account.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
ordersByAccount = ordersByAccount.concat(
|
|
||||||
ordersOfTypeItemOrLiabilityByAccount
|
|
||||||
);
|
|
||||||
|
|
||||||
accounts[account.id] = {
|
accounts[account.id] = {
|
||||||
balance: account.balance,
|
balance: account.balance,
|
||||||
currency: account.currency,
|
currency: account.currency,
|
||||||
@ -2045,41 +2125,39 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const order of ordersByAccount) {
|
for (const {
|
||||||
|
Account,
|
||||||
|
quantity,
|
||||||
|
SymbolProfile,
|
||||||
|
type
|
||||||
|
} of ordersByAccount) {
|
||||||
let currentValueOfSymbolInBaseCurrency =
|
let currentValueOfSymbolInBaseCurrency =
|
||||||
order.quantity *
|
getFactor(type) *
|
||||||
(portfolioItemsNow[order.SymbolProfile.symbol]
|
quantity *
|
||||||
?.marketPriceInBaseCurrency ??
|
(portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ??
|
||||||
order.unitPrice ??
|
|
||||||
0);
|
0);
|
||||||
|
|
||||||
if (order.type === 'LIABILITY' || order.type === 'SELL') {
|
if (accounts[Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
|
||||||
currentValueOfSymbolInBaseCurrency *= -1;
|
accounts[Account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
|
||||||
}
|
|
||||||
|
|
||||||
if (accounts[order.Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
|
|
||||||
accounts[order.Account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
|
|
||||||
currentValueOfSymbolInBaseCurrency;
|
currentValueOfSymbolInBaseCurrency;
|
||||||
} else {
|
} else {
|
||||||
accounts[order.Account?.id || UNKNOWN_KEY] = {
|
accounts[Account?.id || UNKNOWN_KEY] = {
|
||||||
balance: 0,
|
balance: 0,
|
||||||
currency: order.Account?.currency,
|
currency: Account?.currency,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
|
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
platforms[order.Account?.Platform?.id || UNKNOWN_KEY]
|
platforms[Account?.Platform?.id || UNKNOWN_KEY]?.valueInBaseCurrency
|
||||||
?.valueInBaseCurrency
|
|
||||||
) {
|
) {
|
||||||
platforms[
|
platforms[Account?.Platform?.id || UNKNOWN_KEY].valueInBaseCurrency +=
|
||||||
order.Account?.Platform?.id || UNKNOWN_KEY
|
currentValueOfSymbolInBaseCurrency;
|
||||||
].valueInBaseCurrency += currentValueOfSymbolInBaseCurrency;
|
|
||||||
} else {
|
} else {
|
||||||
platforms[order.Account?.Platform?.id || UNKNOWN_KEY] = {
|
platforms[Account?.Platform?.id || UNKNOWN_KEY] = {
|
||||||
balance: 0,
|
balance: 0,
|
||||||
currency: order.Account?.currency,
|
currency: Account?.currency,
|
||||||
name: account.Platform?.name,
|
name: account.Platform?.name,
|
||||||
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
|
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { Rule } from '@ghostfolio/api/models/rule';
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
|
||||||
import { CacheModule } from '@nestjs/cache-manager';
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import * as redisStore from 'cache-manager-redis-store';
|
import * as redisStore from 'cache-manager-redis-store';
|
||||||
@ -14,6 +15,7 @@ import { RedisCacheService } from './redis-cache.service';
|
|||||||
inject: [ConfigurationService],
|
inject: [ConfigurationService],
|
||||||
useFactory: async (configurationService: ConfigurationService) => {
|
useFactory: async (configurationService: ConfigurationService) => {
|
||||||
return <RedisClientOptions>{
|
return <RedisClientOptions>{
|
||||||
|
db: configurationService.get('REDIS_DB'),
|
||||||
host: configurationService.get('REDIS_HOST'),
|
host: configurationService.get('REDIS_HOST'),
|
||||||
max: configurationService.get('MAX_ITEM_IN_CACHE'),
|
max: configurationService.get('MAX_ITEM_IN_CACHE'),
|
||||||
password: configurationService.get('REDIS_PASSWORD'),
|
password: configurationService.get('REDIS_PASSWORD'),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getYesterday,
|
getYesterday,
|
||||||
interpolate
|
interpolate
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
|
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
@Controller('sitemap.xml')
|
@Controller('sitemap.xml')
|
||||||
export class SitemapController {
|
export class SitemapController {
|
||||||
|
@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { SitemapController } from './sitemap.controller';
|
import { SitemapController } from './sitemap.controller';
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { Coupon } from '@ghostfolio/common/interfaces';
|
import { Coupon } from '@ghostfolio/common/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { SubscriptionController } from './subscription.controller';
|
import { SubscriptionController } from './subscription.controller';
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
import { SubscriptionOffer, UserWithSettings } from '@ghostfolio/common/types';
|
||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Subscription } from '@prisma/client';
|
import { Subscription } from '@prisma/client';
|
||||||
import { addMilliseconds, isBefore } from 'date-fns';
|
import { addMilliseconds, isBefore } from 'date-fns';
|
||||||
@ -107,17 +109,27 @@ export class SubscriptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSubscription(
|
public getSubscription({
|
||||||
aSubscriptions: Subscription[]
|
createdAt,
|
||||||
): UserWithSettings['subscription'] {
|
subscriptions
|
||||||
if (aSubscriptions.length > 0) {
|
}: {
|
||||||
const { expiresAt, price } = aSubscriptions.reduce((a, b) => {
|
createdAt: UserWithSettings['createdAt'];
|
||||||
|
subscriptions: Subscription[];
|
||||||
|
}): UserWithSettings['subscription'] {
|
||||||
|
if (subscriptions.length > 0) {
|
||||||
|
const { expiresAt, price } = subscriptions.reduce((a, b) => {
|
||||||
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let offer: SubscriptionOffer = price ? 'renewal' : 'default';
|
||||||
|
|
||||||
|
if (isBefore(createdAt, parseDate('2023-01-01'))) {
|
||||||
|
offer = 'renewal-early-bird';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
expiresAt,
|
expiresAt,
|
||||||
offer: price ? 'renewal' : 'default',
|
offer,
|
||||||
type: isBefore(new Date(), expiresAt)
|
type: isBefore(new Date(), expiresAt)
|
||||||
? SubscriptionType.Premium
|
? SubscriptionType.Premium
|
||||||
: SubscriptionType.Basic
|
: SubscriptionType.Basic
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface LookupItem {
|
export interface LookupItem {
|
||||||
|
@ -3,6 +3,7 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
|
|||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -38,9 +39,11 @@ export class SymbolController {
|
|||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async lookupSymbol(
|
public async lookupSymbol(
|
||||||
@Query('includeIndices') includeIndices: boolean = false,
|
@Query('includeIndices') includeIndicesParam = 'false',
|
||||||
@Query('query') query = ''
|
@Query('query') query = ''
|
||||||
): Promise<{ items: LookupItem[] }> {
|
): Promise<{ items: LookupItem[] }> {
|
||||||
|
const includeIndices = includeIndicesParam === 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return this.symbolService.lookup({
|
return this.symbolService.lookup({
|
||||||
includeIndices,
|
includeIndices,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user