Compare commits

...

111 Commits

Author SHA1 Message Date
9002c20165 Release 1.264.0 (#1916) 2023-05-01 17:46:28 +02:00
15c96a9757 Feature/add allocations by platform chart (#1915)
* Add allocations by platform

* Update changelog
2023-05-01 17:44:35 +02:00
1ca3792a4b Feature/clean up initial values from x ray (#1914)
* Clean up initial (original) values from X-Ray

* Refactor current to valueInBaseCurrency

* Update changelog
2023-05-01 17:16:02 +02:00
90fe467114 Feature/deprecate base currency (#1913)
* Deprecate BASE_CURRENCY

* Update changelog
2023-05-01 15:45:59 +02:00
e61b3b34a7 Eliminate getSymbolProfilesBySymbols() (#1912) 2023-05-01 15:45:33 +02:00
1326418ffc Release 1.263.0 (#1911) 2023-04-30 19:21:35 +02:00
a5f0f48ddb Fix accounts page (#1908)
* Add guards

* Update changelog
2023-04-30 19:20:17 +02:00
e500ccb61b Feature/introduce env variable data source exchange rates and data source import (#1910)
* Introduce env variables DATA_SOURCE_EXCHANGE_RATES and DATA_SOURCE_IMPORT

* Update changelog
2023-04-30 18:26:34 +02:00
4090b03406 Release 1.262.0 (#1906) 2023-04-29 10:41:28 +02:00
431d1d5fec Feature/extract locales 20230429 (#1905)
* Update locales

* Update changelog
2023-04-29 10:39:17 +02:00
d74d79198b Feature/add labels to tabs (#1847)
* Add labels

* Update changelog
2023-04-29 10:15:55 +02:00
623a284ba4 Feature/add and update historical data in bulk (#1904)
* Upsert historical data in bulk

* Update changelog
2023-04-29 10:12:50 +02:00
f79c36edbb Fix import (#1902) 2023-04-29 10:00:04 +02:00
f4c748f67a Feature/extend support for impersonation mode (#1898)
* Support impersonation of all users for local development

* Update changelog
2023-04-28 21:02:24 +02:00
672d8dfab2 Fix/holdings always include cash position (#1897)
* Improved holdings table showing cash position also when the filter contains accounts

* Update changelog
2023-04-28 12:08:45 +02:00
0464adccce Fix script (#1885) 2023-04-26 13:41:05 +02:00
c3df6c3194 Release 1.261.0 (#1896) 2023-04-25 20:24:39 +02:00
29d53c7df4 Feature/add distance to now to the subscription expiration date (#1895)
* Add distance to now

* Update changelog
2023-04-25 20:23:07 +02:00
7b77dc044a Feature/add state to market data database schema (#1893)
* Add state (CLOSE / INTRADAY) to MarketData

* Update changelog
2023-04-25 20:09:12 +02:00
67e758365f feature: allow to delete all activities of a user (#1880)
* Allow to delete all activities of a user

* Update changelog
2023-04-23 19:49:32 +02:00
475231ffd8 Release 1.260.0 (#1892) 2023-04-23 12:04:06 +02:00
513a564e2c Restructure services (#1891) 2023-04-23 12:02:01 +02:00
cddea0401f Feature/add data source as unique constraint to market data schema (#1889)
* Add dataSource as unique constraint to MarketData schema

* Update changelog
2023-04-23 11:13:08 +02:00
3dafbf7fef Add schema synchronization (#1890) 2023-04-23 11:02:12 +02:00
fcd75414be Update date (#1860) 2023-04-23 11:01:53 +02:00
c1b5bfff8c Bugfix/remove sort header in comment column of market data table (#1888)
* Remove sort header

* Update changelog
2023-04-23 10:49:11 +02:00
3c322cca0d Release 1.259.0 (#1887) 2023-04-22 16:05:44 +02:00
e965d12e31 Feature/add health check endpoints (#1886)
* Add health check endpoints

* Update changelog
2023-04-22 16:03:45 +02:00
3daf55a0dd Bugfix/remove sort header in comment column of activities table (#1883)
* Remove sort header

* Update changelog
2023-04-22 14:44:45 +02:00
aafedd5f75 Feature/increase robustness if live data is missing (#1884)
* Continuously persist today's market data

* Add fallback to historical market data if data provider does not provide live data

* Update changelog
2023-04-22 14:43:57 +02:00
32956ae04c Fix: performance column header alignment (#1881)
* Fix: performance column header alignment

* Update changelog
2023-04-21 18:05:51 +02:00
bfd0241b2d update target in proxy to work with api in locahost (#1875)
Co-authored-by: francisco <francisco@innonova.ch>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2023-04-20 18:51:35 +02:00
5eff8402db Release/1.258.0 (#1878)
* Release 1.258.0
  * Introduce data source mapping

* Update changelog
2023-04-20 09:07:22 +02:00
ffa020ee2a Release 1.257.0 (#1872) 2023-04-18 20:36:22 +02:00
80a3668aa9 Bugfix/fix world map chart component (#1871)
* Clone countries before manipulation

* Update changelog
2023-04-18 20:32:18 +02:00
7378900050 Bugfix/fix currency inconsistency with GBX and GBp in eod historical data service (#1869)
* Fix currency inconsistency (GBX vs. GBp)

* Update changelog
2023-04-18 20:31:33 +02:00
9be457943c Feature/introduce allocations by etf provider (#1870)
* Introduce allocations by etf provider

* Update changelog
2023-04-18 20:13:03 +02:00
93454c6c15 Release 1.256.0 (#1868) 2023-04-17 14:11:59 +02:00
fccbd76993 Bugfix/fix style of apply current market price button (#1866)
* Fix styles

* Update changelog
2023-04-17 14:09:55 +02:00
922876a893 Feature/add yahoo finance data enhancer (#1865)
* Add Yahoo Finance data enhancer

* Update changelog
2023-04-17 14:09:27 +02:00
654446f068 Feature/improve handling of jobs (#1864)
* Improve handling of jobs
  * Remove jobs on complete
  * Refactor jobs removal

* Update changelog
2023-04-16 19:56:19 +02:00
947460abdd Bugfix/fix ids of gather asset profile process jobs (#1863)
* Fix job ids

* Update changelog
2023-04-16 09:49:27 +02:00
c5635b0050 Release 1.255.0 (#1862) 2023-04-15 16:00:47 +02:00
8a3a6308a3 Feature/make system message expandable (#1861)
* Make system message expandable

* Update changelog
2023-04-15 15:59:04 +02:00
290a07fe79 Feature/upgrade prisma to version 4.12.0 (#1845)
* Update prisma to version 4.12.0

* Update changelog
2023-04-15 15:32:02 +02:00
4c907d56f0 Improve message (#1859) 2023-04-15 15:09:15 +02:00
56b437ca74 Bugfix/improve info message style (#1858)
* Improve info message style

* Update changelog
2023-04-15 15:08:56 +02:00
e23ff33e6f Feature/skip job creation for manual data source without scraper configuration (#1857)
* Skip job creation for MANUAL data source without scraper configuration

* Update changelog
2023-04-15 11:54:47 +02:00
f2d206262e Release 1.254.0 (#1856) 2023-04-14 19:58:49 +02:00
1ed5690b33 Feature/improve queue jobs implementation (#1855)
* Improve queue jobs implementation

* Update changelog
2023-04-14 19:57:23 +02:00
4451514ec5 Release 1.253.0 (#1854) 2023-04-14 07:01:34 +02:00
8f73f85276 Bugfix/fix background color of dialogs in dark mode (#1853)
* Fix background color

* Update changelog
2023-04-13 08:47:19 +02:00
9a5d7b664b Release 1.252.2 (#1852) 2023-04-11 18:06:34 +02:00
7d2d1d971a Feature/deprecate get auth endpoint (#1851)
* Deprecate GET auth endpoint

* Update documentation

* Update changelog
2023-04-11 18:04:18 +02:00
d111493eed Release 1.252.1 (#1849) 2023-04-10 20:52:34 +02:00
e975f92a96 Release 1.252.0 (#1848) 2023-04-10 18:03:02 +02:00
739cb4242d Feature/decrease density of theme (#1846)
* Decrease density

* Update changelog
2023-04-10 17:59:29 +02:00
a37eebc9f1 Feature/upgrade nestjs from version 9.1.4 to 9.4.0 (#1843)
* Upgrade nestjs from version 9.1.4 to 9.4.0

* Update changelog
2023-04-10 13:50:27 +02:00
e92730879e Feature/migrate dialog components to angular material 15 (#1844)
* Migrate MatDialog

* Update changelog
2023-04-10 13:49:53 +02:00
464973f9b0 Feature/migrate form components to angular material 15 (#1842)
* Upgrade @angular/cdk

* Upgrade form components to Angular Material 15

* Update changelog
2023-04-10 10:59:44 +02:00
f6228c099f Migrate MatRadio (#1841) 2023-04-10 09:23:16 +02:00
a57fdfb2bb Feature/migrate slide toggle components to angular material 15 (#1840)
* Upgrade @angular/material

* Change MatSlideToggle to MatCheckbox

* Update changelog
2023-04-09 09:33:36 +02:00
24716f0561 Migrate checkbox and chips components to Angular Material 15 (#1839) 2023-04-09 08:55:09 +02:00
3453100afd Feature/migrate various components to angular material 15 part 2 (#1838)
* Migrate tooltips to Angular Material 15

* Migrate tabs to Angular Material 15
2023-04-09 08:54:32 +02:00
84de2c0c68 Migrate card components to Angular Material 15 (#1837) 2023-04-08 17:08:30 +02:00
1b7b082003 Feature/migrate various components to angular material 15 (#1836)
* Migrate components to Angular Material 15

* Update changelog
2023-04-08 15:33:27 +02:00
1928c2c2cc Release 1.251.0 (#1834) 2023-04-07 19:33:26 +02:00
52e7a7886d Feature/migrate libs components to angular material 15 (#1833)
* Migrate to Angular Material 15

* Update changelog
2023-04-07 17:10:03 +02:00
36298b217e Feature/improve tick abbreviation function (#1828)
* Keep the value if value smaller than 1000

* Update changelog
2023-04-07 17:09:34 +02:00
9bce57894e Feature/increase historical market data gathering to 10 years (#1830)
* Increase historical market data gathering of currency pairs to 10+ years

* Update changelog
2023-04-07 16:35:37 +02:00
9d6bb325cd Improve initialization (#1832) 2023-04-07 16:33:55 +02:00
a5f833c612 Feature/upgrade angular and nx 20230405 (#1826)
* Upgrade angular and Nx

* Update changelog
2023-04-06 19:18:23 +02:00
732b14c6ab Feature/improve activities import for csv files of ibkr (#1824)
* Improve import for csv files by Interactive Brokers

* Update changelog
2023-04-05 20:09:00 +02:00
b74a042da8 Feature/change auth endpoint from get to post (#1823)
* Change auth endpoint from GET to POST
  * Login with security token
  * Login with Internet Identity

* Update changelog
2023-04-05 18:10:29 +02:00
d55c052f57 Feature/improve content of pricing and faq pages (#1822)
* Improve content

* Update changelog
2023-04-03 17:39:32 +02:00
864f585efa Release/1.250.0 (#1821) 2023-04-02 09:47:13 +02:00
6d56146054 Feature/add support for multiple subscription offers (#1818)
* Setup for multiple subscription offers

* Update changelog
2023-04-02 09:44:13 +02:00
2c9f29a3c6 Feature/improve handling of platforms in accounts import (#1820)
* Improve handling of platforms

* Fix issue with pagination

* Update changelog
2023-04-01 10:51:55 +02:00
9bef2e960c Feature/ignore first item in portfolio evolution chart (#1816)
* Ignore first item in portfolio evolution chart

* Update changelog
2023-04-01 10:29:39 +02:00
17b8c41673 Add test file (#1810) 2023-03-30 08:24:44 +02:00
f0afbd7346 Release 1.249.0 (#1815) 2023-03-27 20:38:46 +02:00
5dc7429f6a Feature/add testimonials (#1814)
* Add testimonials

* Update changelog
2023-03-27 20:36:49 +02:00
7b39b32293 Feature/improve allocations page (#1813)
* Improve loading state

* Update changelog
2023-03-26 17:18:48 +02:00
e5b5a9e7e9 Feature/improve language localization for german (#1809)
* Improve language localization

* Update changelog
2023-03-26 17:17:48 +02:00
1f3511368a Feature/always show label in value component (#1812)
* Always show label while loading

* Update changelog
2023-03-26 16:59:52 +02:00
b37df2c84f Bugfix/fix algebraic sign in value component (#1811)
* Fix algebraic sign by resetting member variables

* Update changelog
2023-03-26 16:16:24 +02:00
f92ba54060 Release/1.248.0 (#1808) 2023-03-25 17:45:51 +01:00
a3bbd4030e Improve blog post and add images (#1807) 2023-03-25 17:42:41 +01:00
4b30da2d92 Feature/conditionally hide platform selector (#1805)
* Hide platform selector conditionally

* Update changelog
2023-03-25 16:46:46 +01:00
93d082afbb Feature/add blog post 1000 stars on GitHub (#1804)
* Add blog post: 1000 Stars on GitHub

* Add breadcrumb navigation

* Update changelog
2023-03-25 14:28:06 +01:00
0c85380dbf Feature/refactor portfolio calculator (#1803)
* Refactor chart calculation in portfolio calculator

* Update changelog
2023-03-25 12:20:42 +01:00
fb576376dc Feature/upgrade ng extract i18n merge (#1802)
* Upgrade ng-extract-i18n-merge

* Extract locales

* Update changelog
2023-03-24 17:34:27 +01:00
ff111d4c6c Release 1.247.0 (#1801) 2023-03-23 19:26:01 +01:00
bc6e9a8b68 Bugfix/fix total amount calculation in portfolio evolution chart (#1799)
* Fix total amount calculation

* Update changelog
2023-03-23 19:23:31 +01:00
bd1963ec26 Feature/add asset and asset sub class to search endpoint (#1795)
* Add asset and asset sub class to search endpoint

* Update changelog
2023-03-23 19:11:38 +01:00
a0bec9e97f Feature/remove mail address part 2 (#1800)
* Remove mail address and update Slack url

* Update changelog
2023-03-23 14:14:31 +01:00
c45df20d88 Sort imports (#1797) 2023-03-21 19:34:40 +01:00
fa1d669633 Feature/upgrade prisma to version 4.11.0 (#1798)
* Upgrade prisma to version 4.11.0

* Update changelog
2023-03-20 20:08:08 +01:00
1009b462e9 Feature/add subscription expiration dates to the admin control panel (#1796)
* Add expiration date

* Update changelog
2023-03-19 12:03:47 +01:00
b404858904 Release 1.246.0 (#1794) 2023-03-18 10:36:40 +01:00
7ec033577f Feature/extend trackinsight data enhancer by isin (#1793)
* Extend data enhancer by isin

* Update changelog
2023-03-18 10:34:50 +01:00
c8ca82b803 Feature/extend data source eod historical data by asset class and isin (#1791)
* Extend EodHistoricalDataService

* asset and asset sub class
* isin

* Update changelog
2023-03-18 10:09:11 +01:00
5db2faa17d Bugfix/fix border color in fire calculator (#1792)
* Fix border color

* Update changelog
2023-03-17 19:37:36 +01:00
1605fb8d48 Feature/improve language localization for data gathering (#1790)
* Improve locales

* Update changelog
2023-03-16 20:54:34 +01:00
b6a7804a26 Refactoring (#1784) 2023-03-14 10:46:11 +01:00
de31381fd9 Release 1.245.0 (#1787) 2023-03-12 14:33:00 +01:00
0d92b8d8bb Reduce search requests (#1786) 2023-03-12 14:29:22 +01:00
7c6ff776d9 Feature/add search functionality for eod historical data (#1783)
* Add search functionality for EOD_HISTORICAL_DATA

* Update changelog
2023-03-12 13:13:34 +01:00
e37a34ed6c Feature/improve exchange rate service for specific date (#1785)
* Calculate exchange rate indirectly via base currency

* Update changelog
2023-03-12 12:19:13 +01:00
c4d9c00f92 Feature/upgrade ngx device detector to version 5.0.1 (#1782)
* Upgrade ngx-device-detector to version 5.0.1

* Update changelog
2023-03-12 10:14:35 +01:00
3af8be89e3 Feature/improve usability of fire calculator (#1779)
* Improve usability

* Add debounce
* Persist annualInterestRate
* Partially disable date picker

* Update changelog
2023-03-12 09:55:55 +01:00
350 changed files with 8894 additions and 5848 deletions

View File

@ -5,6 +5,270 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.264.0 - 2023-05-01
### Added
- Introduced the allocations by platform chart on the allocations page
### Changed
- Deprecated the use of the environment variable `BASE_CURRENCY`
- Cleaned up initial values from the _X-ray_ section
## 1.263.0 - 2023-04-30
### Changed
- Split the environment variable `DATA_SOURCE_PRIMARY` in `DATA_SOURCE_EXCHANGE_RATES` and `DATA_SOURCE_IMPORT`
### Fixed
- Fixed the exception on the accounts page
## 1.262.0 - 2023-04-29
### Added
- Added the labels to the tabs to increase the usability
- Extended the support of the impersonation mode for local development
### Changed
- Improved the queue jobs implementation by adding / updating historical market data in bulk
- Improved the language localization for German (`de`)
### Fixed
- Improved the holdings table by showing the cash position also when the filter contains the accounts, so that we can see the total allocation for that account
## 1.261.0 - 2023-04-25
### Added
- Introduced a new button to delete all activities from the portfolio activities page
- Added `state` to the `MarketData` database schema to distinguish `CLOSE` and `INTRADAY` in the data gathering
- Added the distance to now to the subscription expiration date in the users table of the admin control panel
## 1.260.0 - 2023-04-23
### Added
- Added `dataSource` as a unique constraint to the `MarketData` database schema
### Fixed
- Removed the unnecessary sort header of the comment column in the historical market data table of the admin control panel
## 1.259.0 - 2023-04-22
### Added
- Added a fallback to historical market data if a data provider does not provide live data
- Added a general health check endpoint
- Added health check endpoints for data providers
### Changed
- Persisted today's market data continuously
### Fixed
- Fixed the alignment of the performance column header in the holdings table
- Removed the unnecessary sort header of the comment column in the activities table
- Fixed the targets in `proxy.conf.json` from `http://localhost:3333` to `http://0.0.0.0:3333` for local development
## 1.258.0 - 2023-04-20
### Added
- Introduced a data source mapping
## 1.257.0 - 2023-04-18
### Added
- Introduced the allocations by ETF provider chart on the allocations page
### Fixed
- Fixed an issue in the global heat map component caused by manipulating an input property
- Fixed an issue with the currency inconsistency in the _EOD Historical Data_ service (convert from `GBX` to `GBp`)
## 1.256.0 - 2023-04-17
### Added
- Added the _Yahoo Finance_ data enhancer for countries, sectors and urls
### Changed
- Enabled the configuration to immediately remove queue jobs on complete
- Refactored the implementation of removing queue jobs
### Fixed
- Fixed the unique job ids of the gather asset profile process
- Fixed the style of the button to fetch the current market price
## 1.255.0 - 2023-04-15
### Added
- Made the system message expandable
### Changed
- Skipped creating queue jobs for asset profiles with `MANUAL` data source not having a scraper configuration
- Reduced the execution interval of the data gathering to every hour
- Upgraded `prisma` from version `4.11.0` to `4.12.0`
### Fixed
- Improved the style of the system message
## 1.254.0 - 2023-04-14
### Changed
- Improved the queue jobs implementation by adding in bulk
- Improved the queue jobs implementation by introducing unique job ids
- Reverted the execution interval of the data gathering from every 12 hours to every 4 hours
## 1.253.0 - 2023-04-14
### Changed
- Reduced the execution interval of the data gathering to every 12 hours
### Fixed
- Fixed the background color of dialogs in dark mode
## 1.252.2 - 2023-04-11
### Changed
- Deprecated the `auth` endpoint of the login with _Security Token_ (`GET`)
## 1.252.1 - 2023-04-10
### Changed
- Changed the slide toggles to checkboxes on the account page
- Changed the slide toggles to checkboxes in the admin control panel
- Decreased the density of the theme
- Migrated the style of various components to `@angular/material` `15` (mdc)
- Upgraded `@angular/cdk` and `@angular/material` from version `15.2.5` to `15.2.6`
- Upgraded `bull` from version `4.10.2` to `4.10.4`
## 1.251.0 - 2023-04-07
### Changed
- Improved the activities import for `csv` files exported by _Interactive Brokers_
- Improved the rendering of the chart ticks (`0.5K``500`)
- Increased the historical market data gathering of currency pairs to 10+ years
- Improved the content of the Frequently Asked Questions (FAQ) page
- Improved the content of the pricing page
- Changed the `auth` endpoint of the login with _Security Token_ from `GET` to `POST`
- Changed the `auth` endpoint of the _Internet Identity_ login provider from `GET` to `POST`
- Migrated the style of the `libs` components to `@angular/material` `15` (mdc)
- `ActivitiesFilterComponent`
- `ActivitiesTableComponent`
- `BenchmarkComponent`
- `HoldingsTableComponent`
- Upgraded `angular` from version `15.1.5` to `15.2.5`
- Upgraded `Nx` from version `15.7.2` to `15.9.2`
## 1.250.0 - 2023-04-02
### Added
- Added support for multiple subscription offers
### Changed
- Improved the portfolio evolution chart (ignore first item)
- Improved the accounts import by handling the platform
### Fixed
- Fixed an issue with more than 50 activities in the activities import (`dryRun`)
## 1.249.0 - 2023-03-27
### Added
- Extended the testimonial section on the landing page
### Changed
- Improved the loading state of the value component on the allocations page
- Improved the value component by always showing the label (also while loading)
- Improved the language localization for German (`de`)
### Fixed
- Fixed an issue with the algebraic sign in the value component
## 1.248.0 - 2023-03-25
### Added
- Added a blog post: _Ghostfolio reaches 1000 Stars on GitHub_
- Added a breadcrumb navigation to the blog post pages
### Changed
- Refactored the calculation of the chart
- Hid the platform selector if no platforms are available in the create or update account dialog
- Upgraded `ng-extract-i18n-merge` from version `2.5.0` to `2.6.0`
## 1.247.0 - 2023-03-23
### Added
- Added the asset and asset sub class to the search functionality
- Added the subscription expiration date to the users table of the admin control panel
### Changed
- Updated the URL of the Ghostfolio Slack channel
- Upgraded `prisma` from version `4.10.1` to `4.11.0`
### Fixed
- Fixed the total amount calculation in the portfolio evolution chart
## 1.246.0 - 2023-03-18
### Added
- Added support for asset and asset sub class to the `EOD_HISTORICAL_DATA` data source type
- Added `isin` to the asset profile model
### Changed
- Extended the _Trackinsight_ data enhancer for asset profile data by `isin`
- Improved the language localization for _Gather Data_
### Fixed
- Fixed the border color in the _FIRE_ calculator (dark mode)
## 1.245.0 - 2023-03-12
### Added
- Added the search functionality for the `EOD_HISTORICAL_DATA` data source type
### Changed
- Improved the usability of the _FIRE_ calculator
- Improved the exchange rate service for a specific date used in activities with a manual currency
- Upgraded `ngx-device-detector` from version `3.0.0` to `5.0.1`
## 1.244.0 - 2023-03-09 ## 1.244.0 - 2023-03-09
### Added ### Added
@ -166,7 +430,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support to export accounts - Added support to export accounts
- Added suport to import accounts - Added support to import accounts
### Changed ### Changed

View File

@ -18,7 +18,13 @@
### Prisma ### Prisma
#### Create schema migration (local) #### Synchronize schema with database for prototyping
Run `yarn database:push`
https://www.prisma.io/docs/concepts/components/prisma-migrate/db-push
#### Create schema migration
Run `yarn prisma migrate dev --name added_job_title` Run `yarn prisma migrate dev --name added_job_title`

View File

@ -200,7 +200,9 @@ Set the header for each request as follows:
"Authorization": "Bearer eyJh..." "Authorization": "Bearer eyJh..."
``` ```
You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`. You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ accessToken: <INSERT_SECURITY_TOKEN_OF_ACCOUNT> }`)
Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
### Import Activities ### Import Activities
@ -272,6 +274,6 @@ If you like to support this project, get [**Ghostfolio Premium**](https://ghostf
## License ## License
© 2023 [Ghostfolio](https://ghostfol.io) © 2021 - 2023 [Ghostfolio](https://ghostfol.io)
Licensed under the [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.html). Licensed under the [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.html).

View File

@ -2,13 +2,14 @@
export default { export default {
displayName: 'api', displayName: 'api',
globals: { globals: {},
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json'
}
},
transform: { transform: {
'^.+\\.[tj]s$': 'ts-jest' '^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json'
}
]
}, },
moduleFileExtensions: ['ts', 'js', 'html'], moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/api', coverageDirectory: '../../coverage/apps/api',

View File

@ -1,4 +1,4 @@
import { PrismaModule } from '@ghostfolio/api/services/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';

View File

@ -1,4 +1,4 @@
import { PrismaService } from '@ghostfolio/api/services/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';

View File

@ -1,6 +1,6 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { Accounts } from '@ghostfolio/common/interfaces'; import { Accounts } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -87,10 +87,7 @@ export class AccountController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId
): Promise<Accounts> { ): Promise<Accounts> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(impersonationId);
impersonationId,
this.request.user.id
);
return this.portfolioService.getAccountsWithAggregations({ return this.portfolioService.getAccountsWithAggregations({
userId: impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
@ -106,10 +103,7 @@ export class AccountController {
@Param('id') id: string @Param('id') id: string
): Promise<AccountWithValue> { ): Promise<AccountWithValue> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(impersonationId);
impersonationId,
this.request.user.id
);
const accountsWithAggregations = const accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations({ await this.portfolioService.getAccountsWithAggregations({

View File

@ -1,11 +1,11 @@
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/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';

View File

@ -1,5 +1,5 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/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';

View File

@ -1,5 +1,5 @@
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { import {
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS,
@ -100,16 +100,21 @@ export class AdminController {
const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
for (const { dataSource, symbol } of uniqueAssets) { await this.dataGatheringService.addJobsToQueue(
await this.dataGatheringService.addJobToQueue( uniqueAssets.map(({ dataSource, symbol }) => {
GATHER_ASSET_PROFILE_PROCESS, return {
{ data: {
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS name: GATHER_ASSET_PROFILE_PROCESS,
); opts: {
} ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
}
};
})
);
this.dataGatheringService.gatherMax(); this.dataGatheringService.gatherMax();
} }
@ -131,16 +136,21 @@ export class AdminController {
const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
for (const { dataSource, symbol } of uniqueAssets) { await this.dataGatheringService.addJobsToQueue(
await this.dataGatheringService.addJobToQueue( uniqueAssets.map(({ dataSource, symbol }) => {
GATHER_ASSET_PROFILE_PROCESS, return {
{ data: {
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS name: GATHER_ASSET_PROFILE_PROCESS,
); opts: {
} ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
}
};
})
);
} }
@Post('gather/profile-data/:dataSource/:symbol') @Post('gather/profile-data/:dataSource/:symbol')
@ -161,14 +171,17 @@ export class AdminController {
); );
} }
await this.dataGatheringService.addJobToQueue( await this.dataGatheringService.addJobToQueue({
GATHER_ASSET_PROFILE_PROCESS, data: {
{
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS name: GATHER_ASSET_PROFILE_PROCESS,
); opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
}
});
} }
@Post('gather/:dataSource/:symbol') @Post('gather/:dataSource/:symbol')
@ -304,9 +317,10 @@ export class AdminController {
const date = new Date(dateString); const date = new Date(dateString);
return this.marketDataService.updateMarketData({ return this.marketDataService.updateMarketData({
data: { ...data, dataSource }, data: { marketPrice: data.marketPrice, state: 'CLOSE' },
where: { where: {
date_symbol: { dataSource_date_symbol: {
dataSource,
date, date,
symbol symbol
} }

View File

@ -1,12 +1,12 @@
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/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.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/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.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';

View File

@ -1,10 +1,10 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
import { import {
AdminData, AdminData,
@ -100,6 +100,7 @@ export class AdminService {
dataSource, dataSource,
marketDataItemCount, marketDataItemCount,
symbol, symbol,
assetClass: 'CASH',
countriesCount: 0, countriesCount: 0,
sectorsCount: 0 sectorsCount: 0
}; };
@ -186,8 +187,11 @@ export class AdminService {
]); ]);
return { return {
assetProfile, marketData,
marketData assetProfile: assetProfile ?? {
symbol,
currency: '-'
}
}; };
} }

View File

@ -1,4 +1,4 @@
import { DataGatheringModule } from '@ghostfolio/api/services/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';

View File

@ -4,7 +4,7 @@ import {
} 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, Logger } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { JobStatus, Queue } from 'bull'; import { JobStatus, Queue } from 'bull';
@Injectable() @Injectable()
@ -23,14 +23,11 @@ export class QueueService {
}: { }: {
status?: JobStatus[]; status?: JobStatus[];
}) { }) {
const jobs = await this.dataGatheringQueue.getJobs(status); for (const statusItem of status) {
await this.dataGatheringQueue.clean(
for (const job of jobs) { 300,
try { statusItem === 'waiting' ? 'wait' : statusItem
await job.remove(); );
} catch (error) {
Logger.warn(error, 'QueueService');
}
} }
} }
@ -44,18 +41,23 @@ export class QueueService {
const jobs = await this.dataGatheringQueue.getJobs(status); const jobs = await this.dataGatheringQueue.getJobs(status);
const jobsWithState = await Promise.all( const jobsWithState = await Promise.all(
jobs.slice(0, limit).map(async (job) => { jobs
return { .filter((job) => {
attemptsMade: job.attemptsMade + 1, return job;
data: job.data, })
finishedOn: job.finishedOn, .slice(0, limit)
id: job.id, .map(async (job) => {
name: job.name, return {
stacktrace: job.stacktrace, attemptsMade: job.attemptsMade + 1,
state: await job.getState(), data: job.data,
timestamp: job.timestamp finishedOn: job.finishedOn,
}; id: job.id,
}) name: job.name,
stacktrace: job.stacktrace,
state: await job.getState(),
timestamp: job.timestamp
};
})
); );
return { return {

View File

@ -1,4 +1,4 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/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()

View File

@ -1,18 +1,18 @@
import { join } from 'path'; import { join } from 'path';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { CronService } from '@ghostfolio/api/services/cron.service';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.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 { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { MiddlewareConsumer, Module, RequestMethod } 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 { ConfigurationModule } from '../services/configuration.module';
import { CronService } from '../services/cron.service';
import { DataGatheringModule } from '../services/data-gathering.module';
import { DataProviderModule } from '../services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '../services/exchange-rate-data.module';
import { PrismaModule } from '../services/prisma.module';
import { TwitterBotModule } from '../services/twitter-bot/twitter-bot.module';
import { AccessModule } from './access/access.module'; import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module'; import { AccountModule } from './account/account.module';
import { AdminModule } from './admin/admin.module'; import { AdminModule } from './admin/admin.module';
@ -24,6 +24,7 @@ import { CacheModule } from './cache/cache.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
import { FrontendMiddleware } from './frontend.middleware'; import { FrontendMiddleware } from './frontend.middleware';
import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module'; import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module'; import { InfoModule } from './info/info.module';
import { LogoModule } from './logo/logo.module'; import { LogoModule } from './logo/logo.module';
@ -57,6 +58,7 @@ import { UserModule } from './user/user.module';
ExchangeRateModule, ExchangeRateModule,
ExchangeRateDataModule, ExchangeRateDataModule,
ExportModule, ExportModule,
HealthModule,
ImportModule, ImportModule,
InfoModule, InfoModule,
LogoModule, LogoModule,

View File

@ -1,7 +1,7 @@
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller'; import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
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.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/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';

View File

@ -1,5 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/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';

View File

@ -1,5 +1,5 @@
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { ConfigurationService } from '@ghostfolio/api/services/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 {
@ -33,8 +33,11 @@ export class AuthController {
private readonly webAuthService: WebAuthService private readonly webAuthService: WebAuthService
) {} ) {}
/**
* @deprecated
*/
@Get('anonymous/:accessToken') @Get('anonymous/:accessToken')
public async accessTokenLogin( public async accessTokenLoginGet(
@Param('accessToken') accessToken: string @Param('accessToken') accessToken: string
): Promise<OAuthResponse> { ): Promise<OAuthResponse> {
try { try {
@ -50,6 +53,23 @@ export class AuthController {
} }
} }
@Post('anonymous')
public async accessTokenLogin(
@Body() body: { accessToken: string }
): Promise<OAuthResponse> {
try {
const authToken = await this.authService.validateAnonymousLogin(
body.accessToken
);
return { authToken };
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
@Get('google') @Get('google')
@UseGuards(AuthGuard('google')) @UseGuards(AuthGuard('google'))
public googleLogin() { public googleLogin() {
@ -81,13 +101,13 @@ export class AuthController {
} }
} }
@Get('internet-identity/:principalId') @Post('internet-identity')
public async internetIdentityLogin( public async internetIdentityLogin(
@Param('principalId') principalId: string @Body() body: { principalId: string }
): Promise<OAuthResponse> { ): Promise<OAuthResponse> {
try { try {
const authToken = await this.authService.validateInternetIdentityLogin( const authToken = await this.authService.validateInternetIdentityLogin(
principalId body.principalId
); );
return { authToken }; return { authToken };
} catch { } catch {

View File

@ -2,8 +2,8 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.s
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/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';

View File

@ -1,5 +1,5 @@
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.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';

View File

@ -1,4 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/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';

View File

@ -1,6 +1,6 @@
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.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/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';

View File

@ -1,7 +1,7 @@
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto'; import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/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,

View File

@ -1,10 +1,10 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.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.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';

View File

@ -1,9 +1,9 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
MAX_CHART_ITEMS, MAX_CHART_ITEMS,
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS

View File

@ -1,10 +1,10 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/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.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/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';

View File

@ -1,4 +1,4 @@
import { ExchangeRateDataModule } from '@ghostfolio/api/services/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';

View File

@ -1,4 +1,4 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/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()

View File

@ -1,8 +1,8 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/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 { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ExportController } from './export.controller'; import { ExportController } from './export.controller';

View File

@ -1,5 +1,5 @@
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Export } from '@ghostfolio/common/interfaces'; import { Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';

View File

@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/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 { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Injectable, NestMiddleware } from '@nestjs/common';
@ -87,6 +87,13 @@ export class FrontendMiddleware implements NestMiddleware {
) { ) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png'; featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
title = `Ghostfolio meets Umbrel - ${title}`; title = `Ghostfolio meets Umbrel - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
)
) {
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
title = `Ghostfolio reaches 1000 Stars on GitHub - ${title}`;
} }
if ( if (

View File

@ -0,0 +1,44 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import {
Controller,
Get,
HttpException,
Param,
UseInterceptors
} from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { HealthService } from './health.service';
@Controller('health')
export class HealthController {
public constructor(private readonly healthService: HealthService) {}
@Get()
public async getHealth() {}
@Get('data-provider/:dataSource')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getHealthOfDataProvider(
@Param('dataSource') dataSource: DataSource
) {
if (!DataSource[dataSource]) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
const hasResponse = await this.healthService.hasResponseFromDataProvider(
dataSource
);
if (hasResponse !== true) {
throw new HttpException(
getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE),
StatusCodes.SERVICE_UNAVAILABLE
);
}
}
}

View File

@ -0,0 +1,13 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
import { HealthService } from './health.service';
@Module({
controllers: [HealthController],
imports: [ConfigurationModule, DataProviderModule],
providers: [HealthService]
})
export class HealthModule {}

View File

@ -0,0 +1,14 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@Injectable()
export class HealthService {
public constructor(
private readonly dataProviderService: DataProviderService
) {}
public async hasResponseFromDataProvider(aDataSource: DataSource) {
return this.dataProviderService.checkQuote(aDataSource);
}
}

View File

@ -1,6 +1,6 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
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';

View File

@ -3,12 +3,13 @@ import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/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.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.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 +25,7 @@ import { ImportService } from './import.service';
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,
OrderModule, OrderModule,
PlatformModule,
PortfolioModule, PortfolioModule,
PrismaModule, PrismaModule,
RedisCacheModule, RedisCacheModule,

View File

@ -5,8 +5,9 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { PlatformService } from '@ghostfolio/api/services/platform/platform.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { import {
@ -14,7 +15,7 @@ import {
OrderWithAccount OrderWithAccount
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns'; import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -26,6 +27,7 @@ export class ImportService {
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly orderService: OrderService, private readonly orderService: OrderService,
private readonly platformService: PlatformService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -118,15 +120,18 @@ export class ImportService {
const accountIdMapping: { [oldAccountId: string]: string } = {}; const accountIdMapping: { [oldAccountId: string]: string } = {};
if (!isDryRun && accountsDto?.length) { if (!isDryRun && accountsDto?.length) {
const existingAccounts = await this.accountService.accounts({ const [existingAccounts, existingPlatforms] = await Promise.all([
where: { this.accountService.accounts({
id: { where: {
in: accountsDto.map(({ id }) => { id: {
return id; in: accountsDto.map(({ id }) => {
}) return id;
})
}
} }
} }),
}); this.platformService.get()
]);
for (const account of accountsDto) { for (const account of accountsDto) {
// Check if there is any existing account with the same ID // Check if there is any existing account with the same ID
@ -146,19 +151,24 @@ export class ImportService {
delete account.id; delete account.id;
} }
const newAccountObject = { let accountObject: Prisma.AccountCreateInput = {
...account, ...account,
User: { connect: { id: userId } } User: { connect: { id: userId } }
}; };
if (platformId) { if (
Object.assign(newAccountObject, { existingPlatforms.some(({ id }) => {
return id === platformId;
})
) {
accountObject = {
...accountObject,
Platform: { connect: { id: platformId } } Platform: { connect: { id: platformId } }
}); };
} }
const newAccount = await this.accountService.createAccount( const newAccount = await this.accountService.createAccount(
newAccountObject, accountObject,
userId userId
); );
@ -173,9 +183,10 @@ export class ImportService {
for (const activity of activitiesDto) { for (const activity of activitiesDto) {
if (!activity.dataSource) { if (!activity.dataSource) {
if (activity.type === 'ITEM') { if (activity.type === 'ITEM') {
activity.dataSource = 'MANUAL'; activity.dataSource = DataSource.MANUAL;
} else { } else {
activity.dataSource = this.dataProviderService.getPrimaryDataSource(); activity.dataSource =
this.dataProviderService.getDataSourceForImport();
} }
} }
@ -254,6 +265,7 @@ export class ImportService {
countries: null, countries: null,
createdAt: undefined, createdAt: undefined,
id: undefined, id: undefined,
isin: null,
name: null, name: null,
scraperConfiguration: null, scraperConfiguration: null,
sectors: null, sectors: null,

View File

@ -1,12 +1,12 @@
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'; import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/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.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/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.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';

View File

@ -1,8 +1,8 @@
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service'; import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { import {
@ -22,6 +22,7 @@ import { InfoItem } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface'; import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
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 bent from 'bent'; import * as bent from 'bent';
@ -304,19 +305,17 @@ export class InfoService {
return statistics; return statistics;
} }
private async getSubscriptions(): Promise<Subscription[]> { private async getSubscriptions(): Promise<{
[offer in SubscriptionOffer]: Subscription;
}> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined; return undefined;
} }
let subscriptions: Subscription[] = [];
const stripeConfig = (await this.prismaService.property.findUnique({ const stripeConfig = (await this.prismaService.property.findUnique({
where: { key: PROPERTY_STRIPE_CONFIG } where: { key: PROPERTY_STRIPE_CONFIG }
})) ?? { value: '{}' }; })) ?? { value: '{}' };
subscriptions = [JSON.parse(stripeConfig.value)]; return JSON.parse(stripeConfig.value);
return subscriptions;
} }
} }

View File

@ -1,5 +1,5 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/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';

View File

@ -1,4 +1,4 @@
import { SymbolProfileService } from '@ghostfolio/api/services/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';

View File

@ -2,7 +2,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
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';
@ -41,6 +41,23 @@ export class OrderController {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@Delete()
@UseGuards(AuthGuard('jwt'))
public async deleteOrders(): Promise<number> {
if (
!hasPermission(this.request.user.permissions, permissions.deleteOrder)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.orderService.deleteOrders({
userId: this.request.user.id
});
}
@Delete(':id') @Delete(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async deleteOrder(@Param('id') id: string): Promise<OrderModel> { public async deleteOrder(@Param('id') id: string): Promise<OrderModel> {
@ -79,10 +96,7 @@ export class OrderController {
}); });
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(impersonationId);
impersonationId,
this.request.user.id
);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
const activities = await this.orderService.getOrders({ const activities = await this.orderService.getOrders({

View File

@ -3,13 +3,13 @@ import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/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.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/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';

View File

@ -1,8 +1,8 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS GATHER_ASSET_PROFILE_PROCESS_OPTIONS
@ -112,14 +112,17 @@ export class OrderService {
}; };
} }
await this.dataGatheringService.addJobToQueue( await this.dataGatheringService.addJobToQueue({
GATHER_ASSET_PROFILE_PROCESS, data: {
{
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol symbol: data.SymbolProfile.connectOrCreate.create.symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS name: GATHER_ASSET_PROFILE_PROCESS,
); opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}`
}
});
const isDraft = isAfter(data.date as Date, endOfToday()); const isDraft = isAfter(data.date as Date, endOfToday());
@ -178,6 +181,14 @@ export class OrderService {
return order; return order;
} }
public async deleteOrders(where: Prisma.OrderWhereInput): Promise<number> {
const { count } = await this.prismaService.order.deleteMany({
where
});
return count;
}
public async getOrders({ public async getOrders({
filters, filters,
includeDrafts = false, includeDrafts = false,

View File

@ -1,8 +1,8 @@
import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
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';
import { GetValuesObject } from './interfaces/get-values-object.interface';
import { GetValuesParams } from './interfaces/get-values-params.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface';
function mockGetValue(symbol: string, date: Date) { function mockGetValue(symbol: string, date: Date) {
@ -49,11 +49,9 @@ export const CurrentRateServiceMock = {
getValues: ({ getValues: ({
dataGatheringItems, dataGatheringItems,
dateQuery dateQuery
}: GetValuesParams): Promise<{ }: GetValuesParams): Promise<GetValuesObject> => {
dataProviderInfos: DataProviderInfo[];
values: GetValueObject[];
}> => {
const values: GetValueObject[] = []; const values: GetValueObject[] = [];
if (dateQuery.lt) { if (dateQuery.lt) {
for ( for (
let date = resetHours(dateQuery.gte); let date = resetHours(dateQuery.gte);
@ -85,6 +83,7 @@ export const CurrentRateServiceMock = {
} }
} }
} }
return Promise.resolve({ values, dataProviderInfos: [] });
return Promise.resolve({ values, dataProviderInfos: [], errors: [] });
} }
}; };

View File

@ -1,13 +1,13 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValuesObject } from './interfaces/get-values-object.interface';
jest.mock('@ghostfolio/api/services/market-data.service', () => { jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
return { return {
MarketDataService: jest.fn().mockImplementation(() => { MarketDataService: jest.fn().mockImplementation(() => {
return { return {
@ -18,7 +18,8 @@ jest.mock('@ghostfolio/api/services/market-data.service', () => {
createdAt: date, createdAt: date,
dataSource: DataSource.YAHOO, dataSource: DataSource.YAHOO,
id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584', id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584',
marketPrice: 1847.839966 marketPrice: 1847.839966,
state: 'CLOSE'
}); });
}, },
getRange: ({ getRange: ({
@ -37,6 +38,7 @@ jest.mock('@ghostfolio/api/services/market-data.service', () => {
date: dateRangeStart, date: dateRangeStart,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d', id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902, marketPrice: 1841.823902,
state: 'CLOSE',
symbol: symbols[0] symbol: symbols[0]
}, },
{ {
@ -45,6 +47,7 @@ jest.mock('@ghostfolio/api/services/market-data.service', () => {
date: dateRangeEnd, date: dateRangeEnd,
id: '082d6893-df27-4c91-8a5d-092e84315b56', id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966, marketPrice: 1847.839966,
state: 'CLOSE',
symbol: symbols[0] symbol: symbols[0]
} }
]); ]);
@ -54,14 +57,27 @@ jest.mock('@ghostfolio/api/services/market-data.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/services/exchange-rate-data.service', () => { jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return {
initialize: () => Promise.resolve(),
toCurrency: (value: number) => {
return 1 * value;
}
};
})
};
}
);
jest.mock('@ghostfolio/api/services/property/property.service', () => {
return { return {
ExchangeRateDataService: jest.fn().mockImplementation(() => { PropertyService: jest.fn().mockImplementation(() => {
return { return {
initialize: () => Promise.resolve(), getByKey: (key: string) => Promise.resolve({})
toCurrency: (value: number) => {
return 1 * value;
}
}; };
}) })
}; };
@ -72,9 +88,18 @@ describe('CurrentRateService', () => {
let dataProviderService: DataProviderService; let dataProviderService: DataProviderService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let marketDataService: MarketDataService; let marketDataService: MarketDataService;
let propertyService: PropertyService;
beforeAll(async () => { beforeAll(async () => {
dataProviderService = new DataProviderService(null, [], null); propertyService = new PropertyService(null);
dataProviderService = new DataProviderService(
null,
[],
null,
null,
propertyService
);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
null, null,
@ -104,21 +129,14 @@ describe('CurrentRateService', () => {
}, },
userCurrency: 'CHF' userCurrency: 'CHF'
}) })
).toMatchObject<{ ).toMatchObject<GetValuesObject>({
dataProviderInfos: DataProviderInfo[];
values: GetValueObject[];
}>({
dataProviderInfos: [], dataProviderInfos: [],
errors: [],
values: [ values: [
{ {
date: undefined, date: undefined,
marketPriceInBaseCurrency: 1841.823902, marketPriceInBaseCurrency: 1841.823902,
symbol: 'AMZN' symbol: 'AMZN'
},
{
date: undefined,
marketPriceInBaseCurrency: 1847.839966,
symbol: 'AMZN'
} }
] ]
}); });

View File

@ -1,13 +1,14 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/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';
import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isToday } from 'date-fns';
import { flatten } from 'lodash'; import { flatten, isEmpty, uniqBy } from 'lodash';
import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValueObject } from './interfaces/get-value-object.interface';
import { GetValuesObject } from './interfaces/get-values-object.interface';
import { GetValuesParams } from './interfaces/get-values-params.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface';
@Injectable() @Injectable()
@ -23,10 +24,7 @@ export class CurrentRateService {
dataGatheringItems, dataGatheringItems,
dateQuery, dateQuery,
userCurrency userCurrency
}: GetValuesParams): Promise<{ }: GetValuesParams): Promise<GetValuesObject> {
dataProviderInfos: DataProviderInfo[];
values: GetValueObject[];
}> {
const dataProviderInfos: DataProviderInfo[] = []; const dataProviderInfos: DataProviderInfo[] = [];
const includeToday = const includeToday =
(!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) &&
@ -34,9 +32,10 @@ export class CurrentRateService {
(!dateQuery.in || this.containsToday(dateQuery.in)); (!dateQuery.in || this.containsToday(dateQuery.in));
const promises: Promise<GetValueObject[]>[] = []; const promises: Promise<GetValueObject[]>[] = [];
const quoteErrors: ResponseError['errors'] = [];
const today = resetHours(new Date());
if (includeToday) { if (includeToday) {
const today = resetHours(new Date());
promises.push( promises.push(
this.dataProviderService this.dataProviderService
.getQuotes(dataGatheringItems) .getQuotes(dataGatheringItems)
@ -51,18 +50,26 @@ export class CurrentRateService {
); );
} }
result.push({ if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
date: today, result.push({
marketPriceInBaseCurrency: date: today,
this.exchangeRateDataService.toCurrency( marketPriceInBaseCurrency:
dataResultProvider?.[dataGatheringItem.symbol] this.exchangeRateDataService.toCurrency(
?.marketPrice ?? 0, dataResultProvider?.[dataGatheringItem.symbol]
dataResultProvider?.[dataGatheringItem.symbol]?.currency, ?.marketPrice,
userCurrency dataResultProvider?.[dataGatheringItem.symbol]?.currency,
), userCurrency
symbol: dataGatheringItem.symbol ),
}); symbol: dataGatheringItem.symbol
});
} else {
quoteErrors.push({
dataSource: dataGatheringItem.dataSource,
symbol: dataGatheringItem.symbol
});
}
} }
return result; return result;
}) })
); );
@ -94,10 +101,60 @@ export class CurrentRateService {
}) })
); );
return { const values = flatten(await Promise.all(promises));
const response: GetValuesObject = {
dataProviderInfos, dataProviderInfos,
values: flatten(await Promise.all(promises)) errors: quoteErrors.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
}),
values: uniqBy(values, ({ date, symbol }) => `${date}-${symbol}`)
}; };
if (!isEmpty(quoteErrors)) {
for (const { symbol } of quoteErrors) {
try {
// If missing quote, fallback to the latest available historical market price
let value: GetValueObject = response.values.find((currentValue) => {
return currentValue.symbol === symbol && isToday(currentValue.date);
});
if (!value) {
value = {
symbol,
date: today,
marketPriceInBaseCurrency: 0
};
response.values.push(value);
}
const [latestValue] = response.values
.filter((currentValue) => {
return (
currentValue.symbol === symbol &&
currentValue.marketPriceInBaseCurrency
);
})
.sort((a, b) => {
if (a.date < b.date) {
return 1;
}
if (a.date > b.date) {
return -1;
}
return 0;
});
value.marketPriceInBaseCurrency =
latestValue.marketPriceInBaseCurrency;
} catch {}
}
}
return response;
} }
private containsToday(dates: Date[]): boolean { private containsToday(dates: Date[]): boolean {

View File

@ -0,0 +1,9 @@
import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces';
import { GetValueObject } from './get-value-object.interface';
export interface GetValuesObject {
dataProviderInfos: DataProviderInfo[];
errors: ResponseError['errors'];
values: GetValueObject[];
}

View File

@ -86,7 +86,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentage: 13.100263852242744, netPerformanceInPercentage: 13.100263852242744,
netPerformance: 19.86, netPerformance: 19.86,
totalInvestment: 0, totalInvestment: 0,
value: 19.86 value: 0
}); });
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({

View File

@ -24,9 +24,10 @@ import {
isSameYear, isSameYear,
max, max,
min, min,
set set,
subDays
} from 'date-fns'; } from 'date-fns';
import { first, flatten, isNumber, last, sortBy } from 'lodash'; import { first, flatten, isNumber, last, sortBy, uniq } from 'lodash';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface'; import { CurrentPositions } from './interfaces/current-positions.interface';
@ -182,10 +183,10 @@ export class PortfolioCalculator {
return isBefore(parseDate(transactionPoint.date), end); return isBefore(parseDate(transactionPoint.date), end);
}) ?? []; }) ?? [];
const firstIndex = transactionPointsBeforeEndDate.length; const currencies: { [symbol: string]: string } = {};
const dates: Date[] = []; const dates: Date[] = [];
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: string } = {}; const firstIndex = transactionPointsBeforeEndDate.length;
let day = start; let day = start;
@ -235,87 +236,100 @@ export class PortfolioCalculator {
} }
} }
const netPerformanceValuesBySymbol: { const valuesByDate: {
[symbol: string]: { [date: string]: Big }; [date: string]: {
maxTotalInvestmentValue: Big;
totalCurrentValue: Big;
totalInvestmentValue: Big;
totalNetPerformanceValue: Big;
};
} = {}; } = {};
const investmentValuesBySymbol: { const valuesBySymbol: {
[symbol: string]: { [date: string]: Big }; [symbol: string]: {
currentValues: { [date: string]: Big };
investmentValues: { [date: string]: Big };
maxInvestmentValues: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big };
};
} = {}; } = {};
const maxInvestmentValuesBySymbol: {
[symbol: string]: { [date: string]: Big };
} = {};
const totalNetPerformanceValues: { [date: string]: Big } = {};
const totalInvestmentValues: { [date: string]: Big } = {};
const maxTotalInvestmentValues: { [date: string]: Big } = {};
for (const symbol of Object.keys(symbols)) { for (const symbol of Object.keys(symbols)) {
const { investmentValues, maxInvestmentValues, netPerformanceValues } = const {
this.getSymbolMetrics({ currentValues,
end, investmentValues,
marketSymbolMap, maxInvestmentValues,
start, netPerformanceValues
step, } = this.getSymbolMetrics({
symbol, end,
isChartMode: true marketSymbolMap,
}); start,
step,
symbol,
isChartMode: true
});
netPerformanceValuesBySymbol[symbol] = netPerformanceValues; valuesBySymbol[symbol] = {
investmentValuesBySymbol[symbol] = investmentValues; currentValues,
maxInvestmentValuesBySymbol[symbol] = maxInvestmentValues; investmentValues,
maxInvestmentValues,
netPerformanceValues
};
} }
for (const currentDate of dates) { for (const currentDate of dates) {
const dateString = format(currentDate, DATE_FORMAT); const dateString = format(currentDate, DATE_FORMAT);
for (const symbol of Object.keys(netPerformanceValuesBySymbol)) { for (const symbol of Object.keys(valuesBySymbol)) {
totalNetPerformanceValues[dateString] = const symbolValues = valuesBySymbol[symbol];
totalNetPerformanceValues[dateString] ?? new Big(0);
if (netPerformanceValuesBySymbol[symbol]?.[dateString]) { const currentValue =
totalNetPerformanceValues[dateString] = totalNetPerformanceValues[ symbolValues.currentValues?.[dateString] ?? new Big(0);
dateString const investmentValue =
].add(netPerformanceValuesBySymbol[symbol][dateString]); symbolValues.investmentValues?.[dateString] ?? new Big(0);
} const maxInvestmentValue =
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0);
const netPerformanceValue =
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
totalInvestmentValues[dateString] = valuesByDate[dateString] = {
totalInvestmentValues[dateString] ?? new Big(0); totalCurrentValue: (
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
maxTotalInvestmentValues[dateString] = ).add(currentValue),
maxTotalInvestmentValues[dateString] ?? new Big(0); totalInvestmentValue: (
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
if (investmentValuesBySymbol[symbol]?.[dateString]) { ).add(investmentValue),
totalInvestmentValues[dateString] = totalInvestmentValues[ maxTotalInvestmentValue: (
dateString valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0)
].add(investmentValuesBySymbol[symbol][dateString]); ).add(maxInvestmentValue),
} totalNetPerformanceValue: (
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0)
if (maxInvestmentValuesBySymbol[symbol]?.[dateString]) { ).add(netPerformanceValue)
maxTotalInvestmentValues[dateString] = maxTotalInvestmentValues[ };
dateString
].add(maxInvestmentValuesBySymbol[symbol][dateString]);
}
} }
} }
return Object.keys(totalNetPerformanceValues).map((date) => { return Object.entries(valuesByDate).map(([date, values]) => {
const netPerformanceInPercentage = maxTotalInvestmentValues[date].eq(0) const {
maxTotalInvestmentValue,
totalCurrentValue,
totalInvestmentValue,
totalNetPerformanceValue
} = values;
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
? 0 ? 0
: totalNetPerformanceValues[date] : totalNetPerformanceValue
.div(maxTotalInvestmentValues[date]) .div(maxTotalInvestmentValue)
.mul(100) .mul(100)
.toNumber(); .toNumber();
return { return {
date, date,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformance: totalNetPerformanceValues[date].toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
totalInvestment: totalInvestmentValues[date].toNumber(), totalInvestment: totalInvestmentValue.toNumber(),
value: totalInvestmentValues[date] value: totalCurrentValue.toNumber()
.plus(totalNetPerformanceValues[date])
.toNumber()
}; };
}); });
} }
@ -347,7 +361,7 @@ export class PortfolioCalculator {
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
let firstIndex = transactionPointsBeforeEndDate.length; let firstIndex = transactionPointsBeforeEndDate.length;
const dates = []; let dates = [];
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
@ -376,15 +390,37 @@ export class PortfolioCalculator {
dates.push(resetHours(end)); dates.push(resetHours(end));
const { dataProviderInfos, values: marketSymbols } = // Add dates of last week for fallback
await this.currentRateService.getValues({ dates.push(subDays(resetHours(new Date()), 7));
currencies, dates.push(subDays(resetHours(new Date()), 6));
dataGatheringItems, dates.push(subDays(resetHours(new Date()), 5));
dateQuery: { dates.push(subDays(resetHours(new Date()), 4));
in: dates dates.push(subDays(resetHours(new Date()), 3));
}, dates.push(subDays(resetHours(new Date()), 2));
userCurrency: this.currency dates.push(subDays(resetHours(new Date()), 1));
}); dates.push(resetHours(new Date()));
dates = uniq(
dates.map((date) => {
return date.getTime();
})
).map((timestamp) => {
return new Date(timestamp);
});
dates.sort((a, b) => a.getTime() - b.getTime());
const {
dataProviderInfos,
errors: currentRateErrors,
values: marketSymbols
} = await this.currentRateService.getValues({
currencies,
dataGatheringItems,
dateQuery: {
in: dates
},
userCurrency: this.currency
});
this.dataProviderInfos = dataProviderInfos; this.dataProviderInfos = dataProviderInfos;
@ -459,7 +495,13 @@ export class PortfolioCalculator {
transactionCount: item.transactionCount transactionCount: item.transactionCount
}); });
if (hasErrors && item.investment.gt(0)) { if (
(hasErrors ||
currentRateErrors.find(({ dataSource, symbol }) => {
return dataSource === item.dataSource && symbol === item.symbol;
})) &&
item.investment.gt(0)
) {
errors.push({ dataSource: item.dataSource, symbol: item.symbol }); errors.push({ dataSource: item.dataSource, symbol: item.symbol });
} }
} }
@ -709,7 +751,7 @@ export class PortfolioCalculator {
); );
} else if (!currentPosition.quantity.eq(0)) { } else if (!currentPosition.quantity.eq(0)) {
Logger.warn( Logger.warn(
`Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}`, `Missing historical market data for symbol ${currentPosition.symbol}`,
'PortfolioCalculator' 'PortfolioCalculator'
); );
hasErrors = true; hasErrors = true;
@ -906,12 +948,16 @@ export class PortfolioCalculator {
if (orders.length <= 0) { if (orders.length <= 0) {
return { return {
currentValues: {},
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
hasErrors: false, hasErrors: false,
initialValue: new Big(0), initialValue: new Big(0),
investmentValues: {},
maxInvestmentValues: {},
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
grossPerformance: new Big(0), netPerformanceValues: {}
grossPerformancePercentage: new Big(0)
}; };
} }
@ -946,6 +992,7 @@ export class PortfolioCalculator {
let grossPerformanceFromSells = new Big(0); let grossPerformanceFromSells = new Big(0);
let initialValue: Big; let initialValue: Big;
let investmentAtStartDate: Big; let investmentAtStartDate: Big;
const currentValues: { [date: string]: Big } = {};
const investmentValues: { [date: string]: Big } = {}; const investmentValues: { [date: string]: Big } = {};
const maxInvestmentValues: { [date: string]: Big } = {}; const maxInvestmentValues: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0); let lastAveragePrice = new Big(0);
@ -1164,6 +1211,7 @@ export class PortfolioCalculator {
} }
if (isChartMode && i > indexOfStartOrder) { if (isChartMode && i > indexOfStartOrder) {
currentValues[order.date] = valueOfInvestment;
netPerformanceValues[order.date] = grossPerformance netPerformanceValues[order.date] = grossPerformance
.minus(grossPerformanceAtStartDate) .minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate)); .minus(fees.minus(feesAtStartDate));
@ -1261,15 +1309,16 @@ export class PortfolioCalculator {
} }
return { return {
initialValue, currentValues,
grossPerformancePercentage, grossPerformancePercentage,
initialValue,
investmentValues, investmentValues,
maxInvestmentValues, maxInvestmentValues,
netPerformancePercentage, netPerformancePercentage,
netPerformanceValues, netPerformanceValues,
grossPerformance: totalGrossPerformance,
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
netPerformance: totalNetPerformance, netPerformance: totalNetPerformance
grossPerformance: totalGrossPerformance
}; };
} }

View File

@ -8,8 +8,8 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { import {
PortfolioDetails, PortfolioDetails,
@ -91,6 +91,7 @@ export class PortfolioController {
filteredValueInPercentage, filteredValueInPercentage,
hasErrors, hasErrors,
holdings, holdings,
platforms,
summary, summary,
totalValueInBaseCurrency totalValueInBaseCurrency
} = await this.portfolioService.getDetails({ } = await this.portfolioService.getDetails({
@ -136,9 +137,12 @@ export class PortfolioController {
portfolioPosition.value / totalValue; portfolioPosition.value / totalValue;
} }
for (const [name, { current, original }] of Object.entries(accounts)) { for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {
accounts[name].current = current / totalValue; accounts[name].valueInPercentage = valueInBaseCurrency / totalValue;
accounts[name].original = original / totalInvestment; }
for (const [name, { valueInBaseCurrency }] of Object.entries(platforms)) {
platforms[name].valueInPercentage = valueInBaseCurrency / totalValue;
} }
} }
@ -182,6 +186,7 @@ export class PortfolioController {
filteredValueInPercentage, filteredValueInPercentage,
hasError, hasError,
holdings, holdings,
platforms,
totalValueInBaseCurrency, totalValueInBaseCurrency,
summary: portfolioSummary summary: portfolioSummary
}; };

View File

@ -3,14 +3,14 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/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.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/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';

View File

@ -7,18 +7,15 @@ import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfol
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 { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
import { 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';
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-initial-investment';
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
EMERGENCY_FUND_TAG_ID, EMERGENCY_FUND_TAG_ID,
MAX_CHART_ITEMS, MAX_CHART_ITEMS,
@ -37,8 +34,7 @@ import {
PortfolioSummary, PortfolioSummary,
Position, Position,
TimelinePosition, TimelinePosition,
UserSettings, UserSettings
UserWithSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type { import type {
@ -47,7 +43,8 @@ import type {
GroupBy, GroupBy,
Market, Market,
OrderWithAccount, OrderWithAccount,
RequestWithUser RequestWithUser,
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';
@ -149,7 +146,8 @@ export class PortfolioService {
} }
} }
const valueInBaseCurrency = details.accounts[account.id]?.current ?? 0; const valueInBaseCurrency =
details.accounts[account.id]?.valueInBaseCurrency ?? 0;
const result = { const result = {
...account, ...account,
@ -462,10 +460,18 @@ export class PortfolioService {
}); });
const holdings: PortfolioDetails['holdings'] = {}; const holdings: PortfolioDetails['holdings'] = {};
const totalInvestmentInBaseCurrency = currentPositions.totalInvestment.plus( const totalValueInBaseCurrency = currentPositions.currentValue.plus(
cashDetails.balanceInBaseCurrency cashDetails.balanceInBaseCurrency
); );
let filteredValueInBaseCurrency = currentPositions.currentValue;
const isFilteredByAccount =
filters?.some((filter) => {
return filter.type === 'ACCOUNT';
}) ?? false;
let filteredValueInBaseCurrency = isFilteredByAccount
? totalValueInBaseCurrency
: currentPositions.currentValue;
if ( if (
filters?.length === 0 || filters?.length === 0 ||
@ -484,13 +490,10 @@ export class PortfolioService {
symbol: position.symbol symbol: position.symbol
}; };
}); });
const symbols = currentPositions.positions.map(
(position) => position.symbol
);
const [dataProviderResponses, symbolProfiles] = await Promise.all([ const [dataProviderResponses, symbolProfiles] = await Promise.all([
this.dataProviderService.getQuotes(dataGatheringItems), this.dataProviderService.getQuotes(dataGatheringItems),
this.symbolProfileService.getSymbolProfilesBySymbols(symbols) this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
]); ]);
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
@ -564,12 +567,11 @@ export class PortfolioService {
}; };
} }
if ( const isFilteredByCash = filters?.some((filter) => {
filters?.length === 0 || return filter.type === 'ASSET_CLASS' && filter.id === 'CASH';
(filters?.length === 1 && });
filters[0].type === 'ASSET_CLASS' &&
filters[0].id === 'CASH') if (filters?.length === 0 || isFilteredByAccount || isFilteredByCash) {
) {
const cashPositions = await this.getCashPositions({ const cashPositions = await this.getCashPositions({
cashDetails, cashDetails,
userCurrency, userCurrency,
@ -581,7 +583,7 @@ export class PortfolioService {
} }
} }
const accounts = await this.getValueOfAccounts({ const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
filters, filters,
orders, orders,
portfolioItemsNow, portfolioItemsNow,
@ -595,7 +597,7 @@ export class PortfolioService {
filters[0].id === EMERGENCY_FUND_TAG_ID && filters[0].id === EMERGENCY_FUND_TAG_ID &&
filters[0].type === 'TAG' filters[0].type === 'TAG'
) { ) {
const cashPositions = await this.getCashPositions({ const emergencyFundCashPositions = await this.getCashPositions({
cashDetails, cashDetails,
userCurrency, userCurrency,
value: filteredValueInBaseCurrency value: filteredValueInBaseCurrency
@ -614,13 +616,12 @@ export class PortfolioService {
accounts[UNKNOWN_KEY] = { accounts[UNKNOWN_KEY] = {
balance: 0, balance: 0,
currency: userCurrency, currency: userCurrency,
current: emergencyFundInCash,
name: UNKNOWN_KEY, name: UNKNOWN_KEY,
original: emergencyFundInCash valueInBaseCurrency: emergencyFundInCash
}; };
holdings[userCurrency] = { holdings[userCurrency] = {
...cashPositions[userCurrency], ...emergencyFundCashPositions[userCurrency],
investment: emergencyFundInCash, investment: emergencyFundInCash,
value: emergencyFundInCash value: emergencyFundInCash
}; };
@ -640,6 +641,7 @@ export class PortfolioService {
return { return {
accounts, accounts,
holdings, holdings,
platforms,
summary, summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth filteredValueInPercentage: summary.netWorth
@ -979,11 +981,13 @@ export class PortfolioService {
}; };
}); });
const symbols = positions.map((position) => position.symbol);
const [dataProviderResponses, symbolProfiles] = await Promise.all([ const [dataProviderResponses, symbolProfiles] = await Promise.all([
this.dataProviderService.getQuotes(dataGatheringItem), this.dataProviderService.getQuotes(dataGatheringItem),
this.symbolProfileService.getSymbolProfilesBySymbols(symbols) this.symbolProfileService.getSymbolProfiles(
positions.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
})
)
]); ]);
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
@ -1168,7 +1172,7 @@ export class PortfolioService {
portfolioItemsNow[position.symbol] = position; portfolioItemsNow[position.symbol] = position;
} }
const accounts = await this.getValueOfAccounts({ const { accounts } = await this.getValueOfAccountsAndPlatforms({
orders, orders,
portfolioItemsNow, portfolioItemsNow,
userCurrency, userCurrency,
@ -1179,10 +1183,6 @@ export class PortfolioService {
rules: { rules: {
accountClusterRisk: await this.rulesService.evaluate( accountClusterRisk: await this.rulesService.evaluate(
[ [
new AccountClusterRiskInitialInvestment(
this.exchangeRateDataService,
accounts
),
new AccountClusterRiskCurrentInvestment( new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
accounts accounts
@ -1196,18 +1196,10 @@ export class PortfolioService {
), ),
currencyClusterRisk: await this.rulesService.evaluate( currencyClusterRisk: await this.rulesService.evaluate(
[ [
new CurrencyClusterRiskBaseCurrencyInitialInvestment(
this.exchangeRateDataService,
positions
),
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
positions positions
), ),
new CurrencyClusterRiskInitialInvestment(
this.exchangeRateDataService,
positions
),
new CurrencyClusterRiskCurrentInvestment( new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
positions positions
@ -1700,7 +1692,7 @@ export class PortfolioService {
}; };
} }
private async getValueOfAccounts({ private async getValueOfAccountsAndPlatforms({
filters = [], filters = [],
orders, orders,
portfolioItemsNow, portfolioItemsNow,
@ -1724,6 +1716,7 @@ export class PortfolioService {
}); });
const accounts: PortfolioDetails['accounts'] = {}; const accounts: PortfolioDetails['accounts'] = {};
const platforms: PortfolioDetails['platforms'] = {};
let currentAccounts: (Account & { let currentAccounts: (Account & {
Order?: Order[]; Order?: Order[];
@ -1768,63 +1761,81 @@ export class PortfolioService {
accounts[account.id] = { accounts[account.id] = {
balance: account.balance, balance: account.balance,
currency: account.currency, currency: account.currency,
current: this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
),
name: account.name, name: account.name,
original: this.exchangeRateDataService.toCurrency( valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
account.balance, account.balance,
account.currency, account.currency,
userCurrency userCurrency
) )
}; };
if (platforms[account.Platform?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
platforms[account.Platform?.id || UNKNOWN_KEY].valueInBaseCurrency +=
this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
);
} else {
platforms[account.Platform?.id || UNKNOWN_KEY] = {
balance: account.balance,
currency: account.currency,
name: account.Platform?.name,
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
)
};
}
for (const order of ordersByAccount) { for (const order of ordersByAccount) {
let currentValueOfSymbolInBaseCurrency = let currentValueOfSymbolInBaseCurrency =
order.quantity * order.quantity *
(portfolioItemsNow[order.SymbolProfile.symbol]?.marketPrice ?? (portfolioItemsNow[order.SymbolProfile.symbol]?.marketPrice ??
order.unitPrice ?? order.unitPrice ??
0); 0);
let originalValueOfSymbolInBaseCurrency =
this.exchangeRateDataService.toCurrency(
order.quantity * order.unitPrice,
order.SymbolProfile.currency,
userCurrency
);
if (order.type === 'SELL') { if (order.type === 'SELL') {
currentValueOfSymbolInBaseCurrency *= -1; currentValueOfSymbolInBaseCurrency *= -1;
originalValueOfSymbolInBaseCurrency *= -1;
} }
if (accounts[order.Account?.id || UNKNOWN_KEY]?.current) { if (accounts[order.Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
accounts[order.Account?.id || UNKNOWN_KEY].current += accounts[order.Account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
currentValueOfSymbolInBaseCurrency; currentValueOfSymbolInBaseCurrency;
accounts[order.Account?.id || UNKNOWN_KEY].original +=
originalValueOfSymbolInBaseCurrency;
} else { } else {
accounts[order.Account?.id || UNKNOWN_KEY] = { accounts[order.Account?.id || UNKNOWN_KEY] = {
balance: 0, balance: 0,
currency: order.Account?.currency, currency: order.Account?.currency,
current: currentValueOfSymbolInBaseCurrency,
name: account.name, name: account.name,
original: originalValueOfSymbolInBaseCurrency valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
};
}
if (
platforms[order.Account?.Platform?.id || UNKNOWN_KEY]
?.valueInBaseCurrency
) {
platforms[
order.Account?.Platform?.id || UNKNOWN_KEY
].valueInBaseCurrency += currentValueOfSymbolInBaseCurrency;
} else {
platforms[order.Account?.Platform?.id || UNKNOWN_KEY] = {
balance: 0,
currency: order.Account?.currency,
name: account.Platform?.name,
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
}; };
} }
} }
} }
return accounts; return { accounts, platforms };
} }
private async getUserId(aImpersonationId: string, aUserId: string) { private async getUserId(aImpersonationId: string, aUserId: string) {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(aImpersonationId);
aImpersonationId,
aUserId
);
return impersonationUserId || aUserId; return impersonationUserId || aUserId;
} }

View File

@ -1,5 +1,5 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CacheManagerOptions, CacheModule, Module } from '@nestjs/common'; import { CacheManagerOptions, CacheModule, Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store'; import * as redisStore from 'cache-manager-redis-store';

View File

@ -1,4 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common'; import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager'; import { Cache } from 'cache-manager';

View File

@ -1,4 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/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 { import {
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,

View File

@ -1,5 +1,5 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/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';

View File

@ -1,12 +1,12 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { import {
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,
PROPERTY_STRIPE_CONFIG PROPERTY_STRIPE_CONFIG
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { UserWithSettings } from '@ghostfolio/common/interfaces';
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface'; import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { UserWithSettings } from '@ghostfolio/common/types';
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';
@ -123,7 +123,9 @@ export class SubscriptionService {
} }
} }
public getSubscription(aSubscriptions: Subscription[]) { public getSubscription(
aSubscriptions: Subscription[]
): UserWithSettings['subscription'] {
if (aSubscriptions.length > 0) { if (aSubscriptions.length > 0) {
const latestSubscription = aSubscriptions.reduce((a, b) => { const latestSubscription = aSubscriptions.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;
@ -131,12 +133,14 @@ export class SubscriptionService {
return { return {
expiresAt: latestSubscription.expiresAt, expiresAt: latestSubscription.expiresAt,
offer: latestSubscription.price === 0 ? 'default' : 'renewal',
type: isBefore(new Date(), latestSubscription.expiresAt) type: isBefore(new Date(), latestSubscription.expiresAt)
? SubscriptionType.Premium ? SubscriptionType.Premium
: SubscriptionType.Basic : SubscriptionType.Basic
}; };
} else { } else {
return { return {
offer: 'default',
type: SubscriptionType.Basic type: SubscriptionType.Basic
}; };
} }

View File

@ -1,6 +1,8 @@
import { DataSource } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
export interface LookupItem { export interface LookupItem {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
name: string; name: string;

View File

@ -1,15 +1,18 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
Get, Get,
HttpException, HttpException,
Inject,
Param, Param,
Query, Query,
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -21,7 +24,10 @@ import { SymbolService } from './symbol.service';
@Controller('symbol') @Controller('symbol')
export class SymbolController { export class SymbolController {
public constructor(private readonly symbolService: SymbolService) {} public constructor(
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolService: SymbolService
) {}
/** /**
* Must be before /:symbol * Must be before /:symbol
@ -33,7 +39,10 @@ export class SymbolController {
@Query() { query = '' } @Query() { query = '' }
): Promise<{ items: LookupItem[] }> { ): Promise<{ items: LookupItem[] }> {
try { try {
return this.symbolService.lookup(query.toLowerCase()); return this.symbolService.lookup({
query: query.toLowerCase(),
user: this.request.user
});
} catch { } catch {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),

View File

@ -1,7 +1,7 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SymbolController } from './symbol.controller'; import { SymbolController } from './symbol.controller';

View File

@ -3,9 +3,10 @@ import {
IDataGatheringItem, IDataGatheringItem,
IDataProviderHistoricalResponse IDataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { format, subDays } from 'date-fns'; import { format, subDays } from 'date-fns';
@ -79,15 +80,24 @@ export class SymbolService {
}; };
} }
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> { public async lookup({
query,
user
}: {
query: string;
user: UserWithSettings;
}): Promise<{ items: LookupItem[] }> {
const results: { items: LookupItem[] } = { items: [] }; const results: { items: LookupItem[] } = { items: [] };
if (!aQuery) { if (!query) {
return results; return results;
} }
try { try {
const { items } = await this.dataProviderService.search(aQuery); const { items } = await this.dataProviderService.search({
query,
user
});
results.items = items; results.items = items;
return results; return results;
} catch (error) { } catch (error) {

View File

@ -3,17 +3,20 @@ import type {
DateRange, DateRange,
ViewMode ViewMode
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Type } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsIn,
IsISO8601, IsISO8601,
IsIn,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString IsString
} from 'class-validator'; } from 'class-validator';
export class UpdateUserSettingDto { export class UpdateUserSettingDto {
@IsNumber()
@IsOptional()
annualInterestRate?: number;
@IsOptional() @IsOptional()
@IsString() @IsString()
baseCurrency?: string; baseCurrency?: string;

View File

@ -1,6 +1,6 @@
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/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 { 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';

View File

@ -1,19 +1,17 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { environment } from '@ghostfolio/api/environments/environment';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config'; import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
import { import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
User as IUser,
UserSettings,
UserWithSettings
} from '@ghostfolio/common/interfaces';
import { import {
getPermissions, getPermissions,
hasRole, hasRole,
permissions permissions
} from '@ghostfolio/common/permissions'; } from '@ghostfolio/common/permissions';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, Role, User } from '@prisma/client'; import { Prisma, Role, User } from '@prisma/client';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
@ -199,6 +197,10 @@ export class UserService {
} }
} }
if (!environment.production && role === 'ADMIN') {
currentPermissions.push(permissions.impersonateAllUsers);
}
user.Account = sortBy(user.Account, (account) => { user.Account = sortBy(user.Account, (account) => {
return account.name; return account.name;
}); });

View File

@ -1,3 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { decodeDataSource } from '@ghostfolio/common/helper'; import { decodeDataSource } from '@ghostfolio/common/helper';
import { import {
CallHandler, CallHandler,
@ -7,8 +8,6 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ConfigurationService } from '../services/configuration.service';
@Injectable() @Injectable()
export class TransformDataSourceInRequestInterceptor<T> export class TransformDataSourceInRequestInterceptor<T>
implements NestInterceptor<T, any> implements NestInterceptor<T, any>

View File

@ -1,4 +1,5 @@
import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { encodeDataSource } from '@ghostfolio/common/helper'; import { encodeDataSource } from '@ghostfolio/common/helper';
import { import {
CallHandler, CallHandler,
@ -10,8 +11,6 @@ import { DataSource } from '@prisma/client';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ConfigurationService } from '../services/configuration.service';
@Injectable() @Injectable()
export class TransformDataSourceInResponseInterceptor<T> export class TransformDataSourceInResponseInterceptor<T>
implements NestInterceptor<T, any> implements NestInterceptor<T, any>

View File

@ -32,12 +32,23 @@ async function bootstrap() {
// Support 10mb csv/json files for importing activities // Support 10mb csv/json files for importing activities
app.use(bodyParser.json({ limit: '10mb' })); app.use(bodyParser.json({ limit: '10mb' }));
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
const HOST = configService.get<string>('HOST') || '0.0.0.0'; const HOST = configService.get<string>('HOST') || '0.0.0.0';
const PORT = configService.get<number>('PORT') || 3333; const PORT = configService.get<number>('PORT') || 3333;
await app.listen(PORT, HOST, () => { await app.listen(PORT, HOST, () => {
logLogo(); logLogo();
Logger.log(`Listening at http://${HOST}:${PORT}`); Logger.log(`Listening at http://${HOST}:${PORT}`);
Logger.log(''); Logger.log('');
if (BASE_CURRENCY) {
Logger.warn(
`The environment variable "BASE_CURRENCY" is deprecated and will be removed in Ghostfolio 2.0.`
);
Logger.warn(
'Please use the currency converter in the activity dialog instead.'
);
}
}); });
} }

View File

@ -1,8 +1,7 @@
import { IOrder } from '@ghostfolio/api/services/interfaces/interfaces';
import { Account, SymbolProfile, Type as TypeOfOrder } from '@prisma/client'; import { Account, SymbolProfile, Type as TypeOfOrder } from '@prisma/client';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { IOrder } from '../services/interfaces/interfaces';
export class Order { export class Order {
private account: Account; private account: Account;
private currency: string; private currency: string;

View File

@ -1,5 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { groupBy } from '@ghostfolio/common/helper'; import { groupBy } from '@ghostfolio/common/helper';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';

View File

@ -1,5 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioPosition, PortfolioPosition,
@ -14,7 +14,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
private accounts: PortfolioDetails['accounts'] private accounts: PortfolioDetails['accounts']
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Current Investment' name: 'Investment'
}); });
} }
@ -28,7 +28,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
for (const [accountId, account] of Object.entries(this.accounts)) { for (const [accountId, account] of Object.entries(this.accounts)) {
accounts[accountId] = { accounts[accountId] = {
name: account.name, name: account.name,
investment: account.current investment: account.valueInBaseCurrency
}; };
} }

View File

@ -1,88 +0,0 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import {
PortfolioDetails,
PortfolioPosition,
UserSettings
} from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private accounts: PortfolioDetails['accounts']
) {
super(exchangeRateDataService, {
name: 'Initial Investment'
});
}
public evaluate(ruleSettings?: Settings) {
const accounts: {
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
investment: number;
};
} = {};
for (const [accountId, account] of Object.entries(this.accounts)) {
accounts[accountId] = {
name: account.name,
investment: account.original
};
}
let maxItem;
let totalInvestment = 0;
for (const account of Object.values(accounts)) {
if (!maxItem) {
maxItem = account;
}
// Calculate total investment
totalInvestment += account.investment;
// Find maximum
if (account.investment > maxItem?.investment) {
maxItem = account;
}
}
const maxInvestmentRatio = maxItem.investment / totalInvestment;
if (maxInvestmentRatio > ruleSettings.threshold) {
return {
evaluation: `Over ${
ruleSettings.threshold * 100
}% of your initial investment is at ${maxItem.name} (${(
maxInvestmentRatio * 100
).toPrecision(3)}%)`,
value: false
};
}
return {
evaluation: `The major part of your initial investment is at ${
maxItem.name
} (${(maxInvestmentRatio * 100).toPrecision(3)}%) and does not exceed ${
ruleSettings.threshold * 100
}%`,
value: true
};
}
public getSettings(aUserSettings: UserSettings): Settings {
return {
baseCurrency: aUserSettings.baseCurrency,
isActive: true,
threshold: 0.5
};
}
}
interface Settings extends RuleSettings {
baseCurrency: string;
isActive: boolean;
threshold: number;
}

View File

@ -1,5 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces'; import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,5 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';
@ -10,7 +10,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
private positions: TimelinePosition[] private positions: TimelinePosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Current Investment: Base Currency' name: 'Investment: Base Currency'
}); });
} }

View File

@ -1,71 +0,0 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: TimelinePosition[]
) {
super(exchangeRateDataService, {
name: 'Initial Investment: Base Currency'
});
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.positions,
'currency',
ruleSettings.baseCurrency
);
let maxItem = positionsGroupedByCurrency[0];
let totalInvestment = 0;
positionsGroupedByCurrency.forEach((groupItem) => {
// Calculate total investment
totalInvestment += groupItem.investment;
// Find maximum
if (groupItem.investment > maxItem.investment) {
maxItem = groupItem;
}
});
const baseCurrencyItem = positionsGroupedByCurrency.find((item) => {
return item.groupKey === ruleSettings.baseCurrency;
});
const baseCurrencyInvestmentRatio =
baseCurrencyItem?.investment / totalInvestment || 0;
if (maxItem.groupKey !== ruleSettings.baseCurrency) {
return {
evaluation: `The major part of your initial investment is not in your base currency (${(
baseCurrencyInvestmentRatio * 100
).toPrecision(3)}% in ${ruleSettings.baseCurrency})`,
value: false
};
}
return {
evaluation: `The major part of your initial investment is in your base currency (${(
baseCurrencyInvestmentRatio * 100
).toPrecision(3)}% in ${ruleSettings.baseCurrency})`,
value: true
};
}
public getSettings(aUserSettings: UserSettings): Settings {
return {
baseCurrency: aUserSettings.baseCurrency,
isActive: true
};
}
}
interface Settings extends RuleSettings {
baseCurrency: string;
}

View File

@ -1,5 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';
@ -10,7 +10,7 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[] private positions: TimelinePosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Current Investment' name: 'Investment'
}); });
} }

View File

@ -1,72 +0,0 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class CurrencyClusterRiskInitialInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: TimelinePosition[]
) {
super(exchangeRateDataService, {
name: 'Initial Investment'
});
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.positions,
'currency',
ruleSettings.baseCurrency
);
let maxItem = positionsGroupedByCurrency[0];
let totalInvestment = 0;
positionsGroupedByCurrency.forEach((groupItem) => {
// Calculate total investment
totalInvestment += groupItem.investment;
// Find maximum
if (groupItem.investment > maxItem.investment) {
maxItem = groupItem;
}
});
const maxInvestmentRatio = maxItem.investment / totalInvestment;
if (maxInvestmentRatio > ruleSettings.threshold) {
return {
evaluation: `Over ${
ruleSettings.threshold * 100
}% of your initial investment is in ${maxItem.groupKey} (${(
maxInvestmentRatio * 100
).toPrecision(3)}%)`,
value: false
};
}
return {
evaluation: `The major part of your initial investment is in ${
maxItem.groupKey
} (${(maxInvestmentRatio * 100).toPrecision(3)}%) and does not exceed ${
ruleSettings.threshold * 100
}%`,
value: true
};
}
public getSettings(aUserSettings: UserSettings): Settings {
return {
baseCurrency: aUserSettings.baseCurrency,
isActive: true,
threshold: 0.5
};
}
}
interface Settings extends RuleSettings {
baseCurrency: string;
threshold: number;
}

View File

@ -1,5 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';
@ -11,7 +11,7 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
private fees: number private fees: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Initial Investment' name: 'Investment'
}); });
} }

View File

@ -1,4 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@Module({ @Module({

View File

@ -1,9 +1,8 @@
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { bool, cleanEnv, host, json, num, port, str } from 'envalid'; import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
import { Environment } from './interfaces/environment.interface';
@Injectable() @Injectable()
export class ConfigurationService { export class ConfigurationService {
private readonly environmentConfiguration: Environment; private readonly environmentConfiguration: Environment;
@ -17,7 +16,8 @@ export class ConfigurationService {
default: 'USD' default: 'USD'
}), }),
CACHE_TTL: num({ default: 1 }), CACHE_TTL: num({ default: 1 }),
DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }),
DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }),
DATA_SOURCES: json({ DATA_SOURCES: json({
default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO] default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO]
}), }),

View File

@ -5,8 +5,8 @@ import {
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule'; import { Cron, CronExpression } from '@nestjs/schedule';
import { DataGatheringService } from './data-gathering.service'; import { DataGatheringService } from './data-gathering/data-gathering.service';
import { ExchangeRateDataService } from './exchange-rate-data.service'; import { ExchangeRateDataService } from './exchange-rate-data/exchange-rate-data.service';
import { TwitterBotService } from './twitter-bot/twitter-bot.service'; import { TwitterBotService } from './twitter-bot/twitter-bot.service';
@Injectable() @Injectable()
@ -19,8 +19,8 @@ export class CronService {
private readonly twitterBotService: TwitterBotService private readonly twitterBotService: TwitterBotService
) {} ) {}
@Cron(CronExpression.EVERY_4_HOURS) @Cron(CronExpression.EVERY_HOUR)
public async runEveryFourHours() { public async runEveryHour() {
await this.dataGatheringService.gather7Days(); await this.dataGatheringService.gather7Days();
} }
@ -38,15 +38,20 @@ export class CronService {
public async runEverySundayAtTwelvePm() { public async runEverySundayAtTwelvePm() {
const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
for (const { dataSource, symbol } of uniqueAssets) { await this.dataGatheringService.addJobsToQueue(
await this.dataGatheringService.addJobToQueue( uniqueAssets.map(({ dataSource, symbol }) => {
GATHER_ASSET_PROFILE_PROCESS, return {
{ data: {
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS name: GATHER_ASSET_PROFILE_PROCESS,
); opts: {
} ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
}
};
})
);
} }
} }

View File

@ -1,17 +1,17 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
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 { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config'; import { DATA_GATHERING_QUEUE } 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 ms from 'ms'; import ms from 'ms';
import { DataGatheringProcessor } from './data-gathering.processor'; import { DataGatheringProcessor } from './data-gathering.processor';
import { ExchangeRateDataModule } from './exchange-rate-data.module';
import { MarketDataModule } from './market-data.module';
import { SymbolProfileModule } from './symbol-profile.module';
@Module({ @Module({
imports: [ imports: [

View File

@ -1,12 +1,16 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
DATA_GATHERING_QUEUE, DATA_GATHERING_QUEUE,
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS,
GATHER_HISTORICAL_MARKET_DATA_PROCESS GATHER_HISTORICAL_MARKET_DATA_PROCESS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Job } from 'bull'; import { Job } from 'bull';
import { import {
format, format,
@ -18,9 +22,6 @@ import {
} from 'date-fns'; } from 'date-fns';
import { DataGatheringService } from './data-gathering.service'; import { DataGatheringService } from './data-gathering.service';
import { DataProviderService } from './data-provider/data-provider.service';
import { IDataGatheringItem } from './interfaces/interfaces';
import { PrismaService } from './prisma.service';
@Injectable() @Injectable()
@Processor(DATA_GATHERING_QUEUE) @Processor(DATA_GATHERING_QUEUE)
@ -28,10 +29,10 @@ export class DataGatheringProcessor {
public constructor( public constructor(
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly prismaService: PrismaService private readonly marketDataService: MarketDataService
) {} ) {}
@Process(GATHER_ASSET_PROFILE_PROCESS) @Process({ concurrency: 1, name: GATHER_ASSET_PROFILE_PROCESS })
public async gatherAssetProfile(job: Job<UniqueAsset>) { public async gatherAssetProfile(job: Job<UniqueAsset>) {
try { try {
await this.dataGatheringService.gatherAssetProfiles([job.data]); await this.dataGatheringService.gatherAssetProfiles([job.data]);
@ -45,18 +46,27 @@ export class DataGatheringProcessor {
} }
} }
@Process(GATHER_HISTORICAL_MARKET_DATA_PROCESS) @Process({ concurrency: 1, name: GATHER_HISTORICAL_MARKET_DATA_PROCESS })
public async gatherHistoricalMarketData(job: Job<IDataGatheringItem>) { public async gatherHistoricalMarketData(job: Job<IDataGatheringItem>) {
try { try {
const { dataSource, date, symbol } = job.data; const { dataSource, date, symbol } = job.data;
let currentDate = parseISO(<string>(<unknown>date));
Logger.log(
`Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format(
currentDate,
DATE_FORMAT
)}`,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS})`
);
const historicalData = await this.dataProviderService.getHistoricalRaw( const historicalData = await this.dataProviderService.getHistoricalRaw(
[{ dataSource, symbol }], [{ dataSource, symbol }],
parseISO(<string>(<unknown>date)), currentDate,
new Date() new Date()
); );
let currentDate = parseISO(<string>(<unknown>date)); const data: Prisma.MarketDataUpdateInput[] = [];
let lastMarketPrice: number; let lastMarketPrice: number;
while ( while (
@ -82,23 +92,13 @@ export class DataGatheringProcessor {
} }
if (lastMarketPrice) { if (lastMarketPrice) {
try { data.push({
await this.prismaService.marketData.create({ dataSource,
data: { symbol,
dataSource, date: getStartOfUtcDate(currentDate),
symbol, marketPrice: lastMarketPrice,
date: new Date( state: 'CLOSE'
Date.UTC( });
getYear(currentDate),
getMonth(currentDate),
getDate(currentDate),
0
)
),
marketPrice: lastMarketPrice
}
});
} catch {}
} }
// Count month one up for iteration // Count month one up for iteration
@ -112,8 +112,13 @@ export class DataGatheringProcessor {
); );
} }
await this.marketDataService.updateMany({ data });
Logger.log( Logger.log(
`Historical market data gathering has been completed for ${symbol} (${dataSource}).`, `Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format(
currentDate,
DATE_FORMAT
)}`,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS})` `DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS})`
); );
} catch (error) { } catch (error) {

View File

@ -1,9 +1,14 @@
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
DATA_GATHERING_QUEUE, DATA_GATHERING_QUEUE,
GATHER_HISTORICAL_MARKET_DATA_PROCESS, GATHER_HISTORICAL_MARKET_DATA_PROCESS,
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS, GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
QUEUE_JOB_STATUS_LIST
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
@ -11,14 +16,8 @@ import { InjectQueue } from '@nestjs/bull';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { JobOptions, Queue } from 'bull'; import { JobOptions, Queue } from 'bull';
import { format, subDays } from 'date-fns'; import { format, min, subDays, subYears } from 'date-fns';
import { isEmpty } from 'lodash';
import { DataProviderService } from './data-provider/data-provider.service';
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from './exchange-rate-data.service';
import { IDataGatheringItem } from './interfaces/interfaces';
import { MarketDataService } from './market-data.service';
import { PrismaService } from './prisma.service';
@Injectable() @Injectable()
export class DataGatheringService { export class DataGatheringService {
@ -34,17 +33,22 @@ export class DataGatheringService {
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public async addJobToQueue(name: string, data: any, options?: JobOptions) { public async addJobToQueue({
const hasJob = await this.hasJob(name, data); data,
name,
opts
}: {
data: any;
name: string;
opts?: JobOptions;
}) {
return this.dataGatheringQueue.add(name, data, opts);
}
if (hasJob) { public async addJobsToQueue(
Logger.log( jobs: { data: any; name: string; opts?: JobOptions }[]
`Job ${name} with data ${JSON.stringify(data)} already exists.`, ) {
'DataGatheringService' return this.dataGatheringQueue.addBulk(jobs);
);
} else {
return this.dataGatheringQueue.add(name, data, options);
}
} }
public async gather7Days() { public async gather7Days() {
@ -97,7 +101,7 @@ export class DataGatheringService {
symbol symbol
}, },
update: { marketPrice }, update: { marketPrice },
where: { date_symbol: { date, symbol } } where: { dataSource_date_symbol: { dataSource, date, symbol } }
}); });
} }
} catch (error) { } catch (error) {
@ -119,12 +123,9 @@ export class DataGatheringService {
const assetProfiles = await this.dataProviderService.getAssetProfiles( const assetProfiles = await this.dataProviderService.getAssetProfiles(
uniqueAssets uniqueAssets
); );
const symbolProfiles = const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
await this.symbolProfileService.getSymbolProfilesBySymbols( uniqueAssets
uniqueAssets.map(({ symbol }) => { );
return symbol;
})
);
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
const symbolMapping = symbolProfiles.find((symbolProfile) => { const symbolMapping = symbolProfiles.find((symbolProfile) => {
@ -152,10 +153,11 @@ export class DataGatheringService {
countries, countries,
currency, currency,
dataSource, dataSource,
isin,
name, name,
sectors, sectors,
url url
} = assetProfiles[symbol]; } = assetProfile;
try { try {
await this.prismaService.symbolProfile.upsert({ await this.prismaService.symbolProfile.upsert({
@ -165,6 +167,7 @@ export class DataGatheringService {
countries, countries,
currency, currency,
dataSource, dataSource,
isin,
name, name,
sectors, sectors,
symbol, symbol,
@ -175,6 +178,7 @@ export class DataGatheringService {
assetSubClass, assetSubClass,
countries, countries,
currency, currency,
isin,
name, name,
sectors, sectors,
url url
@ -206,59 +210,22 @@ export class DataGatheringService {
} }
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) { public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) { await this.addJobsToQueue(
await this.addJobToQueue( aSymbolsWithStartDate.map(({ dataSource, date, symbol }) => {
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
{
dataSource,
date,
symbol
},
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
);
}
}
public async getSymbolsMax(): Promise<IDataGatheringItem[]> {
const startDate =
(
await this.prismaService.order.findFirst({
orderBy: [{ date: 'asc' }]
})
)?.date ?? new Date();
const currencyPairsToGather = this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
return { return {
dataSource, data: {
symbol, dataSource,
date: startDate date,
}; symbol
});
const symbolProfilesToGather = (
await this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }],
select: {
dataSource: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
}, },
scraperConfiguration: true, name: GATHER_HISTORICAL_MARKET_DATA_PROCESS,
symbol: true opts: {
} ...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}-${format(date, DATE_FORMAT)}`
}
};
}) })
).map((symbolProfile) => { );
return {
...symbolProfile,
date: symbolProfile.Order?.[0]?.date ?? startDate
};
});
return [...currencyPairsToGather, ...symbolProfilesToGather];
} }
public async getUniqueAssets(): Promise<UniqueAsset[]> { public async getUniqueAssets(): Promise<UniqueAsset[]> {
@ -295,13 +262,14 @@ export class DataGatheringService {
// Only consider symbols with incomplete market data for the last // Only consider symbols with incomplete market data for the last
// 7 days // 7 days
const symbolsNotToGather = ( const symbolsWithCompleteMarketData = (
await this.prismaService.marketData.groupBy({ await this.prismaService.marketData.groupBy({
_count: true, _count: true,
by: ['symbol'], by: ['symbol'],
orderBy: [{ symbol: 'asc' }], orderBy: [{ symbol: 'asc' }],
where: { where: {
date: { gt: startDate } date: { gt: startDate },
state: 'CLOSE'
} }
}) })
) )
@ -313,8 +281,14 @@ export class DataGatheringService {
}); });
const symbolProfilesToGather = symbolProfiles const symbolProfilesToGather = symbolProfiles
.filter(({ symbol }) => { .filter(({ dataSource, scraperConfiguration, symbol }) => {
return !symbolsNotToGather.includes(symbol); const manualDataSourceWithScraperConfiguration =
dataSource === 'MANUAL' && !isEmpty(scraperConfiguration);
return (
!symbolsWithCompleteMarketData.includes(symbol) &&
(dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration)
);
}) })
.map((symbolProfile) => { .map((symbolProfile) => {
return { return {
@ -326,7 +300,7 @@ export class DataGatheringService {
const currencyPairsToGather = this.exchangeRateDataService const currencyPairsToGather = this.exchangeRateDataService
.getCurrencyPairs() .getCurrencyPairs()
.filter(({ symbol }) => { .filter(({ symbol }) => {
return !symbolsNotToGather.includes(symbol); return !symbolsWithCompleteMarketData.includes(symbol);
}) })
.map(({ dataSource, symbol }) => { .map(({ dataSource, symbol }) => {
return { return {
@ -339,17 +313,56 @@ export class DataGatheringService {
return [...currencyPairsToGather, ...symbolProfilesToGather]; return [...currencyPairsToGather, ...symbolProfilesToGather];
} }
private async hasJob(name: string, data: any) { private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
const jobs = await this.dataGatheringQueue.getJobs( const startDate =
QUEUE_JOB_STATUS_LIST.filter((status) => { (
return status !== 'completed'; await this.prismaService.order.findFirst({
}) orderBy: [{ date: 'asc' }]
); })
)?.date ?? new Date();
return jobs.some((job) => { const currencyPairsToGather = this.exchangeRateDataService
return ( .getCurrencyPairs()
job.name === name && JSON.stringify(job.data) === JSON.stringify(data) .map(({ dataSource, symbol }) => {
); return {
}); dataSource,
symbol,
date: min([startDate, subYears(new Date(), 10)])
};
});
const symbolProfilesToGather = (
await this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }],
select: {
dataSource: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
scraperConfiguration: true,
symbol: true
}
})
)
.filter((symbolProfile) => {
const manualDataSourceWithScraperConfiguration =
symbolProfile.dataSource === 'MANUAL' &&
!isEmpty(symbolProfile.scraperConfiguration);
return (
symbolProfile.dataSource !== 'MANUAL' ||
manualDataSourceWithScraperConfiguration
);
})
.map((symbolProfile) => {
return {
...symbolProfile,
date: symbolProfile.Order?.[0]?.date ?? startDate
};
});
return [...currencyPairsToGather, ...symbolProfilesToGather];
} }
} }

View File

@ -1,5 +1,5 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
@ -7,7 +7,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format, isAfter, isBefore, parse } from 'date-fns'; import { format, isAfter, isBefore, parse } from 'date-fns';
@ -33,7 +33,8 @@ export class AlphaVantageService implements DataProviderInterface {
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
return { return {
dataSource: this.getName() dataSource: this.getName(),
symbol: aSymbol
}; };
} }
@ -109,6 +110,10 @@ export class AlphaVantageService implements DataProviderInterface {
return {}; return {};
} }
public getTestSymbol() {
return undefined;
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
const result = await this.alphaVantage.data.search(aQuery); const result = await this.alphaVantage.data.search(aQuery);

View File

@ -1,5 +1,5 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
@ -160,13 +160,13 @@ export class CoinGeckoService implements DataProviderInterface {
return results; return results;
} }
public getTestSymbol() {
return 'bitcoin';
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
let items: LookupItem[] = []; let items: LookupItem[] = [];
if (aQuery.length <= 2) {
return { items };
}
try { try {
const get = bent( const get = bent(
`${this.URL}/search?query=${aQuery}`, `${this.URL}/search?query=${aQuery}`,
@ -180,6 +180,8 @@ export class CoinGeckoService implements DataProviderInterface {
return { return {
name, name,
symbol, symbol,
assetClass: AssetClass.CASH,
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
currency: this.baseCurrency, currency: this.baseCurrency,
dataSource: this.getName() dataSource: this.getName()
}; };

View File

@ -1,15 +1,27 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service'; import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service';
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@Module({ @Module({
exports: ['DataEnhancers', TrackinsightDataEnhancerService], exports: [
'DataEnhancers',
TrackinsightDataEnhancerService,
YahooFinanceDataEnhancerService
],
imports: [ConfigurationModule, CryptocurrencyModule],
providers: [ providers: [
TrackinsightDataEnhancerService,
YahooFinanceDataEnhancerService,
{ {
inject: [TrackinsightDataEnhancerService], inject: [
TrackinsightDataEnhancerService,
YahooFinanceDataEnhancerService
],
provide: 'DataEnhancers', provide: 'DataEnhancers',
useFactory: (trackinsight) => [trackinsight] useFactory: (trackinsight, yahooFinance) => [trackinsight, yahooFinance]
}, }
TrackinsightDataEnhancerService
] ]
}) })
export class DataEnhancerModule {} export class DataEnhancerModule {}

View File

@ -1,13 +1,15 @@
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import bent from 'bent'; import bent from 'bent';
const getJSON = bent('json'); const getJSON = bent('json');
@Injectable()
export class TrackinsightDataEnhancerService implements DataEnhancerInterface { export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private static baseUrl = 'https://data.trackinsight.com/holdings'; private static baseUrl = 'https://data.trackinsight.com';
private static countries = require('countries-list/dist/countries.json'); private static countries = require('countries-list/dist/countries.json');
private static countriesMapping = { private static countriesMapping = {
'Russian Federation': 'Russia' 'Russian Federation': 'Russia'
@ -32,17 +34,29 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response; return response;
} }
const result = await getJSON( const profile = await getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json` `${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
).catch(() => {
return {};
});
const isin = profile.isin?.split(';')?.[0];
if (isin) {
response.isin = isin;
}
const holdings = await getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
).catch(() => { ).catch(() => {
return getJSON( return getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/${ `${TrackinsightDataEnhancerService.baseUrl}/holdings/${
symbol.split('.')[0] symbol.split('.')?.[0]
}.json` }.json`
); );
}); });
if (result.weight < 0.95) { if (holdings?.weight < 0.95) {
// Skip if data is inaccurate // Skip if data is inaccurate
return response; return response;
} }
@ -52,7 +66,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
(response.countries as unknown as Country[]).length === 0 (response.countries as unknown as Country[]).length === 0
) { ) {
response.countries = []; response.countries = [];
for (const [name, value] of Object.entries<any>(result.countries)) { for (const [name, value] of Object.entries<any>(
holdings?.countries ?? {}
)) {
let countryCode: string; let countryCode: string;
for (const [key, country] of Object.entries<any>( for (const [key, country] of Object.entries<any>(
@ -80,7 +96,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
(response.sectors as unknown as Sector[]).length === 0 (response.sectors as unknown as Sector[]).length === 0
) { ) {
response.sectors = []; response.sectors = [];
for (const [name, value] of Object.entries<any>(result.sectors)) { for (const [name, value] of Object.entries<any>(
holdings?.sectors ?? {}
)) {
response.sectors.push({ response.sectors.push({
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name, name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
weight: value.weight weight: value.weight

View File

@ -1,7 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { YahooFinanceService } from './yahoo-finance.service'; import { YahooFinanceDataEnhancerService } from './yahoo-finance.service';
jest.mock( jest.mock(
'@ghostfolio/api/services/cryptocurrency/cryptocurrency.service', '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service',
@ -25,16 +25,16 @@ jest.mock(
} }
); );
describe('YahooFinanceService', () => { describe('YahooFinanceDataEnhancerService', () => {
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let cryptocurrencyService: CryptocurrencyService; let cryptocurrencyService: CryptocurrencyService;
let yahooFinanceService: YahooFinanceService; let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService;
beforeAll(async () => { beforeAll(async () => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
cryptocurrencyService = new CryptocurrencyService(); cryptocurrencyService = new CryptocurrencyService();
yahooFinanceService = new YahooFinanceService( yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService(
configurationService, configurationService,
cryptocurrencyService cryptocurrencyService
); );
@ -42,25 +42,37 @@ describe('YahooFinanceService', () => {
it('convertFromYahooFinanceSymbol', async () => { it('convertFromYahooFinanceSymbol', async () => {
expect( expect(
await yahooFinanceService.convertFromYahooFinanceSymbol('BRK-B') await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
'BRK-B'
)
).toEqual('BRK-B'); ).toEqual('BRK-B');
expect( expect(
await yahooFinanceService.convertFromYahooFinanceSymbol('BTC-USD') await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
'BTC-USD'
)
).toEqual('BTCUSD'); ).toEqual('BTCUSD');
expect( expect(
await yahooFinanceService.convertFromYahooFinanceSymbol('EURUSD=X') await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
'EURUSD=X'
)
).toEqual('EURUSD'); ).toEqual('EURUSD');
}); });
it('convertToYahooFinanceSymbol', async () => { it('convertToYahooFinanceSymbol', async () => {
expect( expect(
await yahooFinanceService.convertToYahooFinanceSymbol('BTCUSD') await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
'BTCUSD'
)
).toEqual('BTC-USD'); ).toEqual('BTC-USD');
expect( expect(
await yahooFinanceService.convertToYahooFinanceSymbol('DOGEUSD') await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
'DOGEUSD'
)
).toEqual('DOGE-USD'); ).toEqual('DOGE-USD');
expect( expect(
await yahooFinanceService.convertToYahooFinanceSymbol('USDCHF') await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
'USDCHF'
)
).toEqual('USDCHF=X'); ).toEqual('USDCHF=X');
}); });
}); });

View File

@ -0,0 +1,325 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common';
import {
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import { countries } from 'countries-list';
import yahooFinance from 'yahoo-finance2';
import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface';
@Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
private baseCurrency: string;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService
) {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
}
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
let symbol = aYahooFinanceSymbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
);
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
symbol = `${this.baseCurrency}${symbol}`;
}
return symbol.replace('=X', '');
}
/**
* Converts a symbol to a Yahoo Finance symbol
*
* Currency: USDCHF -> USDCHF=X
* Cryptocurrency: BTCUSD -> BTC-USD
* DOGEUSD -> DOGE-USD
*/
public convertToYahooFinanceSymbol(aSymbol: string) {
if (
aSymbol.includes(this.baseCurrency) &&
aSymbol.length > this.baseCurrency.length
) {
if (
isCurrency(
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
)
) {
return `${aSymbol}=X`;
} else if (
this.cryptocurrencyService.isCryptocurrency(
aSymbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
)
)
) {
// Add a dash before the last three characters
// BTCUSD -> BTC-USD
// DOGEUSD -> DOGE-USD
// SOL1USD -> SOL1-USD
return aSymbol.replace(
new RegExp(`-?${this.baseCurrency}$`),
`-${this.baseCurrency}`
);
}
}
return aSymbol;
}
public async enhance({
response,
symbol
}: {
response: Partial<SymbolProfile>;
symbol: string;
}): Promise<Partial<SymbolProfile>> {
if (response.dataSource !== 'YAHOO' && !response.isin) {
return response;
}
try {
let yahooSymbol: string;
if (response.dataSource === 'YAHOO') {
yahooSymbol = symbol;
} else {
const { quotes } = await yahooFinance.search(response.isin);
yahooSymbol = quotes[0].symbol;
}
const { countries, sectors, url } = await this.getAssetProfile(
yahooSymbol
);
if (countries) {
response.countries = countries;
}
if (sectors) {
response.sectors = sectors;
}
if (url) {
response.url = url;
}
} catch (error) {
Logger.error(error, 'YahooFinanceDataEnhancerService');
}
return response;
}
public formatName({
longName,
quoteType,
shortName,
symbol
}: {
longName: Price['longName'];
quoteType: Price['quoteType'];
shortName: Price['shortName'];
symbol: Price['symbol'];
}) {
let name = longName;
if (name) {
name = name.replace('Amundi Index Solutions - ', '');
name = name.replace('iShares ETF (CH) - ', '');
name = name.replace('iShares III Public Limited Company - ', '');
name = name.replace('iShares V PLC - ', '');
name = name.replace('iShares VI Public Limited Company - ', '');
name = name.replace('iShares VII PLC - ', '');
name = name.replace('Multi Units Luxembourg - ', '');
name = name.replace('VanEck ETFs N.V. - ', '');
name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', '');
name = name.replace('Vanguard Funds Public Limited Company - ', '');
name = name.replace('Vanguard Index Funds - ', '');
name = name.replace('Xtrackers (IE) Plc - ', '');
}
if (quoteType === 'FUTURE') {
// "Gold Jun 22" -> "Gold"
name = shortName?.slice(0, -7);
}
return name || shortName || symbol;
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
const response: Partial<SymbolProfile> = {};
try {
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
const assetProfile = await yahooFinance.quoteSummary(symbol, {
modules: ['price', 'summaryProfile', 'topHoldings']
});
const { assetClass, assetSubClass } = this.parseAssetClass({
quoteType: assetProfile.price.quoteType,
shortName: assetProfile.price.shortName
});
response.assetClass = assetClass;
response.assetSubClass = assetSubClass;
response.currency = assetProfile.price.currency;
response.dataSource = this.getName();
response.name = this.formatName({
longName: assetProfile.price.longName,
quoteType: assetProfile.price.quoteType,
shortName: assetProfile.price.shortName,
symbol: assetProfile.price.symbol
});
response.symbol = aSymbol;
if (assetSubClass === AssetSubClass.MUTUALFUND) {
response.sectors = [];
for (const sectorWeighting of assetProfile.topHoldings
?.sectorWeightings ?? []) {
for (const [sector, weight] of Object.entries(sectorWeighting)) {
response.sectors.push({ weight, name: this.parseSector(sector) });
}
}
} else if (
assetSubClass === AssetSubClass.STOCK &&
assetProfile.summaryProfile?.country
) {
// Add country if asset is stock and country available
try {
const [code] = Object.entries(countries).find(([, country]) => {
return country.name === assetProfile.summaryProfile?.country;
});
if (code) {
response.countries = [{ code, weight: 1 }];
}
} catch {}
if (assetProfile.summaryProfile?.sector) {
response.sectors = [
{ name: assetProfile.summaryProfile?.sector, weight: 1 }
];
}
}
const url = assetProfile.summaryProfile?.website;
if (url) {
response.url = url;
}
} catch (error) {
Logger.error(error, 'YahooFinanceService');
}
return response;
}
public getName() {
return DataSource.YAHOO;
}
public parseAssetClass({
quoteType,
shortName
}: {
quoteType: string;
shortName: string;
}): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
switch (quoteType?.toLowerCase()) {
case 'cryptocurrency':
assetClass = AssetClass.CASH;
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
break;
case 'equity':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
break;
case 'etf':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
break;
case 'future':
assetClass = AssetClass.COMMODITY;
assetSubClass = AssetSubClass.COMMODITY;
if (
shortName?.toLowerCase()?.startsWith('gold') ||
shortName?.toLowerCase()?.startsWith('palladium') ||
shortName?.toLowerCase()?.startsWith('platinum') ||
shortName?.toLowerCase()?.startsWith('silver')
) {
assetSubClass = AssetSubClass.PRECIOUS_METAL;
}
break;
case 'mutualfund':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.MUTUALFUND;
break;
}
return { assetClass, assetSubClass };
}
private parseSector(aString: string): string {
let sector = UNKNOWN_KEY;
switch (aString) {
case 'basic_materials':
sector = 'Basic Materials';
break;
case 'communication_services':
sector = 'Communication Services';
break;
case 'consumer_cyclical':
sector = 'Consumer Cyclical';
break;
case 'consumer_defensive':
sector = 'Consumer Staples';
break;
case 'energy':
sector = 'Energy';
break;
case 'financial_services':
sector = 'Financial Services';
break;
case 'healthcare':
sector = 'Healthcare';
break;
case 'industrials':
sector = 'Industrials';
break;
case 'realestate':
sector = 'Real Estate';
break;
case 'technology':
sector = 'Technology';
break;
case 'utilities':
sector = 'Utilities';
break;
}
return sector;
}
}

View File

@ -1,4 +1,4 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service'; import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service';
@ -7,17 +7,24 @@ import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/goog
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DataEnhancerModule } from './data-enhancer/data-enhancer.module';
import { YahooFinanceDataEnhancerService } from './data-enhancer/yahoo-finance/yahoo-finance.service';
import { DataProviderService } from './data-provider.service'; import { DataProviderService } from './data-provider.service';
@Module({ @Module({
imports: [ imports: [
ConfigurationModule, ConfigurationModule,
CryptocurrencyModule, CryptocurrencyModule,
DataEnhancerModule,
MarketDataModule,
PrismaModule, PrismaModule,
PropertyModule,
SymbolProfileModule SymbolProfileModule
], ],
providers: [ providers: [
@ -57,7 +64,8 @@ import { DataProviderService } from './data-provider.service';
rapidApiService, rapidApiService,
yahooFinanceService yahooFinanceService
] ]
} },
YahooFinanceDataEnhancerService
], ],
exports: [DataProviderService, YahooFinanceService] exports: [DataProviderService, YahooFinanceService]
}) })

View File

@ -1,27 +1,108 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataGatheringItem, IDataGatheringItem,
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_DATA_SOURCE_MAPPING } from '@ghostfolio/common/config';
import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
import { format, isValid } from 'date-fns'; import { format, isValid } from 'date-fns';
import { groupBy, isEmpty } from 'lodash'; import { groupBy, isEmpty, isNumber } from 'lodash';
@Injectable() @Injectable()
export class DataProviderService { export class DataProviderService {
private dataProviderMapping: { [dataProviderName: string]: string };
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
@Inject('DataProviderInterfaces') @Inject('DataProviderInterfaces')
private readonly dataProviderInterfaces: DataProviderInterface[], private readonly dataProviderInterfaces: DataProviderInterface[],
private readonly prismaService: PrismaService private readonly marketDataService: MarketDataService,
) {} private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService
) {
this.initialize();
}
public async initialize() {
this.dataProviderMapping =
((await this.propertyService.getByKey(PROPERTY_DATA_SOURCE_MAPPING)) as {
[dataProviderName: string]: string;
}) ?? {};
}
public async checkQuote(dataSource: DataSource) {
const dataProvider = this.getDataProvider(dataSource);
const symbol = dataProvider.getTestSymbol();
const quotes = await this.getQuotes([
{
dataSource,
symbol
}
]);
if (quotes[symbol]?.marketPrice > 0) {
return true;
}
return false;
}
public async getAssetProfiles(items: IDataGatheringItem[]): Promise<{
[symbol: string]: Partial<SymbolProfile>;
}> {
const response: {
[symbol: string]: Partial<SymbolProfile>;
} = {};
const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
const promises = [];
for (const [dataSource, dataGatheringItems] of Object.entries(
itemsGroupedByDataSource
)) {
const symbols = dataGatheringItems.map((dataGatheringItem) => {
return dataGatheringItem.symbol;
});
for (const symbol of symbols) {
const promise = Promise.resolve(
this.getDataProvider(DataSource[dataSource]).getAssetProfile(symbol)
);
promises.push(
promise.then((symbolProfile) => {
response[symbol] = symbolProfile;
})
);
}
}
await Promise.all(promises);
return response;
}
public getDataSourceForExchangeRates(): DataSource {
return DataSource[
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
];
}
public getDataSourceForImport(): DataSource {
return DataSource[this.configurationService.get('DATA_SOURCE_IMPORT')];
}
public async getDividends({ public async getDividends({
dataSource, dataSource,
@ -147,46 +228,6 @@ export class DataProviderService {
return result; return result;
} }
public getPrimaryDataSource(): DataSource {
return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')];
}
public async getAssetProfiles(items: IDataGatheringItem[]): Promise<{
[symbol: string]: Partial<SymbolProfile>;
}> {
const response: {
[symbol: string]: Partial<SymbolProfile>;
} = {};
const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
const promises = [];
for (const [dataSource, dataGatheringItems] of Object.entries(
itemsGroupedByDataSource
)) {
const symbols = dataGatheringItems.map((dataGatheringItem) => {
return dataGatheringItem.symbol;
});
for (const symbol of symbols) {
const promise = Promise.resolve(
this.getDataProvider(DataSource[dataSource]).getAssetProfile(symbol)
);
promises.push(
promise.then((symbolProfile) => {
response[symbol] = symbolProfile;
})
);
}
}
await Promise.all(promises);
return response;
}
public async getQuotes(items: IDataGatheringItem[]): Promise<{ public async getQuotes(items: IDataGatheringItem[]): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: IDataProviderResponse;
}> { }> {
@ -226,7 +267,7 @@ export class DataProviderService {
const promise = Promise.resolve(dataProvider.getQuotes(symbolsChunk)); const promise = Promise.resolve(dataProvider.getQuotes(symbolsChunk));
promises.push( promises.push(
promise.then((result) => { promise.then(async (result) => {
for (const [symbol, dataProviderResponse] of Object.entries( for (const [symbol, dataProviderResponse] of Object.entries(
result result
)) { )) {
@ -241,6 +282,27 @@ export class DataProviderService {
1000 1000
).toFixed(3)} seconds` ).toFixed(3)} seconds`
); );
try {
await this.marketDataService.updateMany({
data: Object.keys(response)
.filter((symbol) => {
return (
isNumber(response[symbol].marketPrice) &&
response[symbol].marketPrice > 0
);
})
.map((symbol) => {
return {
symbol,
dataSource: response[symbol].dataSource,
date: getStartOfUtcDate(new Date()),
marketPrice: response[symbol].marketPrice,
state: 'INTRADAY'
};
})
});
} catch {}
}) })
); );
} }
@ -260,26 +322,51 @@ export class DataProviderService {
return response; return response;
} }
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search({
query,
user
}: {
query: string;
user: UserWithSettings;
}): Promise<{ items: LookupItem[] }> {
const promises: Promise<{ items: LookupItem[] }>[] = []; const promises: Promise<{ items: LookupItem[] }>[] = [];
let lookupItems: LookupItem[] = []; let lookupItems: LookupItem[] = [];
for (const dataSource of this.configurationService.get('DATA_SOURCES')) { if (query?.length < 2) {
promises.push( return { items: lookupItems };
this.getDataProvider(DataSource[dataSource]).search(aQuery) }
);
let dataSources = this.configurationService.get('DATA_SOURCES');
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
) {
dataSources = dataSources.filter((dataSource) => {
return !this.isPremiumDataSource(DataSource[dataSource]);
});
}
for (const dataSource of dataSources) {
promises.push(this.getDataProvider(DataSource[dataSource]).search(query));
} }
const searchResults = await Promise.all(promises); const searchResults = await Promise.all(promises);
searchResults.forEach((searchResult) => { searchResults.forEach(({ items }) => {
lookupItems = lookupItems.concat(searchResult.items); if (items?.length > 0) {
lookupItems = lookupItems.concat(items);
}
}); });
const filteredItems = lookupItems.filter((lookupItem) => { const filteredItems = lookupItems
// Only allow symbols with supported currency .filter((lookupItem) => {
return lookupItem.currency ? true : false; // Only allow symbols with supported currency
}); return lookupItem.currency ? true : false;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
});
return { return {
items: filteredItems items: filteredItems
@ -288,6 +375,21 @@ export class DataProviderService {
private getDataProvider(providerName: DataSource) { private getDataProvider(providerName: DataSource) {
for (const dataProviderInterface of this.dataProviderInterfaces) { for (const dataProviderInterface of this.dataProviderInterfaces) {
if (this.dataProviderMapping[dataProviderInterface.getName()]) {
const mappedDataProviderInterface = this.dataProviderInterfaces.find(
(currentDataProviderInterface) => {
return (
currentDataProviderInterface.getName() ===
this.dataProviderMapping[dataProviderInterface.getName()]
);
}
);
if (mappedDataProviderInterface) {
return mappedDataProviderInterface;
}
}
if (dataProviderInterface.getName() === providerName) { if (dataProviderInterface.getName() === providerName) {
return dataProviderInterface; return dataProviderInterface;
} }
@ -295,4 +397,9 @@ export class DataProviderService {
throw new Error('No data provider has been found.'); throw new Error('No data provider has been found.');
} }
private isPremiumDataSource(aDataSource: DataSource) {
const premiumDataSources: DataSource[] = [DataSource.EOD_HISTORICAL_DATA];
return premiumDataSources.includes(aDataSource);
}
} }

View File

@ -1,28 +1,34 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import {
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import bent from 'bent'; import bent from 'bent';
import { format } from 'date-fns'; import Big from 'big.js';
import { format, isToday } from 'date-fns';
@Injectable() @Injectable()
export class EodHistoricalDataService implements DataProviderInterface { export class EodHistoricalDataService implements DataProviderInterface {
private apiKey: string; private apiKey: string;
private baseCurrency: string;
private readonly URL = 'https://eodhistoricaldata.com/api'; private readonly URL = 'https://eodhistoricaldata.com/api';
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService
private readonly symbolProfileService: SymbolProfileService
) { ) {
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY'); this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
} }
public canHandle(symbol: string) { public canHandle(symbol: string) {
@ -32,8 +38,16 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getAssetProfile( public async getAssetProfile(
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
const [searchResult] = await this.getSearchResult(aSymbol);
return { return {
dataSource: this.getName() assetClass: searchResult?.assetClass,
assetSubClass: searchResult?.assetSubClass,
currency: this.convertCurrency(searchResult?.currency),
dataSource: this.getName(),
isin: searchResult?.isin,
name: searchResult?.name,
symbol: aSymbol
}; };
} }
@ -59,9 +73,11 @@ export class EodHistoricalDataService implements DataProviderInterface {
): Promise<{ ): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
const symbol = this.convertToEodSymbol(aSymbol);
try { try {
const get = bent( const get = bent(
`${this.URL}/eod/${aSymbol}?api_token=${ `${this.URL}/eod/${symbol}?api_token=${
this.apiKey this.apiKey
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format( }&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
to, to,
@ -76,14 +92,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
return response.reduce( return response.reduce(
(result, historicalItem, index, array) => { (result, historicalItem, index, array) => {
result[aSymbol][historicalItem.date] = { result[this.convertFromEodSymbol(symbol)][historicalItem.date] = {
marketPrice: historicalItem.close, marketPrice: this.getConvertedValue({
symbol: aSymbol,
value: historicalItem.close
}),
performance: historicalItem.open - historicalItem.close performance: historicalItem.open - historicalItem.close
}; };
return result; return result;
}, },
{ [aSymbol]: {} } { [this.convertFromEodSymbol(symbol)]: {} }
); );
} catch (error) { } catch (error) {
throw new Error( throw new Error(
@ -108,46 +127,87 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getQuotes( public async getQuotes(
aSymbols: string[] aSymbols: string[]
): Promise<{ [symbol: string]: IDataProviderResponse }> { ): Promise<{ [symbol: string]: IDataProviderResponse }> {
if (aSymbols.length <= 0) { const symbols = aSymbols.map((symbol) => {
return this.convertToEodSymbol(symbol);
});
if (symbols.length <= 0) {
return {}; return {};
} }
try { try {
const get = bent( const get = bent(
`${this.URL}/real-time/${aSymbols[0]}?api_token=${ `${this.URL}/real-time/${symbols[0]}?api_token=${
this.apiKey this.apiKey
}&fmt=json&s=${aSymbols.join(',')}`, }&fmt=json&s=${symbols.join(',')}`,
'GET', 'GET',
'json', 'json',
200 200
); );
const [response, symbolProfiles] = await Promise.all([ const realTimeResponse = await get();
get(),
this.symbolProfileService.getSymbolProfiles( const quotes =
aSymbols.map((symbol) => { symbols.length === 1 ? [realTimeResponse] : realTimeResponse;
return {
symbol, const searchResponse = await Promise.all(
dataSource: DataSource.EOD_HISTORICAL_DATA symbols
}; .filter((symbol) => {
return !symbol.endsWith('.FOREX');
}) })
) .map((symbol) => {
]); return this.search(symbol);
})
);
const quotes = aSymbols.length === 1 ? [response] : response; const lookupItems = searchResponse.flat().map(({ items }) => {
return items[0];
});
return quotes.reduce((result, item, index, array) => { const response = quotes.reduce(
result[item.code] = { (
currency: symbolProfiles.find((symbolProfile) => { result: { [symbol: string]: IDataProviderResponse },
return symbolProfile.symbol === item.code; { close, code, timestamp }
})?.currency, ) => {
dataSource: DataSource.EOD_HISTORICAL_DATA, const currency = lookupItems.find((lookupItem) => {
marketPrice: item.close, return lookupItem.symbol === code;
marketState: 'delayed' })?.currency;
result[this.convertFromEodSymbol(code)] = {
currency: currency ?? this.baseCurrency,
dataSource: DataSource.EOD_HISTORICAL_DATA,
marketPrice: close,
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
};
return result;
},
{}
);
if (response[`${this.baseCurrency}GBP`]) {
response[`${this.baseCurrency}GBp`] = {
...response[`${this.baseCurrency}GBP`],
currency: `${this.baseCurrency}GBp`,
marketPrice: this.getConvertedValue({
symbol: `${this.baseCurrency}GBp`,
value: response[`${this.baseCurrency}GBP`].marketPrice
})
}; };
}
return result; if (response[`${this.baseCurrency}ILS`]) {
}, {}); response[`${this.baseCurrency}ILA`] = {
...response[`${this.baseCurrency}ILS`],
currency: `${this.baseCurrency}ILA`,
marketPrice: this.getConvertedValue({
symbol: `${this.baseCurrency}ILA`,
value: response[`${this.baseCurrency}ILS`].marketPrice
})
};
}
return response;
} catch (error) { } catch (error) {
Logger.error(error, 'EodHistoricalDataService'); Logger.error(error, 'EodHistoricalDataService');
} }
@ -155,7 +215,179 @@ export class EodHistoricalDataService implements DataProviderInterface {
return {}; return {};
} }
public getTestSymbol() {
return 'AAPL.US';
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
return { items: [] }; const searchResult = await this.getSearchResult(aQuery);
return {
items: searchResult
.filter(({ symbol }) => {
return !symbol.endsWith('.FOREX');
})
.map(
({
assetClass,
assetSubClass,
currency,
dataSource,
name,
symbol
}) => {
return {
assetClass,
assetSubClass,
dataSource,
name,
symbol,
currency: this.convertCurrency(currency)
};
}
)
};
}
private convertCurrency(aCurrency: string) {
let currency = aCurrency;
if (currency === 'GBX') {
currency = 'GBp';
}
return currency;
}
private convertFromEodSymbol(aEodSymbol: string) {
let symbol = aEodSymbol;
if (symbol.endsWith('.FOREX')) {
symbol = symbol.replace('GBX', 'GBp');
symbol = symbol.replace('.FOREX', '');
symbol = `${this.baseCurrency}${symbol}`;
}
return symbol;
}
/**
* Converts a symbol to a EOD symbol
*
* Currency: USDCHF -> CHF.FOREX
*/
private convertToEodSymbol(aSymbol: string) {
if (
aSymbol.startsWith(this.baseCurrency) &&
aSymbol.length > this.baseCurrency.length
) {
if (
isCurrency(
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
)
) {
return `${aSymbol
.replace('GBp', 'GBX')
.replace(this.baseCurrency, '')}.FOREX`;
}
}
return aSymbol;
}
private getConvertedValue({
symbol,
value
}: {
symbol: string;
value: number;
}) {
if (symbol === `${this.baseCurrency}GBp`) {
// Convert GPB to GBp (pence)
return new Big(value).mul(100).toNumber();
} else if (symbol === `${this.baseCurrency}ILA`) {
// Convert ILS to ILA
return new Big(value).mul(100).toNumber();
}
return value;
}
private async getSearchResult(aQuery: string): Promise<
(LookupItem & {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
isin: string;
})[]
> {
let searchResult = [];
try {
const get = bent(
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
'GET',
'json',
200
);
const response = await get();
searchResult = response.map(
({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {
const { assetClass, assetSubClass } = this.parseAssetClass({
Exchange,
Type
});
return {
assetClass,
assetSubClass,
isin,
name,
currency: this.convertCurrency(Currency),
dataSource: this.getName(),
symbol: `${Code}.${Exchange}`
};
}
);
} catch (error) {
Logger.error(error, 'EodHistoricalDataService');
}
return searchResult;
}
private parseAssetClass({
Exchange,
Type
}: {
Exchange: string;
Type: string;
}): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
switch (Type?.toLowerCase()) {
case 'common stock':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
break;
case 'currency':
assetClass = AssetClass.CASH;
if (Exchange?.toLowerCase() === 'cc') {
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
}
break;
case 'etf':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
break;
}
return { assetClass, assetSubClass };
} }
} }

View File

@ -1,12 +1,12 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -30,7 +30,8 @@ export class GoogleSheetsService implements DataProviderInterface {
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
return { return {
dataSource: this.getName() dataSource: this.getName(),
symbol: aSymbol
}; };
} }
@ -108,8 +109,14 @@ export class GoogleSheetsService implements DataProviderInterface {
try { try {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};
const symbolProfiles = const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
await this.symbolProfileService.getSymbolProfilesBySymbols(aSymbols); aSymbols.map((symbol) => {
return {
symbol,
dataSource: this.getName()
};
})
);
const sheet = await this.getSheet({ const sheet = await this.getSheet({
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'),
@ -142,9 +149,15 @@ export class GoogleSheetsService implements DataProviderInterface {
return {}; return {};
} }
public getTestSymbol() {
return 'INDEXSP:.INX';
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
const items = await this.prismaService.symbolProfile.findMany({ const items = await this.prismaService.symbolProfile.findMany({
select: { select: {
assetClass: true,
assetSubClass: true,
currency: true, currency: true,
dataSource: true, dataSource: true,
name: true, name: true,

View File

@ -40,5 +40,7 @@ export interface DataProviderInterface {
aSymbols: string[] aSymbols: string[]
): Promise<{ [symbol: string]: IDataProviderResponse }>; ): Promise<{ [symbol: string]: IDataProviderResponse }>;
getTestSymbol(): string;
search(aQuery: string): Promise<{ items: LookupItem[] }>; search(aQuery: string): Promise<{ items: LookupItem[] }>;
} }

View File

@ -4,8 +4,8 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
DATE_FORMAT, DATE_FORMAT,
extractNumberFromString, extractNumberFromString,
@ -34,7 +34,8 @@ export class ManualService implements DataProviderInterface {
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
return { return {
dataSource: this.getName() dataSource: this.getName(),
symbol: aSymbol
}; };
} }
@ -63,8 +64,9 @@ export class ManualService implements DataProviderInterface {
try { try {
const symbol = aSymbol; const symbol = aSymbol;
const [symbolProfile] = const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
await this.symbolProfileService.getSymbolProfilesBySymbols([symbol]); [{ symbol, dataSource: this.getName() }]
);
const { defaultMarketPrice, selector, url } = const { defaultMarketPrice, selector, url } =
symbolProfile.scraperConfiguration ?? {}; symbolProfile.scraperConfiguration ?? {};
@ -127,8 +129,11 @@ export class ManualService implements DataProviderInterface {
} }
try { try {
const symbolProfiles = const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
await this.symbolProfileService.getSymbolProfilesBySymbols(aSymbols); aSymbols.map((symbol) => {
return { symbol, dataSource: this.getName() };
})
);
const marketData = await this.prismaService.marketData.findMany({ const marketData = await this.prismaService.marketData.findMany({
distinct: ['symbol'], distinct: ['symbol'],
@ -162,9 +167,15 @@ export class ManualService implements DataProviderInterface {
return {}; return {};
} }
public getTestSymbol() {
return undefined;
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
let items = await this.prismaService.symbolProfile.findMany({ let items = await this.prismaService.symbolProfile.findMany({
select: { select: {
assetClass: true,
assetSubClass: true,
currency: true, currency: true,
dataSource: true, dataSource: true,
name: true, name: true,

View File

@ -1,5 +1,5 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
@ -27,7 +27,8 @@ export class RapidApiService implements DataProviderInterface {
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
return { return {
dataSource: this.getName() dataSource: this.getName(),
symbol: aSymbol
}; };
} }
@ -112,6 +113,10 @@ export class RapidApiService implements DataProviderInterface {
return {}; return {};
} }
public getTestSymbol() {
return undefined;
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
return { items: [] }; return { items: [] };
} }

View File

@ -1,26 +1,19 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import { DataSource, SymbolProfile } from '@prisma/client';
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { countries } from 'countries-list';
import { addDays, format, isSameDay } from 'date-fns'; import { addDays, format, isSameDay } from 'date-fns';
import yahooFinance from 'yahoo-finance2'; import yahooFinance from 'yahoo-finance2';
import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface';
@Injectable() @Injectable()
export class YahooFinanceService implements DataProviderInterface { export class YahooFinanceService implements DataProviderInterface {
@ -28,7 +21,8 @@ export class YahooFinanceService implements DataProviderInterface {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService private readonly cryptocurrencyService: CryptocurrencyService,
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
) { ) {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
} }
@ -37,127 +31,20 @@ export class YahooFinanceService implements DataProviderInterface {
return true; return true;
} }
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
let symbol = aYahooFinanceSymbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
);
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
symbol = `${this.baseCurrency}${symbol}`;
}
return symbol.replace('=X', '');
}
/**
* Converts a symbol to a Yahoo Finance symbol
*
* Currency: USDCHF -> USDCHF=X
* Cryptocurrency: BTCUSD -> BTC-USD
* DOGEUSD -> DOGE-USD
*/
public convertToYahooFinanceSymbol(aSymbol: string) {
if (
aSymbol.includes(this.baseCurrency) &&
aSymbol.length > this.baseCurrency.length
) {
if (
isCurrency(
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
)
) {
return `${aSymbol}=X`;
} else if (
this.cryptocurrencyService.isCryptocurrency(
aSymbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
)
)
) {
// Add a dash before the last three characters
// BTCUSD -> BTC-USD
// DOGEUSD -> DOGE-USD
// SOL1USD -> SOL1-USD
return aSymbol.replace(
new RegExp(`-?${this.baseCurrency}$`),
`-${this.baseCurrency}`
);
}
}
return aSymbol;
}
public async getAssetProfile( public async getAssetProfile(
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
const response: Partial<SymbolProfile> = {}; const { assetClass, assetSubClass, currency, name } =
await this.yahooFinanceDataEnhancerService.getAssetProfile(aSymbol);
try { return {
const symbol = this.convertToYahooFinanceSymbol(aSymbol); assetClass,
const assetProfile = await yahooFinance.quoteSummary(symbol, { assetSubClass,
modules: ['price', 'summaryProfile', 'topHoldings'] currency,
}); name,
dataSource: this.getName(),
const { assetClass, assetSubClass } = this.parseAssetClass( symbol: aSymbol
assetProfile.price };
);
response.assetClass = assetClass;
response.assetSubClass = assetSubClass;
response.currency = assetProfile.price.currency;
response.dataSource = this.getName();
response.name = this.formatName({
longName: assetProfile.price.longName,
quoteType: assetProfile.price.quoteType,
shortName: assetProfile.price.shortName,
symbol: assetProfile.price.symbol
});
response.symbol = aSymbol;
if (assetSubClass === AssetSubClass.MUTUALFUND) {
response.sectors = [];
for (const sectorWeighting of assetProfile.topHoldings
?.sectorWeightings ?? []) {
for (const [sector, weight] of Object.entries(sectorWeighting)) {
response.sectors.push({ weight, name: this.parseSector(sector) });
}
}
} else if (
assetSubClass === AssetSubClass.STOCK &&
assetProfile.summaryProfile?.country
) {
// Add country if asset is stock and country available
try {
const [code] = Object.entries(countries).find(([, country]) => {
return country.name === assetProfile.summaryProfile?.country;
});
if (code) {
response.countries = [{ code, weight: 1 }];
}
} catch {}
if (assetProfile.summaryProfile?.sector) {
response.sectors = [
{ name: assetProfile.summaryProfile?.sector, weight: 1 }
];
}
}
const url = assetProfile.summaryProfile?.website;
if (url) {
response.url = url;
}
} catch (error) {
Logger.error(error, 'YahooFinanceService');
}
return response;
} }
public async getDividends({ public async getDividends({
@ -177,7 +64,9 @@ export class YahooFinanceService implements DataProviderInterface {
try { try {
const historicalResult = await yahooFinance.historical( const historicalResult = await yahooFinance.historical(
this.convertToYahooFinanceSymbol(symbol), this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
symbol
),
{ {
events: 'dividends', events: 'dividends',
interval: granularity === 'month' ? '1mo' : '1d', interval: granularity === 'month' ? '1mo' : '1d',
@ -227,7 +116,9 @@ export class YahooFinanceService implements DataProviderInterface {
try { try {
const historicalResult = await yahooFinance.historical( const historicalResult = await yahooFinance.historical(
this.convertToYahooFinanceSymbol(aSymbol), this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
aSymbol
),
{ {
interval: '1d', interval: '1d',
period1: format(from, DATE_FORMAT), period1: format(from, DATE_FORMAT),
@ -276,8 +167,9 @@ export class YahooFinanceService implements DataProviderInterface {
if (aSymbols.length <= 0) { if (aSymbols.length <= 0) {
return {}; return {};
} }
const yahooFinanceSymbols = aSymbols.map((symbol) => const yahooFinanceSymbols = aSymbols.map((symbol) =>
this.convertToYahooFinanceSymbol(symbol) this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(symbol)
); );
try { try {
@ -287,7 +179,10 @@ export class YahooFinanceService implements DataProviderInterface {
for (const quote of quotes) { for (const quote of quotes) {
// Convert symbols back // Convert symbols back
const symbol = this.convertFromYahooFinanceSymbol(quote.symbol); const symbol =
this.yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
quote.symbol
);
response[symbol] = { response[symbol] = {
currency: quote.currency, currency: quote.currency,
@ -308,9 +203,10 @@ export class YahooFinanceService implements DataProviderInterface {
response[`${this.baseCurrency}GBp`] = { response[`${this.baseCurrency}GBp`] = {
...response[symbol], ...response[symbol],
currency: 'GBp', currency: 'GBp',
marketPrice: new Big(response[symbol].marketPrice) marketPrice: this.getConvertedValue({
.mul(100) symbol: `${this.baseCurrency}GBp`,
.toNumber() value: response[symbol].marketPrice
})
}; };
} else if ( } else if (
symbol === `${this.baseCurrency}ILS` && symbol === `${this.baseCurrency}ILS` &&
@ -320,9 +216,10 @@ export class YahooFinanceService implements DataProviderInterface {
response[`${this.baseCurrency}ILA`] = { response[`${this.baseCurrency}ILA`] = {
...response[symbol], ...response[symbol],
currency: 'ILA', currency: 'ILA',
marketPrice: new Big(response[symbol].marketPrice) marketPrice: this.getConvertedValue({
.mul(100) symbol: `${this.baseCurrency}ILA`,
.toNumber() value: response[symbol].marketPrice
})
}; };
} else if ( } else if (
symbol === `${this.baseCurrency}ZAR` && symbol === `${this.baseCurrency}ZAR` &&
@ -332,9 +229,10 @@ export class YahooFinanceService implements DataProviderInterface {
response[`${this.baseCurrency}ZAc`] = { response[`${this.baseCurrency}ZAc`] = {
...response[symbol], ...response[symbol],
currency: 'ZAc', currency: 'ZAc',
marketPrice: new Big(response[symbol].marketPrice) marketPrice: this.getConvertedValue({
.mul(100) symbol: `${this.baseCurrency}ZAc`,
.toNumber() value: response[symbol].marketPrice
})
}; };
} }
} }
@ -357,6 +255,10 @@ export class YahooFinanceService implements DataProviderInterface {
} }
} }
public getTestSymbol() {
return 'AAPL';
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
const items: LookupItem[] = []; const items: LookupItem[] = [];
@ -404,15 +306,24 @@ export class YahooFinanceService implements DataProviderInterface {
return currentQuote.symbol === marketDataItem.symbol; return currentQuote.symbol === marketDataItem.symbol;
}); });
const symbol = this.convertFromYahooFinanceSymbol( const symbol =
marketDataItem.symbol this.yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
); marketDataItem.symbol
);
const { assetClass, assetSubClass } =
this.yahooFinanceDataEnhancerService.parseAssetClass({
quoteType: quote.quoteType,
shortName: quote.shortname
});
items.push({ items.push({
assetClass,
assetSubClass,
symbol, symbol,
currency: marketDataItem.currency, currency: marketDataItem.currency,
dataSource: this.getName(), dataSource: this.getName(),
name: this.formatName({ name: this.yahooFinanceDataEnhancerService.formatName({
longName: quote.longname, longName: quote.longname,
quoteType: quote.quoteType, quoteType: quote.quoteType,
shortName: quote.shortname, shortName: quote.shortname,
@ -427,42 +338,6 @@ export class YahooFinanceService implements DataProviderInterface {
return { items }; return { items };
} }
private formatName({
longName,
quoteType,
shortName,
symbol
}: {
longName: Price['longName'];
quoteType: Price['quoteType'];
shortName: Price['shortName'];
symbol: Price['symbol'];
}) {
let name = longName;
if (name) {
name = name.replace('Amundi Index Solutions - ', '');
name = name.replace('iShares ETF (CH) - ', '');
name = name.replace('iShares III Public Limited Company - ', '');
name = name.replace('iShares V PLC - ', '');
name = name.replace('iShares VI Public Limited Company - ', '');
name = name.replace('iShares VII PLC - ', '');
name = name.replace('Multi Units Luxembourg - ', '');
name = name.replace('VanEck ETFs N.V. - ', '');
name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', '');
name = name.replace('Vanguard Funds Public Limited Company - ', '');
name = name.replace('Vanguard Index Funds - ', '');
name = name.replace('Xtrackers (IE) Plc - ', '');
}
if (quoteType === 'FUTURE') {
// "Gold Jun 22" -> "Gold"
name = shortName?.slice(0, -6);
}
return name || shortName || symbol;
}
private getConvertedValue({ private getConvertedValue({
symbol, symbol,
value value
@ -483,89 +358,4 @@ export class YahooFinanceService implements DataProviderInterface {
return value; return value;
} }
private parseAssetClass(aPrice: Price): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
switch (aPrice?.quoteType?.toLowerCase()) {
case 'cryptocurrency':
assetClass = AssetClass.CASH;
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
break;
case 'equity':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
break;
case 'etf':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
break;
case 'future':
assetClass = AssetClass.COMMODITY;
assetSubClass = AssetSubClass.COMMODITY;
if (
aPrice?.shortName?.toLowerCase()?.startsWith('gold') ||
aPrice?.shortName?.toLowerCase()?.startsWith('palladium') ||
aPrice?.shortName?.toLowerCase()?.startsWith('platinum') ||
aPrice?.shortName?.toLowerCase()?.startsWith('silver')
) {
assetSubClass = AssetSubClass.PRECIOUS_METAL;
}
break;
case 'mutualfund':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.MUTUALFUND;
break;
}
return { assetClass, assetSubClass };
}
private parseSector(aString: string): string {
let sector = UNKNOWN_KEY;
switch (aString) {
case 'basic_materials':
sector = 'Basic Materials';
break;
case 'communication_services':
sector = 'Communication Services';
break;
case 'consumer_cyclical':
sector = 'Consumer Cyclical';
break;
case 'consumer_defensive':
sector = 'Consumer Staples';
break;
case 'energy':
sector = 'Energy';
break;
case 'financial_services':
sector = 'Financial Services';
break;
case 'healthcare':
sector = 'Healthcare';
break;
case 'industrials':
sector = 'Industrials';
break;
case 'realestate':
sector = 'Real Estate';
break;
case 'technology':
sector = 'Technology';
break;
case 'utilities':
sector = 'Utilities';
break;
}
return sector;
}
} }

View File

@ -1,12 +1,11 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.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 { MarketDataModule } from './market-data.module';
import { PrismaModule } from './prisma.module';
@Module({ @Module({
exports: [ExchangeRateDataService], exports: [ExchangeRateDataService],
imports: [ imports: [

Some files were not shown because too many files have changed in this diff Show More