Compare commits

..

717 Commits

Author SHA1 Message Date
420f331be9 Release 2.101.0 (#3632) 2024-08-03 16:58:08 +02:00
e0068c4d5d Feature/harden container security following OWASP best practices (#3614)
* Harden container security

* Update changelog
2024-08-03 16:55:18 +02:00
85661884a6 Release 2.100.0 (#3631) 2024-08-03 15:47:52 +02:00
8f6203d296 Feature/manage tags of holdings (#3630)
* Manage tags of holdings

* Update changelog
2024-08-03 15:46:01 +02:00
2fa723dc3c Bugfix/fix language selector of user account settings (#3613)
Fix value of Català
2024-08-03 15:42:21 +02:00
a500fb72c5 Feature/refactor Angular Material theme (#3629) 2024-08-02 20:57:03 +02:00
02db0db733 Feature/persist view mode of holdings tab on home page (#3624)
* Persist view mode of holdings in user settings

* Update changelog
2024-08-02 20:27:58 +02:00
c87b08ca8b Feature/improve language localization for es (#3625)
* Update translations

* Update changelog
2024-08-01 20:28:42 +02:00
fcc2ab1a48 Feature/change color assignment by annualized performance in treemap chart (#3617)
* Change color assignment to annualized performance

* Update changelog
2024-07-31 19:18:50 +02:00
7efda2f890 Feature/improve language localization for Catalan (#3598)
* Update translations

* Update changelog
2024-07-30 14:30:08 +02:00
3794a61d2d Release 2.99.0 (#3618) 2024-07-29 20:10:26 +02:00
c1d1ea9dde Feature/migrate from Yarn 1 (Classic) to npm (#3601)
* Migrate from yarn to npm
2024-07-29 20:08:43 +02:00
0d676a46c8 Release 2.98.0 (#3615) 2024-07-27 19:53:44 +02:00
97db144e01 Feature/skip derived currencies in get quotes of data provider service (#3610)
* Skip derived currencies

* Update changelog
2024-07-27 19:47:06 +02:00
cec55127c8 Bugix/fix dividend import from data provider for holdings without account (#3606)
* Fix dividend import for holdings without account

* Update changelog
2024-07-27 19:45:12 +02:00
f3f359bcfb Feature/Improve language localization for spanish (#3612)
* Update messages.es.xlf
2024-07-26 15:55:26 +02:00
601e6f4147 Feature/improve account selector of create or update activity dialog (#3607)
* Improve empty value of account selector

* Update changelog
2024-07-25 19:39:07 +02:00
e228b4925c Feature/update notes of personal finance tools (#3611)
* Update notes
2024-07-25 19:38:52 +02:00
62e3ffe413 Feature/upgrade prisma to version 5.17.0 (#3597)
* Upgrade prisma to version 5.17.0

* Update changelog
2024-07-24 19:28:05 +02:00
6af885fde0 Feature/improve language localization for Spanish (#3605)
* Improve language localization for Spanish

* Update changelog
2024-07-24 11:51:58 +02:00
dd15bba359 Bugfix/fix public page for non existent access (#3604)
* Handle non-existent access

* Update changelog
2024-07-23 21:00:20 +02:00
43fca7ff43 Feature/improve personal finance tools product page (#3599)
* Localize origin
* Localize regions
* Localize tags
2024-07-23 20:59:23 +02:00
faa6af5694 Feature/improve handling of numerical precision in value component (#3595)
* Improve handling of numerical precision in value component

* Update changelog
2024-07-22 19:35:25 +02:00
d2ea7a0bfb Feature/upgrade nx to version 19.5.1 (#3596)
* Upgrade angular and Nx

* Update changelog
2024-07-21 09:41:16 +02:00
3f6319e00b Feature/setup catala (#3593)
* Set up Català

* Update changelog
2024-07-20 17:13:44 +02:00
5601299648 Release 2.97.0 (#3592) 2024-07-20 11:24:47 +02:00
6060c7cfe0 Feature/upgrade prettier to version 3.3.3 (#3586)
* Upgrade prettier to version 3.3.3

* Update changelog
2024-07-20 11:18:00 +02:00
ba78c2783d Feature/improve numerical precision in holding detail dialog (#3584)
* Improve numerical precision in holding detail dialog

* Update changelog
2024-07-20 11:17:36 +02:00
48eee5f865 Feature/upgrade node.js from version 18 to 20 (#3553)
* Upgrade to Node.js 20

* Update changelog
2024-07-20 10:30:05 +02:00
f4a8acdb46 Feature/add selfh.st logo to landing page (#3582)
* Add selfh.st

* Update changelog
2024-07-20 10:13:28 +02:00
1d6ba22598 Feature/improve language localization for de (#3583)
* Update translations
2024-07-20 10:12:49 +02:00
e38be8d710 Feature/upgrade nx to version 19.4.3 (#3581)
* Upgrade Nx to version 19.4.3

* Update changelog
2024-07-19 11:56:18 +02:00
da5be3fb57 Feature/reuse open-color in portfolio proportion chart component (#3562)
* Reuse open-color
2024-07-18 10:14:12 +02:00
b5317a7f95 Feature/improve language localization for de 20240715 (#3574)
* Update translations

* Update changelog
2024-07-17 17:37:56 +02:00
43afb16808 Feature/introduce isUsedByUsersWithSubscription flag (#3573) 2024-07-16 20:51:49 +02:00
d5c56fb16c Feature/optimize 7d data gathering by prioritization (#3575)
* Optimize 7d data gathering by prioritization

* Update changelog
2024-07-16 20:45:34 +02:00
b94c1f280b Bugfix/fix spacing on pricing page (#3571)
* Fix spacing
2024-07-16 20:42:41 +02:00
acc59866a3 Bugfix/fix table sorting of holdings (#3572)
* Hide holdings table to fix sorting

* Update changelog
2024-07-15 15:14:34 +02:00
c9fc3e402d Release 2.96.0 (#3570) 2024-07-13 20:13:53 +02:00
6c1317f978 Bugfix/fix search for holding in assistant (#3569)
* Fix search for holding

* Update changelog
2024-07-13 20:11:40 +02:00
89be438e66 Bugfix/remove show condition of experimental features setting (#3568)
* Remove show condition of experimental feature setting

* Update changelog
2024-07-13 19:02:47 +02:00
9d6214e93a Bugfix/fix fees calculation in portfolio summary (#3567)
* Fix fees calculation

* Update changelog
2024-07-13 18:24:03 +02:00
0640b24290 Feature/improve site.webmanifest (#3564)
* Separate icon purposes

* Update changelog
2024-07-13 11:40:45 +02:00
6eb9d9d973 Feature/extend personal finance tools 20240713 (#3565) 2024-07-13 11:40:29 +02:00
9ecc3176a5 Feature/improve treemap chart for holdings (#3563)
* Various improvements

* Introduce permission: accessHoldingsChart
* Improve style of toggle
* Add border radius

* Update changelog
2024-07-13 10:45:10 +02:00
96434c5a54 Release 2.95.0 (#3561) 2024-07-12 21:04:38 +02:00
4063c62a17 Feature/setup treemap chart for holdings (#3560)
* Setup treemap chart

* Update changelog
2024-07-12 21:02:12 +02:00
890c5b986c Feature/improve formatting of variables in README.md (#3546) 2024-07-10 17:22:47 +02:00
423bd92b89 Release 2.94.0 (#3556) 2024-07-09 18:44:53 +02:00
5dc331e386 Feature/improve language localization for de 20240709 (#3555)
* Update translations

* Update changelog
2024-07-09 18:43:20 +02:00
744dc51dcd Bugfix/fix pagination issue in activities endpoint by adding secondary sort criterion (#3554)
* Add id as secondary sort criterion to ensure consistent ordering

* Update changelog
2024-07-09 18:42:03 +02:00
b0c53d050a Feature/harmonize delete labels in admin market data (#3552) 2024-07-09 18:20:25 +02:00
830569b38e Release 2.93.0 (#3551) 2024-07-07 18:25:33 +02:00
35b4aef06f Feature/improve market state logic for forex in eod historical data service (#3550) 2024-07-07 18:23:51 +02:00
bc2fd9c970 Feature/add WTD and MTD to documentation (#3542) 2024-07-07 09:55:52 +02:00
c42a8aebed Feature/add platforms concept to faq page (#3549)
* Add concept of platforms

* Update changelog
2024-07-07 09:55:12 +02:00
fad1adb91b Feature/improve usability to delete currency asset profile (#3541)
* Improve usability

* Update changelog
2024-07-07 09:54:54 +02:00
9cd37f8de0 Feature/add crypto coins and stock heatmaps to resources page (#3548)
* Add heatmaps

* Crypto Coins Heatmap
* Stock Heatmap

* Update changelog
2024-07-07 09:40:55 +02:00
d49b90d7a5 Feature/refresh cryptocurrencies list 20240706 (#3544)
* Refresh cryptocurrencies list

* Update changelog
2024-07-07 09:39:29 +02:00
130a9ea062 Feature/remove obsolete version from docker compose files (#3543)
* Remove obsolete version

* Update changelog
2024-07-07 09:16:48 +02:00
ffc6309850 Feature/refactor thresholds of x ray rules (#3545)
* Refactor thresholds

* Update changelog
2024-07-07 08:25:51 +02:00
976cc7f243 Feature/upgrade nx to version 19.4.0 (#3540)
* Upgrade Nx to version 19.4.0

* Update changelog
2024-07-06 22:15:33 +02:00
7067aca04b Feature/replace twitter.com with x.com (#3535)
* Replace twitter.com with x.com
2024-07-05 17:26:12 +02:00
1c9805bb96 Feature/improve allocations by etf holding for impersonation mode (#3534)
* Improve allocations by ETF holding for impersonation mode

* Update changelog
2024-07-04 20:25:15 +02:00
8227a2d91a Feature/improve detection of json used via scraper configuration (#3539)
* Improve detection of json

* Update changelog
2024-07-03 18:16:07 +02:00
194aee97db Feature/update development instructions to control flow (#3466) 2024-07-02 11:58:13 +02:00
0f77169952 Fix wording (#3463) 2024-07-01 21:03:15 +02:00
0f8dc62c53 Release 2.92.0 (#3532) 2024-06-30 09:23:03 +02:00
554136cdcd Feature/bulk deletion for asset profiles (#3531)
* Add support for bulk deletion of asset profiles

* Update changelog
2024-06-30 09:21:04 +02:00
83b5cfff1f Feature/upgrade prisma to version 5.16.1 (#3526)
* Upgrade prisma to version 5.16.1

* Update changelog
2024-06-29 17:06:21 +02:00
dcec3accf0 Feature/improve caching of benchmarks (#3530)
* Improve caching

* Update changelog
2024-06-29 16:53:35 +02:00
f08b0b570b Feature/support derived currencies in currency validation (#3529)
* Support derived currencies in currency validation

* Update changelog
2024-06-29 16:30:40 +02:00
8386fec98a Feature/automatic deletion of unused asset profiles (#3525)
* Automatic deletion of unused asset profiles

* Update changelog
2024-06-29 10:53:25 +02:00
4d3dff3e5b Feature/extend personal finance tools 20240629 (#3528)
* Add Anlage.App

* Add Portfoloo

* Add SharesMaster

* Add Merlin

* Add Holistic

* Add AlphaTrackr

* Add Segmio
2024-06-29 10:53:08 +02:00
76890e63fa Bugfix/fix all time high in benchmarks (#3527)
* Fix all time high

* Update changelog
2024-06-29 10:03:45 +02:00
4fb2aebf4f Release 2.91.0 (#3522) 2024-06-26 20:40:29 +02:00
ed5cd3b978 Feature/upgrade angular to version 18.0.4 (#3520)
* Upgrade Angular to version 18.0.4

* Update changelog
2024-06-26 20:38:26 +02:00
469c1936b4 Bugfix/fix horizontal overflow in historical market data table of admin control panel (#3515)
* Fix horizontal overflow

* Update changelog
2024-06-26 20:38:12 +02:00
8b3cc5c11a Bugfix/fix dialog position on mobile (#3521)
* Fix dialog position on mobile

* Update changelog
2024-06-26 20:19:25 +02:00
ee086638f3 Feature/add benchmarks preset to admin control panel (#3513)
* Add benchmarks preset

* Update changelog
2024-06-26 20:18:53 +02:00
58d1abbd38 Feature/clean up imports (#3514)
* Clean up imports
2024-06-25 19:52:07 +02:00
ba979cbae2 Bugfix/fix addition of manual asset without market data (#3516)
* Provide default value

* Update changelog
2024-06-24 21:24:03 +02:00
8cda43bb63 Bugfix/persist intraday market data only if market state is open (#3509)
* Persist INTRADAY data only if market state is open

* Update changelog
2024-06-23 10:23:03 +02:00
c4499df74c Feature/add wealthy tracker (#3510)
* Add Wealthy Tracker
2024-06-23 10:22:35 +02:00
24bcc15b6a Release 2.90.0 (#3508) 2024-06-22 09:58:19 +02:00
ff121243e4 Feature/extend asset profile for currency (#3495)
* Extend asset profile for currency

* Update changelog
2024-06-22 09:54:23 +02:00
70e633b997 Feature/upgrade ngx device detector to version 8.0.0 (#3505)
* Upgrade ngx-device-detector to version 8.0.0

* Update changelog
2024-06-21 11:05:35 +02:00
0780ee4adb Feature/improve language localization for de 20240620 (#3504)
* Update translations

* Update changelog
2024-06-20 20:45:56 +02:00
09613f9324 Feature/extend self hosting faq by mobile app question (#3500)
* Add question about mobile app

* Update changelog
2024-06-20 17:45:31 +02:00
8642b1a7af Feature/upgrade zone.js to version 0.14.7 (#3501)
* Upgrade zone.js to version 0.14.7

* Update changelog
2024-06-19 13:52:05 +02:00
f96f861341 Feature/upgrade ngx markdown to version 18.0.0 (#3498)
* Upgrade ngx-markdown to version 18.0.0

* Update changelog
2024-06-18 20:53:03 +02:00
a201fc7a97 Feature/upgrade stripe dependencies 20240617 (#3499)
* Upgrade Stripe dependencies

* Update changelog
2024-06-18 20:37:02 +02:00
a97110348c Feature/move active filters indicator to general availability (#3485)
* Move to general availability

* Update changelog
2024-06-18 20:11:49 +02:00
a25d5b9dc0 Feature/improve error handling in biometric authentication registration (#3496)
* Improve error handling in biometric authentication registration

* Update changelog
2024-06-17 16:41:12 +02:00
6c2acf2aa6 Feature/set up ssl for local development (#3482)
* Set up SSL for local development

* Update changelog
2024-06-15 10:53:20 +02:00
519827045a Feature/add dialog for benchmarks in markets overview (#3493)
* Add benchmarks dialog in markets overview

* Update changelog
2024-06-15 09:49:54 +02:00
79a7e12a9f Release 2.89.0 (#3491) 2024-06-14 03:43:47 +02:00
bf20a5de82 Feature/improve date validation in activity endpoints (#3489)
* Improve date validation

* Update changelog
2024-06-14 03:40:47 +02:00
0adefe14e1 Feature/improve language localization (#3487)
* Update translations
2024-06-13 12:08:15 +02:00
f24561cc3d Feature/improve style of personal finance tools list (#3486) 2024-06-13 12:07:42 +02:00
873fd53715 Feature/extend market data with currencies preset by activities count and date (#3460)
* Extend market data with currencies preset by activities count and date

* Update changelog
2024-06-12 20:28:55 +02:00
e5d8faf2dc Feature/improve language localization for de 20240612 (#3483)
* Update translations

* Update changelog
2024-06-12 10:43:54 +02:00
65d3bd2802 Release 2.88.0 (#3484) 2024-06-11 20:02:21 +02:00
ad60373813 Feature/improve style of blog post list (#3481)
* Improve style

* Update changelog
2024-06-11 20:00:41 +02:00
b725e6e2ec Feature/migrate client to control flow (#3475)
* Migrate to control flow

* Update changelog
2024-06-11 19:46:16 +02:00
88c420ca5e Feature/improve language localization for de 20240611 (#3480)
* Update translations

* Update changelog
2024-06-11 19:20:13 +02:00
118e17f78c Feature/improve wording on allocations page (#3479) 2024-06-11 11:58:26 +02:00
cc92592d86 Feature/refactor personal finance tools section (#3478)
* Refactoring
2024-06-11 11:57:24 +02:00
46eb3254a9 Feature/set image source label in Dockerfile (#3477)
* Set image source label in Dockerfile

* Update changelog
2024-06-10 17:42:17 +02:00
2477491f18 Feature/upgrade nx to version 19.2.2 (#3474)
* Upgrade Nx to version 19.2.2 and Angular to version 18.0.2

* Update changelog
2024-06-09 10:24:16 +02:00
5fc9fde129 Release 2.87.0 (#3473) 2024-06-08 19:33:21 +02:00
00e50c6abe Feature/improve portfolio summary (#3472)
* Improve portfolio summary

* Update changelog
2024-06-08 19:31:50 +02:00
8131a7ad03 Feature/improve error handling in http response interceptor (#3471)
* Improve error handling in HttpResponseInterceptor

* Update changelog
2024-06-08 19:30:03 +02:00
f5e6f7dcfe Bugfix/fix initialization of fire calculator (#3470)
* Fix initialization

* Update changelog
2024-06-08 18:44:57 +02:00
87501e094d Feature/improve allocations by ETF holding for mobile (#3469) 2024-06-08 17:20:38 +02:00
d3bfdf78c3 Feature/upgrade prisma to version 5.15.0 (#3462)
* Upgrade prisma to version 5.15.0

* Update changelog
2024-06-08 16:15:36 +02:00
fc4e6ae6db Feature/improve language localization for de 20240608 (#3468)
* Update translations

* Update changelog
2024-06-08 15:51:20 +02:00
23e4d5454d Feature/improve allocations by etf holding (#3467)
* Improve allocations by ETF holding

* Update changelog
2024-06-08 11:17:27 +02:00
fdcf5fd396 Release 2.86.0 (#3465) 2024-06-07 21:47:18 +02:00
8a9ae9bb33 Feature/allocations by etf holding (#3464)
* Setup allocations by ETF holding

* Update changelog
2024-06-07 21:45:07 +02:00
3fb7e746df Feature/upgrade prettier to version 3.3.1 (#3459)
* Upgrade prettier to version 3.3.1

* Update changelog
2024-06-07 12:12:34 +02:00
137e8e090a Release 2.85.0 (#3461) 2024-06-06 17:25:08 +02:00
6d941500cd Bugfix/fix default locale in value component (#3454)
* Fix default locale

* Update changelog
2024-06-06 17:22:23 +02:00
9c4e22978d Feature/update translations (#3458) 2024-06-05 17:40:39 +02:00
af65a99398 Feature/extend personal finance tools 20240604 (#3457)
* Add Koyfin

* Add Navexa

* Add Stonksfolio
2024-06-05 17:32:43 +02:00
1a0cb561cd Feature/add ability to close user account (#3444)
* Add ability to close user account

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-06-04 15:07:06 +02:00
9f875adf0c Feature/improve language localization for de 20240602 (#3452)
* Update translations

* Update changelog
2024-06-03 20:26:58 +02:00
6b0dadb895 Feature/upgrade ng extract i18n merge to version 2.12.0 (#3449)
* Upgrade ng-extract-i18n-merge to version 2.12.0

* Update changelog
2024-06-02 10:22:31 +02:00
98a9523eee Feature/extend personal finance tools (#3448) 2024-06-02 10:20:09 +02:00
dfb3365efb Release 2.84.0 (#3447) 2024-06-01 11:16:13 +02:00
0e08d8830e Handle reduce of empty array (#3446) 2024-06-01 11:14:34 +02:00
69d85eadfd Feature/setup cascading on delete for various relations in database schema (#3445)
* Setup cascading on delete

* Update changelog
2024-06-01 11:08:36 +02:00
c009f8c12f Feature/add data provider info to asset profile details dialog (#3434)
* Add data provider info to asset profile details dialog

* Update changelog
2024-06-01 10:55:42 +02:00
60ef46accf Bugfix/fix state handling of currency selector component (#2795) (#3429)
* Fix state handling of currency selector component

* Update changelog
2024-06-01 10:53:02 +02:00
b12ac1fe84 Feature/simplify module imports of api (#3443)
* Simplify module imports
2024-06-01 10:02:43 +02:00
4355c96ab6 Bugfix/fix initial annual interest rate in fire calculator (#3437)
* Fix initial annual interest rate

* Update changelog
2024-05-31 17:29:19 +02:00
fb326fe0cc Release 2.83.0 (#3442) 2024-05-30 20:47:43 +02:00
02cfebd98c Feature/upgrade yahoo finance2 to version 2.11.3 (#3441)
* Upgrade yahoo-finance2 to version 2.11.3

* Update changelog
2024-05-30 20:45:32 +02:00
dd2936d703 Feature/upgrade countup.js to version 2.8.0 (#3436)
* Upgrade countup.js to version 2.8.0

* Update changelog
2024-05-29 16:06:58 +02:00
918d0b85d4 Feature/update passport dependencies (#3433)
* Update passport dependencies

* Refactor Google strategy

* Update changelog
2024-05-28 13:47:45 +02:00
a061595101 Feature/upgrade class validator to version 0.14.1 (#3431)
* Upgrade class-validator to version 0.14.1

* Update changelog
2024-05-27 13:55:24 +02:00
dcd496ac50 Feature/upgrade angular to version 17.3.10 (#3430)
* Upgrade angular to version 17.3.10

* Update changelog
2024-05-26 14:13:43 +02:00
6b9ec549da Feature/upgrade prisma to version 5.14.0 (#3423)
* Upgrade prisma to version 5.14.0

* Update changelog
2024-05-24 15:36:36 +02:00
b5bd4df483 Feature/upgrade nx to version 19.0.5 (#3422)
* Upgrade Nx to version 19.0.5

* Update changelog
2024-05-23 20:48:26 +02:00
766a2d7c2f Release 2.82.0 (#3425) 2024-05-22 19:53:20 +02:00
6dabf7516a Feature/preselect account in create or update activity dialog (#3413)
* Preselect account if there is only one

* Update changelog
2024-05-22 18:15:08 +02:00
5d49ff7a4a Feature/improve usability of date range selector in assistant (#3409)
* Improve usability of date range selector

* Update changelog
2024-05-22 16:39:39 +02:00
8998c18836 Feature/upgrade internationalized number to version 3.5.2 (#3412)
* Upgrade @internationalized/number to version 3.5.2

* Update changelog
2024-05-22 16:36:50 +02:00
741a0e36d2 Add links to tagged issues (#3405)
* help wanted
* good first issue
2024-05-20 22:28:43 +03:00
e31b4c64cb Feature/refactor holding detail dialog to standalone (#3407)
* Refactor holding detail dialog to standalone

* Update changelog
2024-05-19 19:13:14 +03:00
812ff5cbdc Feature/refresh cryptocurrencies list 20240514 (#3411)
* Refresh cryptocurrencies list

* Update changelog
2024-05-18 17:40:25 +03:00
035b90a689 Feature/upgrade zone.js to version 0.14.5 (#3410)
* Upgrade zone.js to version 0.14.5

* Update changelog
2024-05-17 18:08:37 +03:00
5ea3a187f4 Feature/upgrade body parser to version 1.20.2 (#3406)
* Upgrade body-parser to version 1.20.2

* Update changelog
2024-05-16 17:26:27 +02:00
5d9c38663d Feature/migrate various pages to standalone components (#3404)
* Migrate to standalone components

* Update changelog
2024-05-15 12:04:07 +02:00
5616bc4956 Feature/validate account balance creation/update using DTO (#3400)
* Validate create account balance using DTO
2024-05-13 13:47:33 +02:00
9ad1c2177c Release 2.81.0 (#3403) 2024-05-12 10:42:04 +02:00
15bf9f2f9c Feature/add Türkçe to footer (#3401) 2024-05-12 10:34:33 +02:00
ebc5008569 Feature/improve language localization for de 20240512 (#3402)
* Update translations

* Update changelog
2024-05-12 10:25:59 +02:00
37759ba03f Feature/improve language localization for tr 20240501 (#3355)
* Improve translations

* Update changelog

---------

Co-authored-by: sadmimye <134071831+sadmimye@users.noreply.github.com>
2024-05-12 09:58:11 +02:00
8319b216bb Feature/support delete activities with filtering (#3394)
* Support delete activities with filtering

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-05-12 09:56:07 +02:00
782d131b0d Feature/add indicator for active filters (#3398)
* Add indicator for active filters

* Update changelog
2024-05-12 09:42:27 +02:00
72e75208df Bugfix/fix position detail dialog close functionality (#3396)
* Handle holding detail dialog open functionality in a single place (AppComponent)

* Update changelog
2024-05-11 20:27:18 +02:00
4b1c27c245 Feature/upgrade nx to version 19.0.2 (#3391)
* Upgrade Nx dependencies to version 19.0.2

* Update changelog
2024-05-11 08:33:33 +02:00
61f0da35bc Feature/disable delete all activities if filters are active (#3389)
* Disable delete all activities button if filters are active

* Update changelog
2024-05-10 08:51:34 +02:00
80464c7846 Release 2.80.0 (#3386) 2024-05-08 20:55:39 +02:00
74f4323903 Feature/increase spacing around floating action buttons (#3385)
* Increase spacing around floating action buttons

* Update changelog
2024-05-08 20:54:13 +02:00
127dbf9dcd Update translations (#3384) 2024-05-08 20:37:53 +02:00
66bdb374e8 Feature/set icon columns of tables to stick at beginning (#3377)
* Set icon columns to stick at the beginning

* Update changelog
2024-05-08 20:04:58 +02:00
4ad4fa2b30 Feature/clean up deprecated GET api/portfolio/positions endpoint (#3373) 2024-05-08 20:04:32 +02:00
1fd836194f Feature/add absolute change column to holdings table (#3378)
* Add absolute change column

* Update changelog
2024-05-08 20:02:50 +02:00
2090db1199 Feature/increase number of attempts of queue jobs (#3376)
* Increase number of attempts

* Update changelog
2024-05-07 20:48:02 +02:00
053c7e591e Feature/upgrade ionicons to version 7.4.0 (#3356)
* Upgrade ionicons to version 7.4.0

* Update changelog
2024-05-07 19:00:55 +02:00
9b5e350e3b Feature/harmonize log message (#3343) 2024-05-06 17:03:58 +02:00
378e57c3bc Feature/add links to Home Assistant add-on (#3367)
* Add links to Home Assistant add-on
2024-05-06 17:02:18 +02:00
6765191a8c Bugfix/fix position detail dialog open in holding search of assistant (#3374)
* Open position detail dialog (via holding search of assistant)

* Update changelog
2024-05-05 09:09:57 +02:00
8438a45bcf Update changelog (#3372) 2024-05-04 16:32:32 +02:00
30a64e7fc1 Release 2.79.0 (#3371) 2024-05-04 15:54:29 +02:00
f2cb671c7f Feature/optimize get porfolio details endpoint (#3366)
* Eliminate getPerformance() from getSummary() function

* Disable cache for getDetails()

* Add hint to portfolio summary

* Update changelog
2024-05-04 15:53:02 +02:00
3f41e5c5de Bugfix/fix locale in markets overview (#3369)
* Fix locale if no user is logged in

* Update changelog
2024-05-04 15:51:09 +02:00
c1ad483f33 Improve alignment (#3370) 2024-05-04 15:50:47 +02:00
f3d961bc16 Feature/move holdings table to holdings tab of home page (#3368)
* Move holdings table to holdings tab of home page

* Deprecate api/v1/portfolio/positions endpoint

* Update changelog
2024-05-04 14:11:37 +02:00
42b70ef568 Feature/improve performance labels in position detail dialog (#3363)
* Improve performance labels (with and without currency effects)

* Update changelog
2024-05-04 07:49:37 +02:00
77beaaba08 Refactoring portfolio service (#3365) 2024-05-03 21:48:46 +02:00
d9c07456cd Release 2.78.0 (#3361) 2024-05-02 20:32:47 +02:00
0a53df4293 Feature/improve inactive user role (#3360)
* Improve inactive role

* Update changelog
2024-05-02 20:31:20 +02:00
4416ba0c88 Feature/set performance column of holdings table to stick at end (#3353)
* Set up stickyEnd in performance column

* Update changelog
2024-05-02 19:16:59 +02:00
486de968a2 Bugfix/fix division by zero error in dividend yield calculation (#3354)
* Handle division by zero

* Update changelog
2024-05-02 17:53:34 +02:00
a5833566a8 Feature/skip caching in portfolio calculator if active filters (#3348)
* Skip caching if active filters

* Update changelog
2024-05-02 17:52:39 +02:00
261f5844dd Add type column to README.md (#3295) 2024-04-30 14:08:14 +02:00
2173c418a7 Feature/validate forms using DTO for access, asset profile, tag and platform management (#3337)
* Validate forms using DTO for access, asset profile, tag and platform management

* Update changelog
2024-04-30 08:04:45 +02:00
4efd5cefd8 Bugfix/calculation of portfolio summary caused by future liabilities (#3342)
* Adapt date of future activities

* Update changelog
2024-04-29 20:12:12 +02:00
d735e4db75 Release 2.77.1 (#3340) 2024-04-27 19:25:23 +02:00
e10707fde4 Add missing guard to fix public page (#3339) 2024-04-27 19:24:08 +02:00
ac953df809 Release 2.77.0 (#3338) 2024-04-27 15:37:48 +02:00
bb4ee50738 Feature/update browserslist database 20240417 (#3288)
* Update the browserslist database

* Update changelog
2024-04-27 15:35:57 +02:00
4f41bac328 Feature/set up caching in portfolio calculator (#3335)
* Set up caching

* Update changelog
2024-04-27 15:35:28 +02:00
cd07802400 Bugfix/fix historical market data gathering for asset profiles with manual data source (#3336)
* Fix historical market data gathering for asset profiles with MANUAL data source

* Update changelog
2024-04-27 15:29:32 +02:00
b692b7432c Feature/set up event system for portfolio changes (#3333)
* Set up event system for portfolio changes

* Update changelog
2024-04-26 20:13:53 +02:00
a4efbc0131 Feature/migrate UI components to control flow (#3324)
* Migrate to control flow

* Update changelog
2024-04-26 17:40:00 +02:00
55b0fe232c Bugfix/reset form values to null if empty string (#3327)
* Reset form values to null if empty string

* Update changelog
2024-04-25 19:04:28 +02:00
46432edce9 Feature/extend faq by custom asset instructions (#3326)
* Add instructions for custom asset

* Update changelog
2024-04-24 20:35:39 +02:00
990028316e Refactor form controls to form getter (#3325) 2024-04-24 20:20:56 +02:00
37871fbabc Feature/upgrade prisma to version 5.13.0 (#3323)
* Upgrade prisma to version 5.13.0

* Update changelog
2024-04-24 20:20:09 +02:00
0fdeef7953 Release 2.76.0 (#3322) 2024-04-23 18:58:43 +02:00
cdbe6eedeb Feature/change cash to liquidity in asset class enum (#3321)
* Change CASH to LIQUIDITY in asset class enum

* Update changelog
2024-04-23 18:55:37 +02:00
a6dde8ad43 Release 2.75.1 (#3317) 2024-04-21 17:14:35 +02:00
39bd4a349b Feature/improve chart in account detail dialog (#3314)
* Improve net worth calculation in portfolio performance chart

* Improve account balance management

* Update changelog
2024-04-21 17:11:53 +02:00
ab59eb5c92 Release 2.75.0 (#3316) 2024-04-21 10:30:14 +02:00
1132dc9bdd Feature/add unique constraint to account balance database schema (#3315)
* Add accountId and date as unique constraint to AccountBalance schema

* Update changelog
2024-04-21 10:28:51 +02:00
2d70b18593 Remove links (#3306) 2024-04-21 10:08:22 +02:00
b6ea7d23fa Bugfix/add total value in base currency to redacted values (#3313)
* Add totalValueInBaseCurrency

* Update changelog
2024-04-21 08:33:38 +02:00
95382581f1 Release 2.74.0 (#3312) 2024-04-20 15:43:03 +02:00
551b83a6e3 Bugfix/fix gaps in portfolio performance charts (#3311)
* Fix gaps in charts

* Update changelog
2024-04-20 15:40:59 +02:00
895c4fe299 Improve style on mobile (#3310) 2024-04-20 12:54:23 +02:00
ccb1bf881e Feature/improve language localization for de 20240420 (#3309)
* Update translations

* Update changelog
2024-04-20 12:54:06 +02:00
7788b5a987 Improve messages (#3308) 2024-04-20 12:53:51 +02:00
47fd029e0c Clean up styles (#3305) 2024-04-20 12:15:21 +02:00
458ee159e1 Feature/upgrade nx to version 18.3.3 (#3307)
* Upgrade angular and Nx dependencies

* Update changelog
2024-04-20 12:14:50 +02:00
dfacbed66d Feature/add support to create account cash balances (#3260)
* Add support to create account cash balances

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-04-20 11:00:00 +02:00
22d63c6102 Improve logging (#3304)
* Improve logging
2024-04-20 10:49:18 +02:00
212aa6a63b Feature/add date range support to portfolio holdings page (#3303)
* Add date range support

* Update changelog
2024-04-20 09:51:12 +02:00
bed9ae916c Migrate UI components to standalone (#3302) 2024-04-19 20:57:47 +02:00
b6ad362850 Feature/remove date range support in activities table on portfolio activities page (#3301)
* Remove date range support

* Update changelog
2024-04-19 20:44:28 +02:00
ab86dd5318 Add Home Assistant (#3291) 2024-04-19 20:43:52 +02:00
dba73d80a3 Improve logging (#3300) 2024-04-19 20:31:38 +02:00
92fb05320a Migrate UI components to standalone (#3296) 2024-04-19 18:31:35 +02:00
73d62bb51f Migrate UI components to standalone (#3290) 2024-04-18 20:46:12 +02:00
127b7d4f25 Make pre-commit executable by default (#3283) 2024-04-18 18:39:20 +02:00
e79d607ab8 Release 2.73.0 (#3287) 2024-04-17 17:42:28 +02:00
5f7d083f7c Feature/upgrade yahoo finance2 to version 2.11.2 (#3286)
* Upgrade yahoo-finance2 to version 2.11.2

* Update changelog
2024-04-17 17:40:58 +02:00
15857118fe Feature/let data gathering queue jobs fail by throwing errors (#3281)
* Let data gathering queue jobs fail by throwing errors

* Update changelog
2024-04-17 17:35:51 +02:00
ff91ed21df Upgrade @types/lodash to version 4.17.0 (#3227) 2024-04-15 19:25:46 +02:00
9241c04d5a Feature/add form validation against DTO for activity and account (#3230)
* Add form validation against DTO for activity and account

* Update changelog
2024-04-14 19:52:41 +02:00
5d4e2fba8c Feature/move wealth item and liability calculations to portfolio calculator (#3272)
* Move (wealth) item calculations to portfolio calculator

* Move liability calculations to portfolio calculator

* Update changelog
2024-04-14 08:12:32 +02:00
6c57609db8 Feature/move dividend fee and interest calculation to portfolio calculator (#3267)
* Move dividend, feee and interest calculation to portfolio calculator

* Update changelog
2024-04-13 11:07:18 +02:00
b31bbbe2d1 Release 2.72.0 (#3270) 2024-04-13 09:31:07 +02:00
7d308917dd Feature/upgrade yahoo finance2 to version 2.11.1 (#3254)
* Upgrade yahoo-finance2 to version 2.11.1

* Update changelog
2024-04-13 09:28:38 +02:00
4e7d93db13 Feature/adapt priorities of data gathering jobs (#3262)
* Adapt priorities of data gathering jobs

* Update changelog
2024-04-13 09:28:14 +02:00
45340b581f Bugfix/fix public page by including markets data (#3263)
* Include markets data

* Update changelog
2024-04-12 15:13:34 +02:00
6f8fe45fc2 Update OSS Friends (#3258) 2024-04-11 19:14:56 +02:00
34d9ceb009 Feature/add support to immediately execute queue job (#3259)
* Add support to immediately execute queue job

* Update changelog
2024-04-11 19:14:03 +02:00
2b97bbd05d Move getChart() to portfolio calculator (#3255) 2024-04-09 13:44:23 +02:00
3bf7ac76a0 Feature/upgrade nx to version 18.2.3 (#3256)
* Upgrade Nx to version 17.3.3

* Update changelog
2024-04-08 08:07:00 +02:00
71892e67b2 Feature/upgrade prisma to version 5.12.1 (#3253)
* Upgrade prisma to version 5.12.1

* Update changelog
2024-04-07 20:27:38 +02:00
6e2885ed20 Release 2.71.0 (#3252) 2024-04-07 11:32:55 +02:00
07c0e5a612 Feature/add currency to order database schema (#3251)
* Add currency to Order database schema

* Update changelog
2024-04-07 11:30:32 +02:00
719bbe156e Feature/optimize calculation of allocations by market (#3249)
* Optimize calculation of allocations by market

* Update changelog
2024-04-07 09:26:30 +02:00
b51255a543 Feature/add key to x ray rule (#3248)
* Add key

* Update changelog
2024-04-07 09:25:14 +02:00
50dbbf0569 Feature/refactor symbol icon module to asset profile icon component (#3245)
* Refactor symbol icon module to asset profile icon component (standalone)
2024-04-06 21:15:19 +02:00
ca2e748c56 Bugfix/add missing tags in portfolio calculator (#3243)
* Add missing tags

* Update changelog
2024-04-06 20:03:16 +02:00
5c480109d5 Feature/add quotes in README.md for API auth documentation (#3246) 2024-04-06 20:00:56 +02:00
6152ff4b44 Remove condition (#3242) 2024-04-06 12:50:35 +02:00
c6641fde36 Feature/add icon to create or update platform dialog (#3241)
* Add platform icon

* Update changelog
2024-04-06 09:11:15 +02:00
4ae7e9fcbe Feature/add icon to asset profile dialog (#3240)
* Add asset profile icon

* Update changelog
2024-04-06 08:42:06 +02:00
c10ae431a2 Feature/extend faq by data providers (#3239)
* Add data providers

* Update changelog
2024-04-06 08:40:44 +02:00
883e30e451 Feature/improve language localization for de 20240403 (#3236)
* Update translations

* Update changelog
2024-04-05 19:38:10 +02:00
f1f4f6247d Feature/validate url in create and update platform dto (#3235)
* Validate url

* Update changelog
2024-04-04 09:08:37 +02:00
82fe1de1a7 Feature/add support to override asset (sub) class and url in admin control panel (#3218)
* Add support to override asset (sub) class and url in admin control panel

* Update changelog
2024-04-03 20:24:38 +02:00
371c999fbc Feature/Add dividend yield to position detail dialog (#2636)
* Add dividend yield to position detail dialog

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-04-03 19:47:53 +02:00
26b9660e11 Release 2.70.0 (#3234) 2024-04-02 20:29:47 +02:00
ca7717f9c5 Bugfix/Enable tini in docker compose files instead of adding it to the Dockerfile (#3232)
* Enable tini in docker compose files instead of adding it to the Dockerfile

* Update changelog
2024-04-02 20:26:14 +02:00
6f3cce1c5f Feature/disable option to update cash balance if date is not today (#3229)
* Disable option to update cash balance if date is not today

* Update changelog
2024-04-02 20:17:16 +02:00
efdc9b387f Eliminate ghostfolio-style.scss (#3228) 2024-04-01 13:24:00 +02:00
d7b579e3e8 Feature/refactor getAnnualizedPerformancePercent to portfolio calculator (#3226)
* Move getAnnualizedPerformancePercent() to portfolio calculator
2024-04-01 10:42:15 +02:00
b8533050b0 Bugfix/fix duplicated tags in position detail dialog (#3224)
* Fix duplicated tags

* Update changelog
2024-04-01 09:22:35 +02:00
1b81409b35 Add OpenAlternative logo (#3225) 2024-04-01 09:02:10 +02:00
8cd6c34ed8 Feature/introduce portfolio calculator factory (#3214)
* Introduce portfolio calculator factory

* Update changelog
2024-03-31 20:07:58 +02:00
0c68474802 Extract locales (#3223) 2024-03-31 11:41:44 +02:00
34997f91db Feature/setup webpack bundle analyzer (#3222)
* Set up  Webpack Bundle Analyzer

* Update changelog
2024-03-31 11:38:09 +02:00
084467ee9a Feature/reverse order of specific years in date range selector of assistant (#3221)
* Reverse order

* Update changelog
2024-03-31 11:24:50 +02:00
af47889d65 Add Chinese translations (#3215)
* Add Chinese translations
2024-03-31 11:24:32 +02:00
51203ec96e Feature/setup chinese (#3220)
* Set up chinese

* Update changelog
2024-03-31 10:46:30 +02:00
a2277dea2c Release 2.69.0 (#3212) 2024-03-30 13:43:43 +01:00
debd233c32 Feature/set up Tini to avoid zombie processes (#3195)
* Set up Tini to avoid zombie processes

* Update changelog
2024-03-30 13:42:02 +01:00
f1eeee0525 Feature/extend date range support by specific years (#3190)
* Extend date range support by specific years

* Support date range in benchmark endpoint

* Support date range in activities endpoint

* Update changelog
2024-03-30 13:06:42 +01:00
5ffc39c32f Feature/improve usability to delete asset profile (#3208)
* Disable delete button for benchmarks

* Update changelog
2024-03-30 11:23:59 +01:00
a668a66e84 Feature/add missing dates to edit historical market data in asset profile details dialog (#3206)
* Add missing dates to edit historical market data in asset profile details dialog

* Update changelog
2024-03-30 08:50:06 +01:00
0581b8b9ec Release 2.68.0 (#3205) 2024-03-29 17:54:17 +01:00
63a61fb492 Move portfolio calculator (#3204)
* Move portfolio calculator
2024-03-29 17:47:44 +01:00
5788c6474e Refactor portfolio calculator (#3203)
* Refactor portfolio calculator

* Consume Activity[]
* Change computeTransactionPoints() to private
* Eliminate getTransactionPoints()

* Update changelog
2024-03-29 17:34:22 +01:00
5529fdc0ee Move transaction points to constructor (#3202) 2024-03-29 13:50:24 +01:00
88a9b518f6 Bugfix/fix issue with overridden names in activities table (#3200)
* Fix issue with overridden names

* Update changelog
2024-03-29 09:50:04 +01:00
98de2355c4 Feature/Support overriding name of asset profile dialog (#3199)
* Support overriding name of asset profile dialog

* Update changelog
2024-03-29 09:35:41 +01:00
b41eb60348 Fix chart tooltip of benchmark comparator (#3167)
* Fix chart tooltip of benchmark comparator

* Update changelog
2024-03-28 18:23:56 +01:00
0edebe30e1 Feature/Add user account currency to export (#3194)
* Add user account currency to export

* Update changelog
2024-03-27 18:08:34 +01:00
e3abe4feee Release 2.67.0 (#3197) 2024-03-26 17:45:32 +01:00
50391e199a Feature/improve generation of random strings (#3196)
* Replace Math.random() with crypto.randomBytes()

* Update changelog
2024-03-26 17:43:42 +01:00
a33f8d5bed Upgrade @types/big.js to version 6.2.2 (#3191)
* Upgrade @types/big.js to version 6.2.2

* Refactor imports
2024-03-25 08:34:30 +01:00
636be8441e Feature/upgrade zone.js to version 0.14.4 (#3184)
* Upgrade zone.js to version 0.14.4

* Update changelog
2024-03-24 10:30:06 +01:00
654dc2ba32 Feature/upgrade yahoo finance2 to version 2.11.0 (#3183)
* Upgrade yahoo-finance2 to version 2.11.0

* Update changelog
2024-03-24 09:50:45 +01:00
458ef169f4 Feature/add support for toncoin cryptocurrency (#3189)
* Add Toincoin

* Update changelog
2024-03-24 09:50:00 +01:00
5bb01bb03c Feature/upgrade ionicons to version 7.3.0 (#3182)
* Upgrade ionicons to version 7.3.0

* Update changelog
2024-03-24 09:09:09 +01:00
43e9528d8c Release 2.66.3 (#3188) 2024-03-23 20:56:22 +01:00
522c54c9b4 Release 2.66.2 (#3187) 2024-03-23 20:40:09 +01:00
0004ced4e1 Release 2.66.1 (#3186) 2024-03-23 20:26:10 +01:00
274c60e961 Release 2.66.0 (#3185) 2024-03-23 19:55:17 +01:00
754e98099c Feature/Set up tini (#3168)
* Set up tini to avoid zombie processes and perform signal forwarding in docker image

* Update changelog
2024-03-23 19:53:27 +01:00
87bf8df1c3 Bugfix/missing performance chart in presenter view (#3181)
* Fix missing performance chart in presenter view

* Update changelog
2024-03-23 19:41:28 +01:00
3f7d6b25c7 Feature/extend faq by backup strategy (#3180)
* Add backup strategy

* Update changelog
2024-03-23 19:10:23 +01:00
8a062e03ab Feature/add benchmark name to tooltip of benchmark comparator (#3177)
* Add benchmark name to chart tooltip

* Update changelog
2024-03-23 09:38:35 +01:00
a70f45cbf3 Feature/add index for data source symbol to market data table (#3179)
* Add index

* Update changelog
2024-03-23 09:23:59 +01:00
f268264c46 Feature/upgrade nx to version 18.1.2 (#3174)
* Upgrade angular and Nx dependencies

* Update changelog
2024-03-23 09:09:10 +01:00
bbe5d70720 Rename Twitter to X (#3009) 2024-03-21 15:10:35 +01:00
f1d2a52cba Release 2.65.0 (#3170) 2024-03-19 20:13:22 +01:00
87cc887865 Feature/Set meta theme color dynamically to respect appearance (#3129)
* Set meta theme color dynamically to respect appearance (dark mode)

* Update changelog
2024-03-19 20:11:34 +01:00
61ecd15f1d Feature/change edit button to a in admin market data page (#3164)
* Change edit button to a

* Update changelog
2024-03-19 19:55:08 +01:00
eb853f05ae Feature/add support to delete asset profile from dialog (#3165)
* Add support to delete asset profile from dialog

* Update changelog
2024-03-19 19:27:16 +01:00
6285417903 Feature/change grant private access with permissions to general availability (#3169)
* Change grant private access with permissions from experimental to general availability

* Update changelog
2024-03-19 19:25:43 +01:00
ca674a654e Feature/add symbol and isin to position detail dialog (#3163)
* Add symbol and ISIN

* Update translations

* Update changelog
2024-03-18 13:58:46 +01:00
2729c5651f Release 2.64.0 (#3161) 2024-03-16 19:00:50 +01:00
7e28e42995 Feature/exclude fees from holdings (#3160) 2024-03-16 18:59:23 +01:00
e21563d903 Feature/increase timeout to load benchmarks (#3158)
* Increase request timeout

* Update changelog
2024-03-16 18:15:34 +01:00
3ede69650c Feature/upgrade prisma to version 5.11.0 (#3159)
* Upgrade prisma to version 5.11.0

* Update changelog
2024-03-16 16:51:07 +01:00
c289793c6d Feature/switch between active and closed holdings (#3146)
* Switch between active and closed holdings on the portfolio holdings page

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-03-16 14:20:58 +01:00
a90c067da0 Clean up (#3157) 2024-03-16 13:40:23 +01:00
38c2baf943 Feature/improve exception handling of current investments in various rules (#3156)
* Improve exception handling

* Update changelog
2024-03-16 12:29:21 +01:00
82c78cad6b Format (#3155) 2024-03-16 12:28:58 +01:00
bffe6060bd Pass portfolio calculator to getChart() (#3153) 2024-03-16 10:05:16 +01:00
841bd5c33f Bugfix/fix dividend accumulation in symbol metrics (#3152)
* Fix total dividend calculation

* Update changelog
2024-03-16 10:04:57 +01:00
3b895afc9e Enable account balance update for fee and interest activities (#3145)
* Enable account balance update for fee and interest activities

* Update changelog
2024-03-15 18:56:17 +01:00
00c2ede85e Feature/improve usability of platform and tag management (#3144)
* Improve usability

* Update changelog
2024-03-15 08:37:41 +01:00
8420cb830c Feature/add product roadmap to faq (#3143)
* Add product roadmap

* Update changelog
2024-03-14 14:09:20 +01:00
a0ddd1f9b9 Fix date conversion in import of historical market data (#3117)
* Fix date conversion in import of historical market data

* Update changelog
2024-03-13 20:44:33 +01:00
40d93066ff Introduce .env.dev (#3120) 2024-03-13 20:21:54 +01:00
671e4e316b Release 2.63.2 (#3138) 2024-03-12 20:50:32 +01:00
473136e9aa Release 2.63.1 (#3135)
* Release 2.63.1
2024-03-11 21:35:43 +01:00
9a3db91982 Release 2.63.0 (#3134) 2024-03-11 20:19:23 +01:00
d23cb5f190 Feature/upgrade simplewebauthn dependencies to version 9.0 (#3130)
* Upgrade @simplewebauthn/browser and @simplewebauthn/server to version 9.0

* Update changelog
2024-03-11 20:17:47 +01:00
7a364472c8 Bugfix/fix liability issue in allocations (#3133)
* Remove liabilities from allocations calculation

* Update changelog
2024-03-11 20:16:56 +01:00
59c064e3c8 Feature/upgrade yahoo finance2 to version 2.10.0 (#3127)
* Upgrade yahoo-finance2 to version 2.10.0

* Update changelog
2024-03-11 20:15:55 +01:00
e792924606 Update OSS friends (#3132) 2024-03-11 20:15:32 +01:00
d32dd5e860 Feature/upgrade countries list to version 3.1.0 (#3131)
* Upgrade countries-list to version 3.1.0

* Update changelog
2024-03-11 19:16:20 +01:00
bb86f85203 Feature/add available home server systems to faq (#3126)
* Add available home server systems

* Update changelog

* Add CasaOS to README.md
2024-03-10 09:50:43 +01:00
0bca8897d6 Fix average price calculation by only considering BUY transactions (#3125)
* Fix average price calculation by only considering buy transactions

* Update changelog
2024-03-10 09:35:47 +01:00
ba73f6de2e Release 2.62.0 (#3124) 2024-03-09 19:57:56 +01:00
eb75be8535 Optimize details endpoint (#3123)
* Make summary optional

* Introduce dedicated holdings endpoint

* Update changelog
2024-03-09 19:56:26 +01:00
6d2a897366 Refactor orders with activities (#3122) 2024-03-09 17:17:52 +01:00
d8bfb23f20 Refactor reduce() with getSum() (#3121) 2024-03-09 16:53:59 +01:00
d9d71e7827 Fix issue with removing account from activity (#3112)
* Fix issue with removing account from activity

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-03-09 15:52:05 +01:00
b642ce08e5 Refactor item type (#3119) 2024-03-09 12:32:56 +01:00
bc8d8309d4 Improve handling of future liabilities (#3118)
* Improve handling of future liabilities

* Refactor currentValue to currentValueInBaseCurrency

* Update changelog
2024-03-09 11:07:01 +01:00
1f2f9f22f2 Feature/remove environment variable web auth rp (#3115)
* Remove environment variable WEB_AUTH_RP_ID

* Update changelog
2024-03-08 19:00:21 +01:00
7a3237f1ff Adapt style of inactive users (#3114) 2024-03-08 18:59:23 +01:00
07661d9262 Feature/integrate dividend into transaction point concept (#3092)
* Integrate dividend into transaction point concept

* Update changelog
2024-03-07 20:07:50 +01:00
77358eed65 Feature/Include user role in admin endpoint (#3107)
* Include user role in admin endpoint
2024-03-07 19:38:57 +01:00
c641c28b12 Release 2.61.1 (#3110) 2024-03-06 22:08:00 +01:00
c54392b7bb Bugfix/fix exception in account value calculation (#3109)
* Fix exception in value of account calculation caused by liabilities

* Update changelog
2024-03-06 22:06:27 +01:00
f3a8822a77 Feature/remove-v-from-version-in-admin-endpoint (#3101)
* Remove "v" from version in admin endpoint
2024-03-06 11:11:10 +01:00
f1dc075c36 Update translations (#3093) 2024-03-05 10:16:59 +01:00
144d831954 Release 2.61.0 (#3097) 2024-03-04 20:17:05 +01:00
c37ad9bad4 Bugfix/fix activities import (#3095)
* Fix query parameter handling of booleans

* Update changelog
2024-03-04 20:15:41 +01:00
4ab3f81384 Extract getFactor() (#3089)
* Extract getFactor()

* Refactoring
2024-03-03 20:04:49 +01:00
b932bac9aa Feature/optimize summary calculation (#3088)
* Optimize calculation

* Update changelog
2024-03-03 08:24:51 +01:00
bcdd873222 Add missing title (#3087) 2024-03-02 17:32:32 +01:00
25b3de5828 Release 2.60.0 (#3086) 2024-03-02 14:44:00 +01:00
40b454d2f3 Feature/refresh cryptocurrencies list 20240302 (#3085)
* Update cryptocurrencies.json

* Add UNI7083

* Update changelog
2024-03-02 14:42:40 +01:00
5596e5f03b Feature/integrate wealth items into transaction point concept (#3084)
* Integrate (wealth) items into transaction point concept

* Update changelog
2024-03-02 14:29:03 +01:00
66992ef915 Bugfix/change show condition of button to fetch current market price (#3079)
* Change show condition of button to fetch current market price

* Update changelog
2024-03-02 12:59:54 +01:00
7f67430685 Bugfix/readd value in base currency to activity (#3078)
* Readd valueInBaseCurrency

* Update changelog
2024-03-02 10:03:10 +01:00
8a49a04324 Feature/improve usability of benchmarks in markets overview (#3077)
* Improve icons, localize label

* Update changelog
2024-03-02 09:48:53 +01:00
5d7c19b0ed Fix typo (#3076) 2024-03-02 09:42:41 +01:00
cde74b6c62 Release 2.59.0 (#3069) 2024-02-29 21:06:18 +01:00
633c65e33c Feature/extend self hosting faq (#3068)
* Extend self-hosting FAQ

* Update changelog
2024-02-29 21:04:47 +01:00
d1617f2d87 Feature/add index for is excluded to account database table (#3067)
* Add index for isExcluded to account database table

* Update changelog
2024-02-29 20:50:44 +01:00
68e558f198 Feature/Improve activities import by ISIN number (#3051)
* Improve activities import by ISIN number

* Update changelog
2024-02-29 20:45:40 +01:00
12ca01c862 Update OSS friends (#3066) 2024-02-29 20:26:13 +01:00
2115745471 Bugfix/fix issue with exchange rate calculation of wealth items in accounts (#3065)
* Fix exchange rate calculatio of wealth items in accounts

* Update changelog
2024-02-29 20:14:52 +01:00
2cabd21315 Release 2.58.0 (#3061) 2024-02-27 20:59:44 +01:00
3615e2f057 Feature/improve handling of activities without account (#3060)
* Improve handling of activities without account

* Update changelog
2024-02-27 20:58:28 +01:00
d3679d41b3 Bugfix/fix query to filter activities of excluded accounts (#3059)
* Fix query to filter activities of excluded accounts

* Update changelog
2024-02-27 20:58:04 +01:00
f2d431a6b8 Bugfix/improve asset profile validation in activities import (#3057)
* Improve asset profile validation

* Update changelog
2024-02-27 20:42:23 +01:00
2bc8bebfb8 Clean up dist folders (#3053) 2024-02-26 20:17:18 +01:00
5b20ba3382 Release 2.57.0 (#3054) 2024-02-25 19:14:28 +01:00
15cc294581 Feature/move break down of performance from experimental to general availability (#3047)
* Move break down of performance to general availability

* Update changelog
2024-02-25 19:12:30 +01:00
b060b81204 Fix debugging with VS Code due to missing Source Map (#3050)
Fixes #2801
2024-02-25 19:10:17 +01:00
a8d557eb1b Disable parallel execution of commands causing race condition between mkdir and cp (#3052) 2024-02-25 19:03:28 +01:00
6ae3a47b54 Bugfix/change top and bottom performers to performance with currency effect (#3046)
* Change to performance with currency effect

* Update changelog
2024-02-25 13:43:49 +01:00
88c19eb45e Feature/restructure copy assets nx target (#3045)
* Restructure copy-assets Nx target

* Update changelog
2024-02-25 11:45:00 +01:00
7728706bc8 Release 2.56.0 (#3043) 2024-02-24 20:00:03 +01:00
2e9d40c201 Feature/switch to performance calculations with currency effects (#3039)
* Switch to performance calculations with currency effects

* Improve value redaction in portfolio details endpoint

* Update changelog
2024-02-24 19:58:13 +01:00
c002e37285 Feature/add missing default currency to prepare currencies function (#3042)
* Add missing default currency

* Update changelog
2024-02-24 19:45:51 +01:00
6be38a1c19 Feature/remove is default flag from account database schema (#3041)
* Remove isDefault flag from Account database schema

* Update changelog
2024-02-24 19:44:56 +01:00
a3178fb213 Feature/expose redis database via environment variable (#3036)
* Expose Redis database via environment variable

* Update changelog
2024-02-24 11:56:12 +01:00
e7158f6e16 Feature/upgrade prisma to version 5.10.2 (#3038)
* Upgrade prisma to version 5.10.2

* Update changelog
2024-02-24 11:09:13 +01:00
dbea0456bc Update changelog (#3035) 2024-02-23 19:58:33 +01:00
fefee11301 Release 2.55.0 (#3034) 2024-02-22 20:26:39 +01:00
40836b745b Feature/improve validation for currency in endpoints (#3030)
* Improve validation for currency

* Update changelog
2024-02-22 20:25:22 +01:00
07eabac059 Feature/add missing database indexes part 2 (#3033)
* Add missing database indexes (for orderBy and where clauses)

* Update changelog
2024-02-22 20:21:50 +01:00
48b412cfb8 Feature/harmonize setting of default locale (#3032)
* Harmonize setting of default locale

* Update changelog
2024-02-22 20:10:27 +01:00
b62488628c Prettify markup (#3029) 2024-02-21 09:58:15 +01:00
982c71c728 Feature/set angular parser in prettierrc (#3028)
* Set parser to angular

* Update changelog
2024-02-20 19:54:03 +01:00
5aa16a3779 Release 2.54.0 (#3027) 2024-02-19 19:47:37 +01:00
93de25e5b6 Feature/add missing database indexes (#3026)
* Add missing database indexes

* Update changelog
2024-02-19 19:45:52 +01:00
9acdb41aa2 Refactor params to object (#2987) 2024-02-19 19:32:10 +01:00
ffbdfb86ec Release 2.53.1 (#3025) 2024-02-18 18:56:34 +01:00
be7f6bb657 Feature/add inactive as user role (#3024)
* Add INACTIVE as user role

* Update changelog
2024-02-18 18:54:49 +01:00
6f7cbc93b9 Add missing type (#3023) 2024-02-18 18:54:36 +01:00
0b5c71130d Update OSS friends (#3008) 2024-02-18 18:50:04 +01:00
0578c645d1 Release 2.53.0 (#3021) 2024-02-18 14:39:10 +01:00
67ae86763e Handle premium data provider in getQuotes() (#3020)
* Handle premium data provider in getQuotes()
2024-02-18 14:37:42 +01:00
266c0a9a2c Feature/eliminate search request in get quotes of eod service (#3019)
* Eliminate search request to get quotes

* Update changelog
2024-02-18 14:14:25 +01:00
a3cdb23776 Feature/refactor query to filter activities of excluded accounts (#3016)
* Refactor query to filter activities of excluded accounts

* Update changelog
2024-02-18 12:29:00 +01:00
e1371a8d2b Clean up (#3018) 2024-02-18 12:27:34 +01:00
448cea0b69 Feature/improve usability of holdings table (#3017)
* Improve usability

* Update changelog
2024-02-18 10:18:53 +01:00
ad42c0bf28 Enable FAQ link for self-hoster (#3015) 2024-02-18 08:53:01 +01:00
f50670c7fe Feature/improve language localization for de 20240217 (#3014)
* Update translations

* Update changelog
2024-02-18 08:51:36 +01:00
c0029d3b1d Feature/upgrade ng extract i18n merge to version 2.10.0 (#3013)
* Upgrade ng-extract-i18n-merge to version 2.10.0

* Update changelog
2024-02-17 22:15:50 +01:00
2518a8fd9d Feature/add accounts tab to position detail dialog (#3012)
* Add accounts tab to position detail dialog

* Update changelog
2024-02-17 21:32:56 +01:00
572dcf075a Release 2.52.0 (#3011) 2024-02-16 20:05:01 +01:00
29cb83d469 Bugfix/improve x axis scale of dividend and investment timeline (#3010)
* Improve X-axis scale

* Update changelog
2024-02-16 20:03:20 +01:00
cac73ac111 Feature/divide faq page in three sections (#3003)
* Divide FAQ page in three sections

* General
* Cloud (SaaS)
* Self-Hosting

* Update changelog
2024-02-16 18:57:34 +01:00
02cf4295a9 Feature/add loading indicator to dividend and investment timelines (#3007)
* Add loading indicators

* Dividend timeline
* Investment timeline

* Update changelog
2024-02-16 09:43:51 +01:00
78b3328bf7 Add activities count (#3005) 2024-02-15 10:25:47 +01:00
e0d6d9e8ca Migrate if / else to control flow (#3001) 2024-02-13 20:46:21 +01:00
54310f2214 Feature/add support for jupiter cryptocurrency (#2999)
* Add JUP29210

* Update changelog
2024-02-13 19:46:15 +01:00
1fec49fbc2 Improve states (#3000) 2024-02-13 19:44:17 +01:00
d00489b547 Release 2.51.0 (#2998) 2024-02-12 20:37:07 +01:00
2985dd67c5 Feature/improve ordered list in safari (#2996)
* Improve ordered list in Safari (without text-truncate)

* Update changelog
2024-02-12 20:35:16 +01:00
5eba764c04 Feature/upgrade eslint dependencies 20240212 (#2995)
* Upgrade eslint dependencies

* Update changelog
2024-02-12 17:22:29 +01:00
cc0ce18627 Bugfix/fix date conversion in import of historical market data (#2994)
* Fix date conversion

* Update changelog
2024-02-12 17:21:22 +01:00
b758654158 Fix renew plan label (#2993) 2024-02-11 21:37:11 +01:00
d5d40c0ea1 Migrate to control flow (#2991)
* Migrate to control flow
2024-02-11 20:51:06 +01:00
fd294d4d2b Migrate to control flow (#2992) 2024-02-11 20:47:24 +01:00
e82cf2e7d0 Feature/upgrade nx to version 18.0.4 (#2990)
* Upgrade Nx to version 18.0.4

* Update changelog
2024-02-11 20:19:45 +01:00
446c7cb517 Remove closing tags of mat-* components (#2989) 2024-02-11 18:12:16 +01:00
e921ed7f52 Reorder imports (#2988) 2024-02-11 17:50:18 +01:00
865402be3a Feature/replace import sort with prettier plugin sort imports (#2872)
* Replace import-sort with prettier-plugin-sort-imports

* Update changelog
2024-02-11 17:26:35 +01:00
6eb659d7e6 Release 2.50.0 (#2984) 2024-02-11 12:41:58 +01:00
37430b7bdc Feature/upgrade prisma to version 5.9.1 (#2983)
* Upgrade prisma to version 5.9.1

* Update changelog
2024-02-11 12:38:49 +01:00
ef9d77312e Introduce renewal-early-bird (#2982) 2024-02-11 12:33:34 +01:00
ccaf06360a Feature/introduce admin setting to disable data gathering (#2981)
* Introduce setting to disable data gathering

* Update changelog
2024-02-11 10:06:07 +01:00
f83e75df44 Feature/harmonize env variables of various api keys (#2980)
* Harmonize env variables of various API keys

* Update changelog
2024-02-11 09:44:12 +01:00
00a2b60eb5 Release 2.49.0 (#2979) 2024-02-09 19:30:00 +01:00
fcbf2f1645 Feature/remove lazy from name of activities table (#2978)
* Remove lazy from name

* Update translations
2024-02-09 18:48:05 +01:00
460266a501 Feature/upgrade yahoo finance2 to version 2.9.1 (#2963)
* Upgrade yahoo-finance2 to version 2.9.1

* Update changelog
2024-02-09 18:41:18 +01:00
9fe90273c7 Feature/move assistant to general availability (#2977)
* Move assistant from experimental to general availability

* Update changelog
2024-02-09 18:25:15 +01:00
4078229fe6 Feature/add button to apply filters in assistant (#2971)
* Add apply filters button

* Update changelog
2024-02-09 09:45:54 +01:00
609c03f174 Add analytics image (#2970) 2024-02-08 08:00:04 +01:00
e7d4641d13 Feature/reload data on logo click (#2959)
* Reload data on logo click

* Update changelog
2024-02-07 21:03:28 +01:00
cc1d9811e0 Release 2.48.1 (#2968) 2024-02-06 17:00:40 +01:00
35450ac004 Bugfix/add missing data provider info to search results of coingecko (#2967)
* Add missing data provider info

* Update changelog
2024-02-06 16:58:54 +01:00
9c18f48a32 Release 2.48.0 (#2962) 2024-02-05 19:57:40 +01:00
87529490c3 Feature/refresh cryptocurrencies list 20240205 (#2961)
* Update cryptocurrencies.json

* Update changelog
2024-02-05 19:56:16 +01:00
893e76f83f Feature/provide data provider info in search (#2958)
* Provide data provider info in search

* Update changelog
2024-02-05 19:55:39 +01:00
06ba7a4b1b Feature/extend assistant by asset class selector (#2957)
* Remove tabs

* Add asset class selector

* Update changelog
2024-02-04 15:50:58 +01:00
c68d113d27 Feature/improve usability of account and tag selector of assistant (#2955)
* Change radio button to select

* account
* tag

* Update changelog
2024-02-04 12:11:01 +01:00
69e3bee52c Feature/upgrade prettier to version 3.2.5 (#2954)
* Upgrade prettier to version 3.2.5

* Update changelog
2024-02-04 12:10:33 +01:00
cea569c987 Add Fina (#2956) 2024-02-04 12:10:22 +01:00
Hey
2a38a16f6b Feature/Improve error logs for timeout in data provider services (#2953)
* Improve error logs for timeout in data provider services

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-02-04 11:56:00 +01:00
0f9455cf02 Release 2.47.0 (#2951) 2024-02-03 09:44:29 +01:00
d4afa03505 Bugfix/fix rendering issue with date range selector of assistant (#2950)
* Improve click handling

* Improve locales

* Update changelog
2024-02-03 09:42:50 +01:00
c9237146e2 Feature/add investment value to chart (#2948)
* Add investment value to chart

* Update changelog
2024-02-03 09:23:19 +01:00
faad65b6f3 Update translations (#2940)
* Update translations

* Update changelog
2024-02-02 20:42:12 +01:00
e459c72100 Update OSS friends (#2942) 2024-02-01 20:54:25 +01:00
a8add30125 Feature/upgrade prettier to version 3.2.4 (#2928)
* Upgrade prettier to version 3.2.4

* Update changelog
2024-01-31 18:12:09 +01:00
b535aee91d Remove reference to Internet Identity (#2946) 2024-01-31 17:19:05 +01:00
4434d0315f Remove unused timeline calculation (#2947) 2024-01-30 20:56:41 +01:00
8b10695353 Feature/only show used tags in tag selector of assistant (#2943)
* Only show used tags in tag selector

* Update changelog
2024-01-29 19:53:47 +01:00
e82dcc8ace Feature/fix export in lazy-loaded activities table (#2939)
* Fix export in lazy-loaded activities table

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-01-29 19:37:09 +01:00
6dcb0d8583 Release 2.46.0 (#2938) 2024-01-28 10:00:07 +01:00
40b6777814 Add upgrade plan button (#2937) 2024-01-28 09:58:38 +01:00
25deba16df Feature/add reset filters button to assistant (#2936)
* Add reset filters button

* Update changelog
2024-01-28 09:47:28 +01:00
be93ca8968 Feature/migrate allocations page to work with filters of assistant (#2933)
* Migrate portfolio allocations to work with filters of assistant

* Update changelog
2024-01-28 09:20:32 +01:00
0436cc6487 Migrate ngx-skeleton-loader components to self-closing tags (#2935) 2024-01-28 08:51:02 +01:00
857708dc4d Migrate gf-* components to self-closing tags (#2934) 2024-01-28 08:50:43 +01:00
1ca4f885b0 Feature/migrate holdings page to work with filters of assistant (#2932)
* Migrate portfolio holdings to work with filters of assistant

* Update changelog
2024-01-27 19:28:13 +01:00
c9368c5cf2 Release 2.45.0 (#2931) 2024-01-27 10:54:35 +01:00
29423efea3 Update translations (#2930) 2024-01-27 10:53:19 +01:00
f3ee99fb2b Feature/extend assistant by account selector (#2929)
* Add account selector to assistant

* Update changelog
2024-01-27 10:48:46 +01:00
3df8810412 Feature/Add support to grant private access with permissions (#2870)
* Add support to grant private access with permissions

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-01-27 09:44:13 +01:00
b8ca88c6df Add auto-renewal (#2920) 2024-01-27 08:46:27 +01:00
2c068c412d Feature/migrate tag selector to form group in assistant (#2926)
* Introduce filter form group

* Update changelog
2024-01-27 08:45:55 +01:00
9fdbd22cb5 Bugfix/fix activities import for manual data source (#2923)
* Fix import

* Update changelog
2024-01-27 08:41:45 +01:00
8f5f4c5875 Feature/format name in eod historical data service (#2922)
* Format name

* Update changelog
2024-01-26 22:37:47 +01:00
50fb82a6e6 Extend personal finance tools (#2925) 2024-01-26 22:37:26 +01:00
2c10cd7edf Bugfix/remove holdings with incomplete data from top3 bottom3 performers (#2921)
* Remove holdings with incomplete data

* Update changelog
2024-01-26 08:35:23 +01:00
bbde86c66e Feature/improve language localization for de 20240124 (#2918)
* Update translations

* Update changelog
2024-01-25 21:11:07 +01:00
73c0843d51 Feature/add permissions to access model (#2833)
* Add permissions to Access model

* Update changelog
2024-01-24 19:23:58 +01:00
04fc2cd3e1 Release 2.44.0 (#2917) 2024-01-24 12:26:36 +01:00
b39c97ab9f Bugfix/improve validation for non numeric results in eod service (#2916)
* Improve validation of non-numeric numbers

* Update changelog
2024-01-24 12:24:38 +01:00
1dd5e9c787 Release 2.43.1 (#2914) 2024-01-23 20:46:37 +01:00
a9985b65b8 Adjust Dockerfile to enable healthcheck (#2913) 2024-01-23 20:44:57 +01:00
0a35d5f236 Release 2.43.0 (#2911) 2024-01-23 15:49:58 +01:00
09ce8b1cd0 Feature/add support for importing dividends from eod (#2910)
* Add support for importing dividends

* Update changelog
2024-01-23 15:48:09 +01:00
a5ed49fe4c Feature/Add date range selector to assistant (#2905)
* Add date range selector including WTD and MTD to assistant

* Update changelog
2024-01-23 11:57:37 +01:00
5c23ece62c Feature/improve usability of benchmark management (#2904)
* Add icon

* Update changelog
2024-01-22 20:22:32 +01:00
4e9e3f7b6b Feature/Add wtd and mtd as possible values for date range (#2902)
* Add `wtd` and `mtd` as possible values for date range
  'wtd': week-to-date (from the start of the week)
  'mtd': month-to-date (from the start of the month)

* Update changelog
2024-01-21 16:51:30 +01:00
5fc84a06cc Feature/Add healthcheck for Ghostfolio service (#2893)
* Add curl to Dockerfile image

* Add healthcheck to docker-compose.yml and docker-compose.build.yml

* Update changelog
2024-01-21 11:48:32 +01:00
12186e1c6c Release 2.42.0 (#2899) 2024-01-21 11:16:04 +01:00
f2803aecbc Add guard (#2898) 2024-01-21 11:14:44 +01:00
5ba5b86d5f Feature/improve handling of derived currencies (#2891)
* Improve handling of derived currencies

* Update changelog
2024-01-21 11:12:48 +01:00
6167f105fe Refactoring (#2897) 2024-01-21 10:27:10 +01:00
8d5f2fd91d Fix fee conversion (#2896)
* Fix fee conversion

* Update changelog
2024-01-21 10:24:43 +01:00
4ac661fb94 Feature/improve language localization for de 20240120 (#2894)
* Improve translations

* Update changelog
2024-01-21 10:16:18 +01:00
e763bfb2e2 Feature/improve labels in portfolio evolution chart and investment timeline (#2892)
* Improve labels

* Update changelog
2024-01-20 09:15:05 +01:00
88c7e34cc3 Feature/upgrade prisma to version 5.8.1 (#2859)
* Upgrade prisma to version 5.8.1

* Update changelog
2024-01-18 19:22:20 +01:00
0ee632470e Improve typings (#2889) 2024-01-17 18:06:53 +01:00
c918deeb1c Feature/Support for editing countries and sectors (#2854)
* Add support for editing countries and sectors

* Update changelog
2024-01-17 11:40:02 +01:00
1877b31f00 Release 2.41.0 (#2888) 2024-01-16 22:33:13 +01:00
00895b7bb1 Feature/increase timeout to load historical data in data provider service (#2887)
* Increase timeout to load historical data

* Update changelog
2024-01-16 22:31:28 +01:00
bff60ddbe0 Feature/improve asset profile validation in activities import for manual data source (#2886)
* Improve asset profile validation for MANUAL data source

* Update changelog
2024-01-16 21:21:51 +01:00
d46de0a15e Fix typo (#2878) 2024-01-16 19:42:56 +01:00
7b45a8b3fc Introduce type (#2885) 2024-01-16 19:42:39 +01:00
693791d113 Feature/add validation of search results in eod historical data service (#2883)
* Validate currency

* Update changelog
2024-01-16 18:03:31 +01:00
1b2d2a9860 Feature/Add holdings tab to account detail dialog (#2853) (#2864)
* Feature/Add holdings tab to account detail dialog (#2853)

* Update changelog
2024-01-15 20:35:45 +01:00
bde8be1385 Release 2.40.0 (#2877) 2024-01-15 19:42:11 +01:00
74ca058364 Extend pricing page section (#2876) 2024-01-15 19:40:27 +01:00
ba3cf82c6e Feature/increase robustness of exchange rates by always getting quotes (#2875)
* Always get quotes (with fallback to historical data)

* Update changelog
2024-01-15 19:02:00 +01:00
217bb6aa5a Release 2.39.0 (#2871) 2024-01-14 17:25:05 +01:00
440dc470fa Bugfix/fix currency inconsistency with conversion of ZAR to ZAc (#2869)
* Fix conversion from ZAR to ZAc

* Update changelog
2024-01-14 17:22:03 +01:00
165ca94f5b Feature/improve alignment in portfolio performance component (#2867)
* Improve alignment

* Update changelog
2024-01-14 17:05:42 +01:00
c418e75139 Bugfix/fix currency in error log in exchange rate data service (#2868)
* Fix currency

* Update changelog
2024-01-14 10:36:32 +01:00
76bf839010 Release 2.38.0 (#2865) 2024-01-13 16:19:57 +01:00
3bdc4c9b4a Feature/upgrade prettier to version 3.2.1 (#2860)
* Upgrade prettier to version 3.2.1

* Update changelog
2024-01-13 16:18:18 +01:00
005890d785 Improve extractNumberFromString() for international number formats (#2843)
* Set up test

* Add support for international formatted numbers

* Expose locale in scraper configuration

* Update changelog
2024-01-13 16:17:38 +01:00
256c020e88 Feature/improve indicator for delayed market data (#2862)
* Improve indicator for delayed market data

* Update changelog
2024-01-13 16:08:37 +01:00
5fa3388609 Feature/break down performance into asset and currency (#2863)
* Break down performance into asset and currency

* Nullify values

* Update changelog
2024-01-13 14:23:00 +01:00
be801b481e Feature/Add exchange rate effects to portfolio calculation (#2834)
* Add exchange rate effects to portfolio calculation

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-01-13 13:07:33 +01:00
a72e98f73c Add AllInvestView (#2861) 2024-01-13 13:06:18 +01:00
f5df970685 Release 2.37.0 (#2858) 2024-01-11 18:57:59 +01:00
edfdc0c346 Feature/improve chart size in asset profile details dialog (#2849)
* Improve chart size

* Update changelog
2024-01-11 18:55:52 +01:00
fcfe7b1787 Clean up (#2844) 2024-01-11 18:55:34 +01:00
170b8acc65 Feature/Add git pre-commit hook for yarn format (#2840)
* Set up git pre-commit hook for yarn format

* Update changelog
2024-01-10 20:36:10 +01:00
a47829082e Bugfix/fix hidden fifth tab on mobile (#2848)
* Fix hidden fifth tab

* Update changelog
2024-01-09 08:28:01 +01:00
48ab5fcf08 Feature/update docker compose instructions to compose v2 (#2836)
* Update docker compose instructions

* Update changelog
2024-01-08 20:21:47 +01:00
dc8b60eeb1 Rename "Jobs" to "Job Queue" (#2847) 2024-01-08 20:21:13 +01:00
ee67432ffc Release 2.36.0 (#2842) 2024-01-07 17:15:30 +01:00
7755a6b655 Update translations (#2841) 2024-01-07 17:13:19 +01:00
d7f72819de Feature/extend assistant by tag selector (#2838)
* Extend assistant by tag selector

* Update changelog
2024-01-07 16:56:25 +01:00
2a4d7bf14f Feature/improve language localization for de 20240106 (#2837)
* Update translations

* Update changelog
2024-01-07 16:52:02 +01:00
d49287922f Feature/refresh cryptocurrencies list 20240106 (#2835)
* Update cryptocurrencies.json

* Update changelog
2024-01-06 19:29:03 +01:00
ac0f6f40cf Remove closing tags (#2816) 2024-01-06 19:28:35 +01:00
d91f947ab0 Feature/Add Coingecko api keys support (#2827)
* Add CoinGecko API keys support

* Update changelog
2024-01-06 19:06:07 +01:00
af71274ea9 Feature/remove account type enum (#2832)
* Remove AccountType enum

* Update changelog
2024-01-06 14:19:42 +01:00
0feba4b8d9 Release 2.35.0 (#2831) 2024-01-06 10:45:26 +01:00
62f85293e2 #2820 Grant private access (#2822)
* Grant private access

* Update changelog
2024-01-06 10:27:21 +01:00
6a048cee85 Feature/add hint for twr to portfolio summary (#2824)
* Add hint for TWR

* Add TWR to README.md

* Update changelog
2024-01-06 09:31:59 +01:00
0d93612d16 Feature/improve style of assistant (#2828)
* Minor style improvements

* Update changelog
2024-01-06 09:14:48 +01:00
9bf68b0d20 Feature/enable redis authentication in docker compose files (#2805)
* Enable redis authentication in docker-compose files

* Update changelog
2024-01-04 20:14:45 +01:00
371f1dc451 Feature/support rest api in scraper (#2810)
* Support REST APIs in scraper

* Update changelog
2024-01-03 21:59:45 +01:00
5cb2ec6411 Feature/Improve user interface of access table (#2821)
* Improve alignment

* Update changelog
2024-01-03 21:08:35 +01:00
3723a1d8b8 Release 2.34.0 (#2817) 2024-01-02 17:05:12 +01:00
4c30e9459d Feature/extend assistant by date range selector (#2815)
* Extend assistant by date range selector

* Update changelog
2024-01-02 17:02:15 +01:00
23d323073d Fix performance percentage for 1d (#2814)
* Fix performance percentage for 1d

* Improve response of positions endpoint

* Update changelog
2024-01-02 14:10:08 +01:00
0ad734262a Bugfix/improve tabs on ios (#2811)
* Improve tabs on iOS (Add to Home Screen)

* Update changelog
2024-01-02 10:06:13 +01:00
0649f9fd2c Clean up (#2787) 2024-01-02 10:05:31 +01:00
d089662dab Feature/improve the style of the top 3 and bottom 3 performers (#2807)
* Refactor to ordered list

* Update changelog
2024-01-02 09:44:15 +01:00
8c1c336fc6 Feature/upgrade nx to version 17.2.8 (#2809)
* Upgrade Nx to version 17.2.8

* Update changelog
2024-01-01 17:14:53 +01:00
43b4f14ace Feature/add button to test scraper configuration (#2808)
* Add button to test scraper configuration

* Update changelog

---------

Co-authored-by: Manushreshta B L <manushreshta27@gmail.com>
Co-authored-by: Hugo Persson <hugo.e.persson@gmail.com>
2024-01-01 11:53:42 +01:00
3717e38845 Update year (#2803) 2024-01-01 10:11:04 +01:00
265d4d0450 Release 2.33.0 (#2806) 2023-12-31 13:30:27 +01:00
726e727c7d Feature/benchmark currency correction (#2790)
* Convert benchmark performance to base currency

* Introduce getExchangeRates() for multiple dates

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2023-12-31 13:28:11 +01:00
cb664774c0 Update translations (#2798)
* Update translations
2023-12-31 10:33:12 +01:00
b89bf1d5e8 Feature/increase timeout to load currencies (#2800)
* Increase timeout

* Update changelog
2023-12-31 10:23:56 +01:00
53ce37a83a Update OSS friends (#2799) 2023-12-31 10:23:13 +01:00
e9ac9057ff Fix debug instructions (#2802) 2023-12-30 21:11:56 +01:00
7020fc2a93 Feature/add hint for community language support (#2793)
* Add hint

* Update changelog
2023-12-30 10:55:11 +01:00
efcd9539dd Feature/upgrade ng extract i18n merge to 2.9.1 (#2797)
* Upgrade ng-extract-i18n-merge to version 2.9.1

* Update changelog
2023-12-30 10:54:04 +01:00
61ecc48d0e Feature/improve language localization for de 20231229 (#2796)
* Improve language localizations

* Update changelog
2023-12-30 10:34:17 +01:00
e465f1b791 Feature/add support to edit currency of asset profile with data source manual (#2789)
* Add support to edit currency

* Update changelog
2023-12-29 19:55:51 +01:00
01b6c14bcc Feature/improve handling of derived currency usx (#2788)
* Improve handling of USX

* Update changelog
2023-12-29 17:31:50 +01:00
34b02210df Feature/expose the environment variable REQUEST_TIMEOUT (#2792)
* Expose the environment variable `REQUEST_TIMEOUT`

* Update changelog
2023-12-29 17:29:33 +01:00
0034776b34 Feature/upgrade nx to version 17.2.7 (#2781)
* Upgrade Nx to version 17.2.7

* Update changelog
2023-12-29 11:26:24 +01:00
b183c45027 Time weighted portfolio performance calculation (#2778)
* Implement time weighted portfolio performance calculation

* Update changelog
2023-12-27 15:55:35 +01:00
7d68905f1b Feature/use has permission annotation in endpoints (#2771)
* Use HasPermission in endpoints

* Update changelog
2023-12-26 19:23:25 +01:00
0953c072fe Release 2.32.0 (#2784) 2023-12-26 10:18:32 +01:00
d152187ee8 Feature/upgrade prisma to version 5.7.1 (#2780)
* Upgrade prisma to version 5.7.1

* Update changelog
2023-12-26 10:16:20 +01:00
3c5affce88 Feature/upgrade prettier to version 3.1.1 (#2768)
* Upgrade prettier to version 3.1.1

* Update changelog
2023-12-24 16:23:17 +01:00
f27e21f9a0 Extend issue template (#2776) 2023-12-23 17:45:37 +01:00
337ca328c3 Feature/drop activity id on import (#2769)
* Drop activity id on import

* Update changelog
2023-12-22 20:16:02 +01:00
beb9e2c43f Feature/modernize nx executors (#2767)
* Modernize Nx executors

* @nx/eslint:lint
* @nx/webpack:webpack

* Update changelog
2023-12-21 11:44:36 +01:00
4d79df90a7 Feature/support search by asset profile id (#2765)
* Add support to search for an asset profile by id

* Update changelog
2023-12-20 19:24:03 +01:00
aa72d9b730 Feature/improve validation of currency management (#2761)
* Improve validation

* Update changelog
2023-12-20 11:53:40 +01:00
80e899a5d3 Bugfix/reset letter spacing in buttons (#2762)
* Reset letter spacing

* Update changelog
2023-12-19 19:54:20 +01:00
7c33120546 Reimplement redactObject() without cloneDeep() (#2760)
* Reimplement redactObject() without cloneDeep()

* Update changelog
2023-12-19 19:53:37 +01:00
7f3c86038f Sort imports (#2739) 2023-12-18 19:52:57 +01:00
c1446f8559 Feature/set select column to sticky in lazy loaded activities table (#2755)
* Set select column to sticky

* Update changelog
2023-12-17 20:09:56 +01:00
88d5dfe435 Release 2.31.0 (#2757) 2023-12-16 20:01:38 +01:00
7dc8f80fdf Feature/upgrade to nx version 17.2.5 (#2756)
* Upgrade Nx to version 17.2.5

* Update changelog
2023-12-16 20:00:04 +01:00
96f90c7259 Feature/add lazy loaded activities table to import activities dialog (#2754)
* Add lazy-loaded activities table

* Update changelog
2023-12-16 19:23:01 +01:00
a10d9cb6ba Feature/add lazy loaded activities table to account detail dialog (#2752)
* Add lazy-loaded activities table

* Update changelog
2023-12-16 19:04:08 +01:00
4547c5da1d Feature/add lazy loaded activities table to position detail dialog (#2753)
* Add lazy-loaded activities table

* Update changelog
2023-12-16 17:10:19 +01:00
28706d7b26 Refactor order service (#2751) 2023-12-16 17:08:59 +01:00
492bc5e17b Feature/improve font weight in value component (#2747)
* Improve font weight

* Update changelog
2023-12-16 14:41:50 +01:00
6c37737051 Bugfix/fix loading state of lazy loaded activities table component (#2744)
* Fix loading state

* Update changelog
2023-12-16 10:24:04 +01:00
8677d20c2c Feature/upgrade inter to version 4 (#2746)
* Upgrade to Inter 4

* Update changelog
2023-12-16 10:23:08 +01:00
4d905065ad Extend turkish translation (#2750) 2023-12-16 09:23:42 +01:00
5599b41b83 Bugfix/fix edit of activity in lazy loaded activities table (#2749)
* Fix edit

* Update changelog
2023-12-16 09:19:09 +01:00
8d5a60d777 Update turkish locales (#2748)
* Update translation

* Update changelog
2023-12-15 08:15:51 +01:00
695acf4f3f Update turkish translation (#2731)
* Update locales

* Update changelog
2023-12-14 20:04:22 +01:00
67dbef3b7a Release 2.30.0 (#2743) 2023-12-12 19:56:46 +01:00
0e94112dc7 Feature/adjust holdings weight threshold in trackinsight data enhancer (#2742)
* Adjust holdings weight threshold

* Update changelog
2023-12-12 19:54:58 +01:00
b22edff16b Feature/add support for column sorting to lazy loaded activities table (#2738)
* Add support for column sorting

* Update changelog
2023-12-12 19:54:40 +01:00
ffb7cbff50 Feature/highlight all time high in benchmarks of the markets overview (#2740)
* Highlight all time high

* Update changelog
2023-12-11 19:45:30 +01:00
25424ad280 Feature/update prisma to version 5.7.0 (#2737)
* Update prisma to version 5.7.0

* Update changelog
2023-12-10 14:50:32 +01:00
a768902b00 Feature/update prisma to version 5.7.0 (#2734)
* Update prisma to version 5.7.0

* Update changelog
2023-12-09 17:24:25 +01:00
2c7ece50fe Release 2.29.0 (#2733) 2023-12-09 17:14:13 +01:00
51a0ede3e4 Feature/introduce lazy loaded activities table (#2729)
* Introduce lazy-loaded activities table

* Add icon column

* Emit paginator event

* Add pagination logic

* Integrate total items

* Update changelog
2023-12-09 17:12:09 +01:00
531964636b Fix type (#2708) 2023-12-08 19:59:06 +01:00
e461fff1d7 Extend turkish localization (#2730) 2023-12-07 14:13:28 +01:00
4f9a5f0340 Feature/set actions columns of tables to stick at end (#2726)
* Set up stickyEnd in actions columns

* Update changelog
2023-12-06 18:35:03 +01:00
8d80e840b8 Feature/upgrade ngx markdown to version 17.1.1 (#2714)
* Upgrade marked and ngx-markdown

* Update changelog
2023-12-05 18:16:03 +01:00
833982a9de Bugfix/fix biometric authentication registration (#2713)
* Remove token on device registration

* Update changelog
2023-12-05 18:13:35 +01:00
c85966e5ed Improve language localization for tr (#2717)
* Improve language localization for tr

* Update changelog
2023-12-04 21:04:20 +01:00
43f67ba832 Feature/upgrade ng extract i18n merge to version 2.9.0 (#2715)
* Upgrade ng-extract-i18n-merge to version 2.9.0

* Update changelog
2023-12-04 19:54:43 +01:00
cbea8ac9d3 Feature/increase tab height on mobile (#2712)
* Increase tab height on mobile

* Update changelog
2023-12-04 19:53:50 +01:00
d4c939e41d Feature/improve language localization for german 20231202 (#2710)
* Update locales

* Update changelog
2023-12-03 09:26:12 +01:00
c1f129501a Release 2.28.0 (#2709) 2023-12-02 17:20:10 +01:00
377ba75e4c Add support to delete a cash balance (#2707) 2023-12-02 17:17:25 +01:00
77b13b88f0 Relax check for duplicates in activities import (#2704)
* Relax check for duplicates in activities import (allow same day)

* Update changelog
2023-12-02 10:37:03 +01:00
813e73a0a3 Introduce HasPermission annotation (#2693)
* Introduce HasPermission annotation

* Update changelog
2023-12-02 10:21:19 +01:00
1d796a9597 Bugfix/change intraday data gathering to operate synchronously (#2705)
* Change intraday data gathering to operate synchronously

* Update changelog
2023-12-02 10:20:05 +01:00
4eedf64a3c Update OSS Friends (#2701) 2023-12-02 10:00:00 +01:00
ed4dd79c72 Add cash balances table to account detail dialog (#2549)
* Add cash balances table to account detail dialog

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-12-01 21:12:41 +01:00
6f4fd0826c Feature/respect with excluded accounts flag in get account balances (#2697)
* Respect withExcludedAccounts in getAccountBalances()

* Update changelog
2023-12-01 17:22:13 +01:00
8e3a144a37 Fix date (#2699) 2023-11-30 07:43:41 +01:00
07b0a2c40a Add guard (#2696) 2023-11-29 20:10:37 +01:00
c5dc3d4272 Release 2.27.1 (#2698) 2023-11-28 07:53:00 +01:00
73e69273b4 Upgrade Nx to version 17.1.3 (#2694) 2023-11-27 18:47:15 +01:00
e0b74ef418 Release 2.27.0 (#2692) 2023-11-26 21:18:37 +01:00
2b491dc732 Extend performance endpoint by net worth per day (#2574)
* Extend performance endpoint by net worth per day

* Update changelog
2023-11-26 21:17:15 +01:00
79fc22b5ae Change tweet to post (#2684) 2023-11-26 08:30:02 +01:00
0a83bcd697 Feature/improve error log for data source request timeout (#2688)
* Improve error log for timeouts

* Update changelog
2023-11-25 18:50:24 +01:00
52540d460b Fix alignment (#2689) 2023-11-25 18:48:54 +01:00
6ff2e0f952 Introduce base product page (#2687) 2023-11-25 18:48:39 +01:00
b3e72383bc Feature/extract locales 20231125 (#2686)
* Update locales

* Update changelog
2023-11-25 12:46:56 +01:00
bdfba4d509 Upgrade to Nx 17.1 (#2635)
* Upgrade to Nx 17

* Run migrations

* Extend instructions

* Update changelog
2023-11-25 12:31:45 +01:00
8a411b707d Release 2.26.0 (#2685) 2023-11-24 19:17:54 +01:00
e21601202e update ci and add permissions block (#2683) 2023-11-24 10:20:26 +01:00
8f66040df1 Feature/upgrade prisma to version 5.6.0 (#2680)
* Upgrade prisma to version 5.6.0

* Update changelog
2023-11-23 15:16:01 +01:00
5ad248a643 Improve algorithm (#2676) 2023-11-23 15:15:40 +01:00
fa36c42af4 Improve yeekatee comparison data (#2682) 2023-11-23 15:14:59 +01:00
d4ddc781e1 Feature/upgrade yahoo finance2 to version 2.9.0 (#2679)
* Upgrade yahoo-finance2 to version 2.9.0

* Upgdate changelog
2023-11-22 08:11:04 +01:00
386dd56590 Feature/extend personal finance tools pages 20231120 (#2678)
* Improve tags

* Add Compound Planning

* Add Whal

* Add De.Fi

* Add Tiller

* Add Empower
2023-11-22 08:10:42 +01:00
f28b13604a Add as const (#2677) 2023-11-21 20:12:59 +01:00
d827858d0b Release 2.25.1 (#2674) 2023-11-19 16:46:32 +01:00
c758ca4bfa Release 2.25.0 (#2673) 2023-11-19 16:30:36 +01:00
37183a07bd Change black friday to black week (#2672)
* Change black friday to black week

* Change image
2023-11-19 16:28:30 +01:00
fb294fc6e2 Improve wording (#2668) 2023-11-18 11:15:03 +01:00
8898d02442 Bugfix/fix cannot read properties of undefined reading items in get position (#2667)
* Fix "Cannot read properties of undefined (reading 'items')"

* Update changelog
2023-11-18 11:05:05 +01:00
232d30234c Update OSS Friends (#2663) 2023-11-18 09:59:27 +01:00
e2234c4966 Feature/add black friday 2023 blog post (#2664)
* Add blog post: Black Friday 2023

* Update changelog
2023-11-17 20:20:49 +01:00
272a34195b Refactor folder (#2665) 2023-11-17 20:09:19 +01:00
8c25294da7 Feature/upgrade http status codes to version 2.3.0 (#2644)
* Upgrade http-status-codes to version 2.3.0

* Update changelog
2023-11-17 20:08:23 +01:00
6f11627006 Release 2.24.0 (#2661) 2023-11-16 20:29:49 +01:00
215098e418 Feature/improve language localization for german 20231116 (#2660)
* Update locales

* Update changelog
2023-11-16 20:28:09 +01:00
781496383b Bugfix/improve get range query in market data service (#2659)
* Attempt to fix "too many bind variables in prepared statement, expected maximum of 32767"

* Update changelog
2023-11-16 20:22:56 +01:00
f0f304c012 Change tweet to post (#2658) 2023-11-16 20:22:18 +01:00
4bf97c104b Update changelog (#2656) 2023-11-15 21:45:38 +01:00
0b35a3c7a7 Release 2.23.0 (#2655) 2023-11-15 21:22:20 +01:00
1586cd3a59 Feature/change twitter to x (#2654)
* Change Twitter to X

* Update changelog
2023-11-15 21:20:51 +01:00
ae763cbb87 Improve style of sub title (#2652) 2023-11-15 21:11:10 +01:00
aa72287d54 Extend benchmarks in the markets overview by 50-Day and 200-Day trends (#2575)
* Extend benchmarks in the markets overview by 50-Day and 200-Day trends

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-11-15 20:25:16 +01:00
d155ab6f28 Feature/improve data source validation in activities import (#2645)
* Improve data source validation

* Update changelog
2023-11-14 19:15:57 +01:00
913ca71aa5 Feature/upgrade prettier to version 3.1.0 (#2649)
* Upgrade prettier to version 3.1.0

* Update changelog
2023-11-13 20:40:25 +01:00
1ffde2a27e Feature/setup polish (#2650)
* Set up polski

* Update changelog
2023-11-13 20:35:15 +01:00
fcf0cea982 Change BTC to BTCUSD (#2646) 2023-11-13 20:22:47 +01:00
ae1968aadf Feature/extract locales 20231112 (#2643)
* Update locales

* Update changelog
2023-11-12 19:03:11 +01:00
3e6333ef95 Feature/upgrade ng extract i18n merge to version 2.8.3 (#2642)
* Upgrade ng-extract-i18n-merge to version 2.8.3

* Update changelog
2023-11-12 18:28:26 +01:00
c69686651e Release 2.22.0 (#2638) 2023-11-11 18:59:43 +01:00
93b6011ddc Feature/refactor get range in market data service (#2631)
* Refactor to unique asset in getRange()

* Update changelog
2023-11-11 18:57:41 +01:00
f567e25f27 Feature/introduce action menus in overview of action control panel (#2637)
* Introduce action menus

* Exchange rates management
* Coupons management

* Update changelog
2023-11-11 18:48:23 +01:00
5dc538bafb Feature/optimize testimonial carousel style on mobile (#2634)
* Optimize style on mobile

* Update changelog
2023-11-11 17:29:59 +01:00
b4de06fcf0 Feature/add platform icons to account selectors (#2633)
* Add platform icons to account selectors

* Update changelog
2023-11-11 17:27:29 +01:00
27da0eb26e Feature/harmonize name column of historical market data table (#2632)
* Harmonize name column

* Update changelog
2023-11-11 09:02:12 +01:00
8ff80c10e5 Feature/extend personal finance tools pages 20231110 (#2630)
* Add Magnifi

* Add Basil Finance
2023-11-11 09:01:58 +01:00
5db5d5e79a Release 2.21.0 (#2629) 2023-11-09 19:25:14 +01:00
12aac101bd Feature/extend system message (#2628)
* Extend system message

* Update changelog
2023-11-09 19:23:36 +01:00
3a66ccdebe Bugfix/fix unit in overview of home page (#2626)
* Fix unit

* Update changelog
2023-11-09 19:22:15 +01:00
6a722d1bb7 Bugfix/fix get quotes in financial modeling prep service (#2627)
* Fix get quotes

* Update changelog
2023-11-09 18:03:09 +01:00
7c9407d5dc Release 2.20.0 (#2623) 2023-11-08 19:39:38 +01:00
8abb517ac6 Feature/remove loading indicator of unit in overview of home page (#2622)
* Remove loading indicator of unit

* Update changelog
2023-11-08 19:37:45 +01:00
dec1d89c5c Feature/increase timeout in data provider and enhancer health check endpoint (#2621)
* Increase timeout in health check endpoint (data enhancer and provider)

* Update changelog
2023-11-08 18:02:38 +01:00
24e9ecc3e2 Update locales (#2620) 2023-11-08 16:51:44 +01:00
4a1e05b8cd Improve historical market data import (#2581)
* Add form group for historical data import

* Update changelog
2023-11-07 18:16:00 +01:00
39d1a85267 Feature/extend personal finance tools pages 20231107 (#2618)
* Add Monarch Money

* Add YNAB

* Add Allvue Systems
2023-11-07 18:14:54 +01:00
7cb86de7af Feature/remove account type from account database model (#2616)
* Remove account type

* Update changelog
2023-11-07 17:56:18 +01:00
aa078588e8 Release 2.19.0 (#2615) 2023-11-06 18:02:15 +01:00
fcef0a72d5 Bugfix/improve handling of derived currencies (#2604)
* Improve handling of derived currencies

* Update changelog
2023-11-06 17:58:39 +01:00
29987d3e2f Add missing activity types (#2601)
* FEE
* INTEREST
* LIABILITY
2023-11-06 13:09:14 +01:00
6284b4dfe8 Feature/improve localization of fear and greed index (#2612)
* Improve localization

* Update changelog
2023-11-06 13:07:53 +01:00
00342ca1f7 Improve wording (#2603) 2023-11-06 12:15:42 +01:00
234c4fd511 Add CoinGecko (#2611) 2023-11-06 08:33:23 +01:00
669f1fb60c Feature/add database migration to reset account type in account table (#2602)
* Set accountType to NULL

* Update changelog
2023-11-05 18:31:16 +01:00
52df0c62ab Release 2.18.0 (#2600) 2023-11-05 11:53:38 +01:00
e8e1bb83bf Fix get quotes in CoinGecko service (#2595)
* Fix get quotes in CoinGecko service

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-11-05 11:52:09 +01:00
45510702d0 Feature/add hacktoberfest 2023 debriefing blog post (#2599)
* Add blog post: Hacktoberfest 2023 Debriefing

* Update changelog
2023-11-05 11:36:52 +01:00
1b7e3a1e47 Feature/support activities import by isin for yahoo finance (#2597)
* Add support to import activities by isin

* Update changelog
2023-11-05 09:58:57 +01:00
35f98b9d2d Bugfix/handle failing database query for account find many (#2598)
* Handle issue with account.findMany() -> where: { id: { in: [ null ] } }

* Update changelog
2023-11-05 09:57:23 +01:00
e980aed9e7 Reorder functions (#2594) 2023-11-05 08:50:43 +01:00
d993067e9a Feature/extend personal finance tools pages 20231104 2 (#2591)
* Add Vyzer

* Add FinWise
2023-11-05 08:50:27 +01:00
3d09bfdb0c Feature/upgrade angular to version 16.2.12 (#2590)
* Add upgrade guides for Angular

* Upgrade Angular dependencies to version 16.2.12

* Update changelog
2023-11-04 11:59:49 +01:00
3fbc4f500f Add empty columns (#2589) 2023-11-04 10:39:36 +01:00
373201a98f Add major version to docker tags (#2586)
* Add major version to docker tags

* Update changelog
2023-11-04 10:38:35 +01:00
681f88f002 Clean up import (#2492) 2023-11-04 10:19:15 +01:00
8a523a981a Bugfix/fix fees on account level (#2588)
* Fix fees on account level

* Update changelog
2023-11-04 10:17:58 +01:00
81ded53363 Center membership card (#2582) 2023-11-04 10:17:35 +01:00
5272407af8 Feature/extend personal finance tools pages 20231104 (#2587)
* Introduce alias

* Add Rocket Money

* Add 8FIGURES
2023-11-04 10:17:17 +01:00
c48f89d117 Add empty columns (#2583) 2023-11-04 10:11:58 +01:00
046fdd3ae7 Release 2.17.0 (#2579) 2023-11-02 19:36:59 +01:00
e69c7a753c Feature/add edit exchange rate button to admin control (#2577)
* Ad edit button

* Update changelog
2023-11-02 19:35:03 +01:00
5191415b5a Add Intuit Mint (#2578) 2023-11-02 19:34:42 +01:00
a704378702 Refactor interface of getQuotes() to object (#2570) 2023-11-01 13:55:48 +01:00
cf7ce64de7 Bugfix/improve alignment of menu item icons (#2566)
* Improve alignment

* Update changelog
2023-10-31 14:01:33 +01:00
8c1b45f35b Bugfix/fix exception in webauthn page (#2564)
* Remove useBrowserAutofill option in startAuthentication()

* Update changelog
2023-10-30 19:23:55 +01:00
6ad1528d01 Feature/improve language localization for german 20231029 (#2565)
* Update locales

* Update changelog
2023-10-29 19:33:42 +01:00
4a6fbe4d30 Release 2.16.0 (#2563) 2023-10-29 08:59:00 +01:00
e31741f0c7 Add Capitally (#2562) 2023-10-29 08:57:33 +01:00
b26aa7f51d Feature/improve duplicate check in activities import (#2561)
* Allow different accounts

* Update changelog
2023-10-29 08:40:42 +01:00
c0fccd186f Feature/upgrade prisma to version 5.5.2 (#2560)
* Upgrade prisma to version 5.5.2

* Update changelog
2023-10-29 08:20:55 +01:00
a7baad10d1 Feature/improve import of historical market data (#2559)
* Improve historical market data import

* Update changelog
2023-10-28 21:08:44 +02:00
16f1b16e41 Feature/change checkboxes to slide toggles in admin control panel (#2551)
* Change checkboxes to slide toggles

* Update changelog
2023-10-28 20:46:44 +02:00
409ddc90ce Feature/improve language localization for german 20231028 (#2556)
* Improve localization

* Update changelog
2023-10-28 20:46:13 +02:00
95bc84956e Feature/localize keywords of meta data (#2555)
* Localize keywords

* Update changelog
2023-10-28 15:39:01 +02:00
20cefaba19 Improve usability and validation in cash balance transfer (#2552)
* Improve usability and validation in cash balance transfer

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-28 15:09:01 +02:00
379c651ce0 Add translation for product slogan (#2554) 2023-10-28 15:07:15 +02:00
7804c6879d Clean up (#2543) 2023-10-28 11:24:50 +02:00
877 changed files with 101690 additions and 85863 deletions

25
.env.dev Normal file
View File

@ -0,0 +1,25 @@
COMPOSE_PROJECT_NAME=ghostfolio-development
# CACHE
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=<INSERT_REDIS_PASSWORD>
# POSTGRES
POSTGRES_DB=ghostfolio-db
POSTGRES_USER=user
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
# VARIOUS
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
# DEVELOPMENT
# Nx 18 enables using plugins to infer targets by default
# This is disabled for existing workspaces to maintain compatibility
# For more info, see: https://nx.dev/concepts/inferred-tasks
NX_ADD_PLUGINS=false
NX_NATIVE_COMMAND_RUNNER=false

View File

@ -1,4 +1,4 @@
COMPOSE_PROJECT_NAME=ghostfolio-development
COMPOSE_PROJECT_NAME=ghostfolio
# CACHE
REDIS_HOST=localhost
@ -10,6 +10,7 @@ POSTGRES_DB=ghostfolio-db
POSTGRES_USER=user
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
# VARIOUS
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>

View File

@ -24,12 +24,18 @@
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {}
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {}
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.ts"],

View File

@ -6,7 +6,13 @@ labels: ''
assignees: ''
---
The Issue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) community or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions).
**Important Notice**
The issue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) community or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions).
Incomplete or non-reproducible issues may be closed, but we are here to help! If you encounter difficulties reproducing the bug or need assistance, please reach out to our community channels mentioned above.
Thank you for your understanding and cooperation!
**Bug Description**
@ -36,8 +42,9 @@ The Issue tracker is **ONLY** used for reporting bugs. New features should be di
<!-- Please complete the following information -->
- Cloud or Self-hosted
- Ghostfolio Version X.Y.Z
- Cloud or Self-hosted
- Experimental Features enabled or disabled
- Browser
- OS

View File

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

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker metadata
id: meta
@ -21,6 +21,7 @@ jobs:
with:
images: ghostfolio/ghostfolio
tags: |
type=semver,pattern={{major}}
type=semver,pattern={{version}}
- name: Set up QEMU

5
.gitignore vendored
View File

@ -5,8 +5,8 @@
/tmp
# dependencies
/.yarn
/node_modules
npm-debug.log
# IDEs and editors
/.idea
@ -28,15 +28,14 @@
.env
.env.prod
.nx/cache
.nx/workspace-data
/.sass-cache
/connect.lock
/coverage
/dist
/libpeerconnection.log
npm-debug.log
testem.log
/typings
yarn-error.log
# System Files
.DS_Store

2
.nvmrc
View File

@ -1 +1 @@
v18
v20

View File

@ -1,3 +1,5 @@
/.nx/cache
/.nx/workspace-data
/apps/client/src/polyfills.ts
/dist
/test/import

View File

@ -9,7 +9,26 @@
],
"attributeSort": "ASC",
"endOfLine": "auto",
"plugins": ["prettier-plugin-organize-attributes"],
"importOrder": ["^@ghostfolio/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
},
{
"files": "*.ts",
"options": {
"importOrderParserPlugins": ["decorators-legacy", "typescript"]
}
}
],
"plugins": [
"prettier-plugin-organize-attributes",
"@trivago/prettier-plugin-sort-imports"
],
"printWidth": 80,
"singleQuote": true,
"tabWidth": 2,

View File

@ -1 +0,0 @@
network-timeout 600000

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ Remove permission in `UserService` using `without()`
### Frontend
Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
Use `@if (user?.settings?.isExperimentalFeatures) {}` in HTML template
## Git
@ -20,30 +20,36 @@ Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
## Dependencies
### Angular
#### Upgrade (minor versions)
1. Run `npx npm-check-updates --upgrade --target "minor" --filter "/@angular.*/"`
### Nx
#### Upgrade
1. Run `yarn nx migrate latest`
1. Make sure `package.json` changes make sense and then run `yarn install`
1. Run `yarn nx migrate --run-migrations`
1. Run `npx nx migrate latest`
1. Make sure `package.json` changes make sense and then run `npm install`
1. Run `npx nx migrate --run-migrations`
### Prisma
#### Access database via GUI
Run `yarn database:gui`
Run `npm run database:gui`
https://www.prisma.io/studio
#### Synchronize schema with database for prototyping
Run `yarn database:push`
Run `npm run 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 `npm run prisma migrate dev --name added_job_title`
https://www.prisma.io/docs/concepts/components/prisma-migrate#getting-started-with-prisma-migrate

View File

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:18-slim as builder
FROM --platform=$BUILDPLATFORM node:20-slim AS builder
# Build application and add additional files
WORKDIR /ghostfolio
@ -8,18 +8,17 @@ WORKDIR /ghostfolio
COPY ./CHANGELOG.md CHANGELOG.md
COPY ./LICENSE LICENSE
COPY ./package.json package.json
COPY ./yarn.lock yarn.lock
COPY ./.yarnrc .yarnrc
COPY ./package-lock.json package-lock.json
COPY ./prisma/schema.prisma prisma/schema.prisma
RUN apt update && apt install -y \
git \
g++ \
make \
openssl \
python3 \
&& rm -rf /var/lib/apt/lists/*
RUN yarn install
RUN apt-get update && apt-get install -y --no-install-suggests \
g++ \
git \
make \
openssl \
python3 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install
# See https://github.com/nrwl/nx/issues/6586 for further details
COPY ./decorate-angular-cli.js decorate-angular-cli.js
@ -33,29 +32,36 @@ COPY ./tsconfig.base.json tsconfig.base.json
COPY ./libs libs
COPY ./apps apps
RUN yarn build:production
RUN npm run build:production
# Prepare the dist image with additional node_modules
WORKDIR /ghostfolio/dist/apps/api
# package.json was generated by the build process, however the original
# yarn.lock needs to be used to ensure the same versions
COPY ./yarn.lock /ghostfolio/dist/apps/api/yarn.lock
# package-lock.json needs to be used to ensure the same versions
COPY ./package-lock.json /ghostfolio/dist/apps/api/package-lock.json
RUN yarn
RUN npm install
COPY prisma /ghostfolio/dist/apps/api/prisma
# Overwrite the generated package.json with the original one to ensure having
# all the scripts
COPY package.json /ghostfolio/dist/apps/api
RUN yarn database:generate-typings
RUN npm run database:generate-typings
# Image to run, copy everything needed from builder
FROM node:18-slim
RUN apt update && apt install -y \
openssl \
&& rm -rf /var/lib/apt/lists/*
FROM node:20-slim
LABEL org.opencontainers.image.source="https://github.com/ghostfolio/ghostfolio"
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y --no-install-suggests \
curl \
openssl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
COPY ./docker/entrypoint.sh /ghostfolio/entrypoint.sh
RUN chown -R node:node /ghostfolio
WORKDIR /ghostfolio/apps/api
EXPOSE ${PORT:-3333}
CMD [ "yarn", "start:production" ]
USER node
CMD [ "/ghostfolio/entrypoint.sh" ]

106
README.md
View File

@ -7,14 +7,12 @@
**Open Source Wealth Management Software**
[**Ghostfol.io**](https://ghostfol.io) | [**Live Demo**](https://ghostfol.io/en/demo) | [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) | [**FAQ**](https://ghostfol.io/en/faq) |
[**Blog**](https://ghostfol.io/en/blog) | [**Slack**](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) | [**Twitter**](https://twitter.com/ghostfolio_)
[**Blog**](https://ghostfol.io/en/blog) | [**Slack**](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) | [**X**](https://x.com/ghostfolio_)
[![Shield: Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-Support-yellow?logo=buymeacoffee)](https://www.buymeacoffee.com/ghostfolio)
[![Shield: Contributions Welcome](https://img.shields.io/badge/Contributions-Welcome-orange.svg)](#contributing)
[![Shield: License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2)
</div>
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
@ -49,7 +47,7 @@ Ghostfolio is for you if you are...
- ✅ Create, update and delete transactions
- ✅ Multi account management
- ✅ Portfolio performance for `Today`, `YTD`, `1Y`, `5Y`, `Max`
- ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `WTD`, `MTD`, `YTD`, `1Y`, `5Y`, `Max`
- ✅ Various charts
- ✅ Static analysis to identify potential risks in your portfolio
- ✅ Import and export transactions
@ -87,19 +85,23 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
### Supported Environment Variables
| Name | Default Value | Description |
| ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | | A random string used as salt for access tokens |
| `DATABASE_URL` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
| `HOST` | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | | A random string used for _JSON Web Tokens_ (JWT) |
| `PORT` | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
| `REDIS_HOST` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | | The password of _Redis_ |
| `REDIS_PORT` | | The port where _Redis_ is running |
| Name | Type | Default Value | Description |
| ------------------------ | ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens |
| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key |
| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key |
| `DATABASE_URL` | `string` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database |
| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ |
| `REDIS_HOST` | `string` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | `string` | | The password of _Redis_ |
| `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds |
### Run with Docker Compose
@ -115,7 +117,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
Run the following command to start the Docker images from [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio):
```bash
docker-compose --env-file ./.env -f docker/docker-compose.yml up -d
docker compose --env-file ./.env -f docker/docker-compose.yml up -d
```
#### b. Build and run environment
@ -123,8 +125,8 @@ docker-compose --env-file ./.env -f docker/docker-compose.yml up -d
Run the following commands to build and start the Docker images:
```bash
docker-compose --env-file ./.env -f docker/docker-compose.build.yml build
docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
docker compose --env-file ./.env -f docker/docker-compose.build.yml build
docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
```
#### Setup
@ -135,61 +137,61 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
#### Upgrade Version
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
1. Run the following command to start the new Docker image: `docker compose --env-file ./.env -f docker/docker-compose.yml up -d`
At each start, the container will automatically apply the database schema migrations if needed.
### Home Server Systems (Community)
Ghostfolio is available for various home server systems, including [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Home Assistant](https://github.com/lildude/ha-addon-ghostfolio), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
## Development
### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version 18+)
- [Yarn](https://yarnpkg.com/en/docs/install)
- [Node.js](https://nodejs.org/en/download) (version 20+)
- Create a local copy of this Git repository (clone)
- Copy the file `.env.example` to `.env` and populate it with your data (`cp .env.example .env`)
- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`)
### Setup
1. Run `yarn install`
1. Run `docker-compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
1. Run `yarn database:setup` to initialize the database schema
1. Run `npm install`
1. Run `docker compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
1. Run `npm run database:setup` to initialize the database schema
1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks
1. Start the server and the client (see [_Development_](#Development))
1. Open http://localhost:4200/en in your browser
1. Open https://localhost:4200/en in your browser
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
### Start Server
#### Debug
Run `yarn watch:server` and click _Launch Program_ in [Visual Studio Code](https://code.visualstudio.com)
Run `npm run watch:server` and click _Debug API_ in [Visual Studio Code](https://code.visualstudio.com)
#### Serve
Run `yarn start:server`
Run `npm run start:server`
### Start Client
Run `yarn start:client` and open http://localhost:4200/en in your browser
Run `npm run start:client` and open https://localhost:4200/en in your browser
### Start _Storybook_
Run `yarn start:storybook`
Run `npm run start:storybook`
### Migrate Database
With the following command you can keep your database schema in sync:
```bash
yarn database:push
npm run database:push
```
## Testing
Run `yarn test`
Run `npm test`
## Public API
@ -201,7 +203,7 @@ Set the header for each request as follows:
"Authorization": "Bearer eyJh..."
```
You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ accessToken: <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>`.
@ -230,18 +232,18 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
}
```
| Field | Type | Description |
| ---------- | ------------------- | -------------------------------------------------- |
| accountId | string (`optional`) | Id of the account |
| comment | string (`optional`) | Comment of the activity |
| currency | string | `CHF` \| `EUR` \| `USD` etc. |
| dataSource | string | `MANUAL` (for type `ITEM`) \| `YAHOO` |
| date | string | Date in the format `ISO-8601` |
| fee | number | Fee of the activity |
| quantity | number | Quantity of the activity |
| symbol | string | Symbol of the activity (suitable for `dataSource`) |
| type | string | `BUY` \| `DIVIDEND` \| `ITEM` \| `SELL` |
| unitPrice | number | Price per unit of the activity |
| Field | Type | Description |
| ------------ | ------------------- | ----------------------------------------------------------------------------- |
| `accountId` | `string` (optional) | Id of the account |
| `comment` | `string` (optional) | Comment of the activity |
| `currency` | `string` | `CHF` \| `EUR` \| `USD` etc. |
| `dataSource` | `string` | `COINGECKO` \| `MANUAL` (for type `ITEM`) \| `YAHOO` |
| `date` | `string` | Date in the format `ISO-8601` |
| `fee` | `number` | Fee of the activity |
| `quantity` | `number` | Quantity of the activity |
| `symbol` | `string` | Symbol of the activity (suitable for `dataSource`) |
| `type` | `string` | `BUY` \| `DIVIDEND` \| `FEE` \| `INTEREST` \| `ITEM` \| `LIABILITY` \| `SELL` |
| `unitPrice` | `number` | Price per unit of the activity |
#### Response
@ -272,12 +274,16 @@ Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ r
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you.
Not sure what to work on? We have [some ideas](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22), even for [newcomers](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://x.com/ghostfolio_) on _X_. We would love to hear from you.
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
## Analytics
![Alt](https://repobeats.axiom.co/api/embed/281a80b2d0c4af1162866c24c803f1f18e5ed60e.svg 'Repobeats analytics image')
## License
© 2021 - 2023 [Ghostfolio](https://ghostfol.io)
© 2021 - 2024 [Ghostfolio](https://ghostfol.io)
Licensed under the [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.html).

View File

@ -13,7 +13,6 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/api',
testTimeout: 10000,
testEnvironment: 'node',
preset: '../../jest.preset.js'
};

View File

@ -7,14 +7,16 @@
"generators": {},
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"executor": "@nx/webpack:webpack",
"options": {
"outputPath": "dist/apps/api",
"compiler": "tsc",
"deleteOutputPath": false,
"main": "apps/api/src/main.ts",
"tsConfig": "apps/api/tsconfig.app.json",
"assets": ["apps/api/src/assets"],
"outputPath": "dist/apps/api",
"sourceMap": true,
"target": "node",
"compiler": "tsc"
"tsConfig": "apps/api/tsconfig.app.json",
"webpackConfig": "apps/api/webpack.config.js"
},
"configurations": {
"production": {
@ -32,6 +34,26 @@
},
"outputs": ["{options.outputPath}"]
},
"copy-assets": {
"executor": "nx:run-commands",
"options": {
"commands": [
{
"command": "shx rm -rf dist/apps/api"
},
{
"command": "shx mkdir -p dist/apps/api/assets/locales"
},
{
"command": "shx cp -r apps/api/src/assets/* dist/apps/api/assets"
},
{
"command": "shx cp -r apps/client/src/locales/* dist/apps/api/assets/locales"
}
],
"parallel": false
}
},
"serve": {
"executor": "@nx/js:node",
"options": {
@ -39,7 +61,7 @@
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"executor": "@nx/eslint:lint",
"options": {
"lintFilePatterns": ["apps/api/**/*.ts"]
}
@ -47,8 +69,7 @@
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/api/jest.config.ts",
"passWithNoTests": true
"jestConfig": "apps/api/jest.config.ts"
},
"outputs": ["{workspaceRoot}/coverage/apps/api"]
}

View File

@ -1,6 +1,10 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { Access } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -17,7 +21,6 @@ import { AuthGuard } from '@nestjs/passport';
import { Access as AccessModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccessModule } from './access.module';
import { AccessService } from './access.service';
import { CreateAccessDto } from './create-access.dto';
@ -25,11 +28,12 @@ import { CreateAccessDto } from './create-access.dto';
export class AccessController {
public constructor(
private readonly accessService: AccessService,
private readonly configurationService: ConfigurationService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getAllAccesses(): Promise<Access[]> {
const accessesWithGranteeUser = await this.accessService.accesses({
include: {
@ -39,32 +43,38 @@ export class AccessController {
where: { userId: this.request.user.id }
});
return accessesWithGranteeUser.map((access) => {
if (access.GranteeUser) {
return accessesWithGranteeUser.map(
({ alias, GranteeUser, id, permissions }) => {
if (GranteeUser) {
return {
alias,
id,
permissions,
grantee: GranteeUser?.id,
type: 'PRIVATE'
};
}
return {
alias: access.alias,
grantee: access.GranteeUser?.id,
id: access.id,
type: 'RESTRICTED_VIEW'
alias,
id,
permissions,
grantee: 'Public',
type: 'PUBLIC'
};
}
return {
alias: access.alias,
grantee: 'Public',
id: access.id,
type: 'PUBLIC'
};
});
);
}
@HasPermission(permissions.createAccess)
@Post()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createAccess(
@Body() data: CreateAccessDto
): Promise<AccessModel> {
if (
!hasPermission(this.request.user.permissions, permissions.createAccess)
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -72,25 +82,30 @@ export class AccessController {
);
}
return this.accessService.createAccess({
alias: data.alias || undefined,
GranteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } }
: undefined,
User: { connect: { id: this.request.user.id } }
});
try {
return this.accessService.createAccess({
alias: data.alias || undefined,
GranteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } }
: undefined,
permissions: data.permissions,
User: { connect: { id: this.request.user.id } }
});
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
public async deleteAccess(@Param('id') id: string): Promise<AccessModule> {
@HasPermission(permissions.deleteAccess)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> {
const access = await this.accessService.access({ id });
if (
!hasPermission(this.request.user.permissions, permissions.deleteAccess) ||
!access ||
access.userId !== this.request.user.id
) {
if (!access || access.userId !== this.request.user.id) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN

View File

@ -1,4 +1,6 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { AccessController } from './access.controller';
@ -7,7 +9,7 @@ import { AccessService } from './access.service';
@Module({
controllers: [AccessController],
exports: [AccessService],
imports: [PrismaModule],
imports: [ConfigurationModule, PrismaModule],
providers: [AccessService]
})
export class AccessModule {}

View File

@ -1,5 +1,6 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { AccessWithGranteeUser } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { Access, Prisma } from '@prisma/client';

View File

@ -1,4 +1,5 @@
import { IsOptional, IsString } from 'class-validator';
import { AccessPermission } from '@prisma/client';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateAccessDto {
@IsOptional()
@ -6,10 +7,10 @@ export class CreateAccessDto {
alias?: string;
@IsOptional()
@IsString()
@IsUUID()
granteeUserId?: string;
@IsEnum(AccessPermission, { each: true })
@IsOptional()
@IsString()
type?: 'PUBLIC';
permissions?: AccessPermission[];
}

View File

@ -0,0 +1,84 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
Body,
Post,
Delete,
HttpException,
Inject,
Param,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { AccountBalance } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccountBalanceService } from './account-balance.service';
import { CreateAccountBalanceDto } from './create-account-balance.dto';
@Controller('account-balance')
export class AccountBalanceController {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@HasPermission(permissions.createAccountBalance)
@Post()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createAccountBalance(
@Body() data: CreateAccountBalanceDto
): Promise<AccountBalance> {
const account = await this.accountService.account({
id_userId: {
id: data.accountId,
userId: this.request.user.id
}
});
if (!account) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.accountBalanceService.createOrUpdateAccountBalance({
accountId: account.id,
balance: data.balance,
date: data.date,
userId: account.userId
});
}
@HasPermission(permissions.deleteAccountBalance)
@Delete(':id')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccountBalance(
@Param('id') id: string
): Promise<AccountBalance> {
const accountBalance = await this.accountBalanceService.accountBalance({
id,
userId: this.request.user.id
});
if (!accountBalance) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.accountBalanceService.deleteAccountBalance({
id: accountBalance.id,
userId: accountBalance.userId
});
}
}

View File

@ -0,0 +1,16 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { AccountBalanceController } from './account-balance.controller';
import { AccountBalanceService } from './account-balance.service';
@Module({
controllers: [AccountBalanceController],
exports: [AccountBalanceService],
imports: [ExchangeRateDataModule, PrismaModule],
providers: [AccountBalanceService, AccountService]
})
export class AccountBalanceModule {}

View File

@ -0,0 +1,142 @@
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { resetHours } from '@ghostfolio/common/helper';
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AccountBalance, Prisma } from '@prisma/client';
import { parseISO } from 'date-fns';
import { CreateAccountBalanceDto } from './create-account-balance.dto';
@Injectable()
export class AccountBalanceService {
public constructor(
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
public async accountBalance(
accountBalanceWhereInput: Prisma.AccountBalanceWhereInput
): Promise<AccountBalance | null> {
return this.prismaService.accountBalance.findFirst({
include: {
Account: true
},
where: accountBalanceWhereInput
});
}
public async createOrUpdateAccountBalance({
accountId,
balance,
date,
userId
}: CreateAccountBalanceDto & {
userId: string;
}): Promise<AccountBalance> {
const accountBalance = await this.prismaService.accountBalance.upsert({
create: {
Account: {
connect: {
id_userId: {
userId,
id: accountId
}
}
},
date: resetHours(parseISO(date)),
value: balance
},
update: {
value: balance
},
where: {
accountId_date: {
accountId,
date: resetHours(parseISO(date))
}
}
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId
})
);
return accountBalance;
}
public async deleteAccountBalance(
where: Prisma.AccountBalanceWhereUniqueInput
): Promise<AccountBalance> {
const accountBalance = await this.prismaService.accountBalance.delete({
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: <string>where.userId
})
);
return accountBalance;
}
public async getAccountBalances({
filters,
user,
withExcludedAccounts
}: {
filters?: Filter[];
user: UserWithSettings;
withExcludedAccounts?: boolean;
}): Promise<AccountBalancesResponse> {
const where: Prisma.AccountBalanceWhereInput = { userId: user.id };
const accountFilter = filters?.find(({ type }) => {
return type === 'ACCOUNT';
});
if (accountFilter) {
where.accountId = accountFilter.id;
}
if (withExcludedAccounts === false) {
where.Account = { isExcluded: false };
}
const balances = await this.prismaService.accountBalance.findMany({
where,
orderBy: {
date: 'asc'
},
select: {
Account: true,
date: true,
id: true,
value: true
}
});
return {
balances: balances.map((balance) => {
return {
...balance,
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
balance.value,
balance.Account.currency,
user.Settings.settings.baseCurrency
)
};
})
};
}
}

View File

@ -0,0 +1,12 @@
import { IsISO8601, IsNumber, IsUUID } from 'class-validator';
export class CreateAccountBalanceDto {
@IsUUID()
accountId: string;
@IsNumber()
balance: number;
@IsISO8601()
date: string;
}

View File

@ -1,17 +1,20 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import {
AccountBalancesResponse,
Accounts
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { permissions } from '@ghostfolio/common/permissions';
import type {
AccountWithValue,
RequestWithUser
} from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -47,17 +50,9 @@ export class AccountController {
) {}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.deleteAccount)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccount(@Param('id') id: string): Promise<AccountModel> {
if (
!hasPermission(this.request.user.permissions, permissions.deleteAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const account = await this.accountService.accountWithOrders(
{
id_userId: {
@ -68,7 +63,7 @@ export class AccountController {
{ Order: true }
);
if (account?.isDefault || account?.Order.length > 0) {
if (!account || account?.Order.length > 0) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
@ -87,7 +82,7 @@ export class AccountController {
}
@Get()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAllAccounts(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId
@ -102,7 +97,7 @@ export class AccountController {
}
@Get(':id')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@ -122,31 +117,23 @@ export class AccountController {
}
@Get(':id/balances')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountBalancesById(
@Param('id') id: string
): Promise<AccountBalancesResponse> {
return this.accountBalanceService.getAccountBalances({
accountId: id,
userId: this.request.user.id
filters: [{ id, type: 'ACCOUNT' }],
user: this.request.user
});
}
@HasPermission(permissions.createAccount)
@Post()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createAccount(
@Body() data: CreateAccountDto
): Promise<AccountModel> {
if (
!hasPermission(this.request.user.permissions, permissions.createAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
if (data.platformId) {
const platformId = data.platformId;
delete data.platformId;
@ -172,70 +159,64 @@ export class AccountController {
}
}
@HasPermission(permissions.updateAccount)
@Post('transfer-balance')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async transferAccountBalance(
@Body() { accountIdFrom, accountIdTo, balance }: TransferBalanceDto
) {
if (
!hasPermission(this.request.user.permissions, permissions.updateAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const accountsOfUser = await this.accountService.getAccounts(
this.request.user.id
);
const currentAccountIds = accountsOfUser.map(({ id }) => {
return id;
const accountFrom = accountsOfUser.find(({ id }) => {
return id === accountIdFrom;
});
if (
![accountIdFrom, accountIdTo].every((accountId) => {
return currentAccountIds.includes(accountId);
})
) {
const accountTo = accountsOfUser.find(({ id }) => {
return id === accountIdTo;
});
if (!accountFrom || !accountTo) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
const { currency } = accountsOfUser.find(({ id }) => {
return id === accountIdFrom;
});
if (accountFrom.id === accountTo.id) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
if (accountFrom.balance < balance) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
await this.accountService.updateAccountBalance({
currency,
accountId: accountIdFrom,
accountId: accountFrom.id,
amount: -balance,
currency: accountFrom.currency,
userId: this.request.user.id
});
await this.accountService.updateAccountBalance({
currency,
accountId: accountIdTo,
accountId: accountTo.id,
amount: balance,
currency: accountFrom.currency,
userId: this.request.user.id
});
}
@HasPermission(permissions.updateAccount)
@Put(':id')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) {
if (
!hasPermission(this.request.user.permissions, permissions.updateAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const originalAccount = await this.accountService.account({
id_userId: {
id,

View File

@ -1,12 +1,11 @@
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { AccountBalanceModule } from '@ghostfolio/api/services/account-balance/account-balance.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.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 { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { AccountController } from './account.controller';
@ -18,13 +17,11 @@ import { AccountService } from './account.service';
imports: [
AccountBalanceModule,
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule,
PortfolioModule,
PrismaModule,
RedisCacheModule,
UserModule
RedactValuesInResponseModule
],
providers: [AccountService]
})

View File

@ -1,10 +1,15 @@
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Account, Order, Platform, Prisma } from '@prisma/client';
import Big from 'big.js';
import { Big } from 'big.js';
import { format } from 'date-fns';
import { groupBy } from 'lodash';
import { CashDetails } from './interfaces/cash-details.interface';
@ -13,6 +18,7 @@ import { CashDetails } from './interfaces/cash-details.interface';
export class AccountService {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
@ -20,10 +26,8 @@ export class AccountService {
public async account({
id_userId
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
const { id, userId } = id_userId;
const [account] = await this.accounts({
where: { id, userId }
where: id_userId
});
return account;
@ -86,17 +90,20 @@ export class AccountService {
data
});
await this.prismaService.accountBalance.create({
data: {
Account: {
connect: {
id_userId: { id: account.id, userId: aUserId }
}
},
value: data.balance
}
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId: account.id,
balance: data.balance,
date: format(new Date(), DATE_FORMAT),
userId: aUserId
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
@ -104,9 +111,18 @@ export class AccountService {
where: Prisma.AccountWhereUniqueInput,
aUserId: string
): Promise<Account> {
return this.prismaService.account.delete({
const account = await this.prismaService.account.delete({
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
public async getAccounts(aUserId: string): Promise<Account[]> {
@ -158,8 +174,8 @@ export class AccountService {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, (filter) => {
return filter.type;
} = groupBy(filters, ({ type }) => {
return type;
});
if (filtersByAccount?.length > 0) {
@ -197,21 +213,26 @@ export class AccountService {
): Promise<Account> {
const { data, where } = params;
await this.prismaService.accountBalance.create({
data: {
Account: {
connect: {
id_userId: where.id_userId
}
},
value: <number>data.balance
}
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId: <string>data.id,
balance: <number>data.balance,
date: format(new Date(), DATE_FORMAT),
userId: aUserId
});
return this.prismaService.account.update({
const account = await this.prismaService.account.update({
data,
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
public async updateAccountBalance({
@ -243,17 +264,11 @@ export class AccountService {
);
if (amountInCurrencyOfAccount) {
await this.accountBalanceService.createAccountBalance({
date,
Account: {
connect: {
id_userId: {
userId,
id: accountId
}
}
},
value: new Big(balance).plus(amountInCurrencyOfAccount).toNumber()
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId,
userId,
balance: new Big(balance).plus(amountInCurrencyOfAccount).toNumber(),
date: date.toISOString()
});
}
}

View File

@ -1,4 +1,5 @@
import { AccountType } from '@prisma/client';
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsBoolean,
@ -10,10 +11,6 @@ import {
import { isString } from 'lodash';
export class CreateAccountDto {
@IsOptional()
@IsString()
accountType?: AccountType;
@IsNumber()
balance: number;
@ -24,7 +21,7 @@ export class CreateAccountDto {
)
comment?: string;
@IsString()
@IsCurrencyCode()
currency: string;
@IsOptional()

View File

@ -1,4 +1,5 @@
import { AccountType } from '@prisma/client';
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsBoolean,
@ -10,10 +11,6 @@ import {
import { isString } from 'lodash';
export class UpdateAccountDto {
@IsOptional()
@IsString()
accountType?: AccountType;
@IsNumber()
balance: number;
@ -24,7 +21,7 @@ export class UpdateAccountDto {
)
comment?: string;
@IsString()
@IsCurrencyCode()
currency: string;
@IsString()

View File

@ -1,9 +1,14 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
DATA_GATHERING_QUEUE_PRIORITY_MEDIUM,
GATHER_ASSET_PROFILE_PROCESS,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config';
@ -14,11 +19,12 @@ import {
AdminMarketDataDetails,
EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { permissions } from '@ghostfolio/common/permissions';
import type {
MarketDataPreset,
RequestWithUser
} from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -26,6 +32,7 @@ import {
Get,
HttpException,
Inject,
Logger,
Param,
Patch,
Post,
@ -51,65 +58,34 @@ export class AdminController {
private readonly adminService: AdminService,
private readonly apiService: ApiService,
private readonly dataGatheringService: DataGatheringService,
private readonly manualService: ManualService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get()
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getAdminData(): Promise<AdminData> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.get();
}
@HasPermission(permissions.accessAdminControl)
@Post('gather')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gather7Days(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
this.dataGatheringService.gather7Days();
}
@HasPermission(permissions.accessAdminControl)
@Post('gather/max')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherMax(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
const assetProfileIdentifiers =
await this.dataGatheringService.getAllAssetProfileIdentifiers();
await this.dataGatheringService.addJobsToQueue(
uniqueAssets.map(({ dataSource, symbol }) => {
assetProfileIdentifiers.map(({ dataSource, symbol }) => {
return {
data: {
dataSource,
@ -118,7 +94,8 @@ export class AdminController {
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: getAssetProfileIdentifier({ dataSource, symbol })
jobId: getAssetProfileIdentifier({ dataSource, symbol }),
priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
}
};
})
@ -127,25 +104,15 @@ export class AdminController {
this.dataGatheringService.gatherMax();
}
@HasPermission(permissions.accessAdminControl)
@Post('gather/profile-data')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherProfileData(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
const assetProfileIdentifiers =
await this.dataGatheringService.getAllAssetProfileIdentifiers();
await this.dataGatheringService.addJobsToQueue(
uniqueAssets.map(({ dataSource, symbol }) => {
assetProfileIdentifiers.map(({ dataSource, symbol }) => {
return {
data: {
dataSource,
@ -154,31 +121,21 @@ export class AdminController {
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: getAssetProfileIdentifier({ dataSource, symbol })
jobId: getAssetProfileIdentifier({ dataSource, symbol }),
priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
}
};
})
);
}
@HasPermission(permissions.accessAdminControl)
@Post('gather/profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherProfileDataForSymbol(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
await this.dataGatheringService.addJobToQueue({
data: {
dataSource,
@ -187,53 +144,32 @@ export class AdminController {
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: getAssetProfileIdentifier({ dataSource, symbol })
jobId: getAssetProfileIdentifier({ dataSource, symbol }),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
}
});
}
@Post('gather/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@HasPermission(permissions.accessAdminControl)
public async gatherSymbol(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
this.dataGatheringService.gatherSymbol({ dataSource, symbol });
return;
}
@HasPermission(permissions.accessAdminControl)
@Post('gather/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async gatherSymbolForDate(
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<MarketData> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const date = parseISO(dateString);
if (!isDate(date)) {
@ -251,7 +187,8 @@ export class AdminController {
}
@Get('market-data')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getMarketData(
@Query('assetSubClasses') filterByAssetSubClasses?: string,
@Query('presetId') presetId?: MarketDataPreset,
@ -261,18 +198,6 @@ export class AdminController {
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
@Query('take') take?: number
): Promise<AdminMarketData> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAssetSubClasses,
filterBySearchQuery
@ -289,51 +214,53 @@ export class AdminController {
}
@Get('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
}
@HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol/test')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async testMarketData(
@Body() data: { scraperConfiguration: string },
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<{ price: number }> {
try {
const scraperConfiguration = JSON.parse(data.scraperConfiguration);
const price = await this.manualService.test(scraperConfiguration);
if (price) {
return { price };
}
throw new Error('Could not parse the current market price');
} catch (error) {
Logger.error(error);
throw new HttpException(error.message, StatusCodes.BAD_REQUEST);
}
}
@HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateMarketData(
@Body() data: UpdateBulkMarketDataDto,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map(
({ date, marketPrice }) => ({
dataSource,
date,
marketPrice,
symbol,
date: parseISO(date),
state: 'CLOSE'
})
);
@ -346,26 +273,15 @@ export class AdminController {
/**
* @deprecated
*/
@HasPermission(permissions.accessAdminControl)
@Put('market-data/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async update(
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string,
@Body() data: UpdateMarketDataDto
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const date = parseISO(dateString);
return this.marketDataService.updateMarketData({
@ -380,24 +296,14 @@ export class AdminController {
});
}
@HasPermission(permissions.accessAdminControl)
@Post('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async addProfileData(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<SymbolProfile | never> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.addAssetProfile({
dataSource,
symbol,
@ -406,45 +312,23 @@ export class AdminController {
}
@Delete('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteProfileData(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.deleteProfileData({ dataSource, symbol });
}
@HasPermission(permissions.accessAdminControl)
@Patch('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async patchAssetProfileData(
@Body() assetProfileData: UpdateAssetProfileDto,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<EnhancedSymbolProfile> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.patchAssetProfileData({
...assetProfileData,
dataSource,
@ -452,24 +336,13 @@ export class AdminController {
});
}
@HasPermission(permissions.accessAdminControl)
@Put('settings/:key')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateProperty(
@Param('key') key: string,
@Body() data: PropertyDto
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return await this.adminService.putSetting(key, data.value);
return this.adminService.putSetting(key, data.value);
}
}

View File

@ -1,4 +1,7 @@
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
@ -8,6 +11,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
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 { AdminController } from './admin.controller';
@ -17,16 +21,19 @@ import { QueueModule } from './queue/queue.module';
@Module({
imports: [
ApiModule,
BenchmarkModule,
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
MarketDataModule,
OrderModule,
PrismaModule,
PropertyModule,
QueueModule,
SubscriptionModule,
SymbolProfileModule
SymbolProfileModule,
TransformDataSourceInRequestModule
],
controllers: [AdminController],
providers: [AdminService],

View File

@ -1,3 +1,5 @@
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
@ -13,27 +15,40 @@ import {
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED
} from '@ghostfolio/common/config';
import { isCurrency, getCurrencyFromSymbol } from '@ghostfolio/common/helper';
import {
AdminData,
AdminMarketData,
AdminMarketDataDetails,
AdminMarketDataItem,
EnhancedSymbolProfile,
Filter,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types';
import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import {
AssetClass,
AssetSubClass,
DataSource,
Prisma,
PrismaClient,
Property,
SymbolProfile
} from '@prisma/client';
import { differenceInDays } from 'date-fns';
import { groupBy } from 'lodash';
@Injectable()
export class AdminService {
public constructor(
private readonly benchmarkService: BenchmarkService,
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly orderService: OrderService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService,
@ -64,7 +79,7 @@ export class AdminService {
);
}
return await this.symbolProfileService.add(
return this.symbolProfileService.add(
assetProfiles[symbol] as Prisma.SymbolProfileCreateInput
);
} catch (error) {
@ -94,9 +109,17 @@ export class AdminService {
return currency !== DEFAULT_CURRENCY;
})
.map((currency) => {
const label1 = DEFAULT_CURRENCY;
const label2 = currency;
return {
label1: DEFAULT_CURRENCY,
label2: currency,
label1,
label2,
dataSource:
DataSource[
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
],
symbol: `${label1}${label2}`,
value: this.exchangeRateDataService.toCurrency(
1,
DEFAULT_CURRENCY,
@ -131,7 +154,16 @@ export class AdminService {
[{ symbol: 'asc' }];
const where: Prisma.SymbolProfileWhereInput = {};
if (presetId === 'CURRENCIES') {
if (presetId === 'BENCHMARKS') {
const benchmarkAssetProfiles =
await this.benchmarkService.getBenchmarkAssetProfiles();
where.id = {
in: benchmarkAssetProfiles.map(({ id }) => {
return id;
})
};
} else if (presetId === 'CURRENCIES') {
return this.getMarketDataForCurrencies();
} else if (
presetId === 'ETF_WITHOUT_COUNTRIES' ||
@ -162,6 +194,7 @@ export class AdminService {
if (searchQuery) {
where.OR = [
{ id: { mode: 'insensitive', startsWith: searchQuery } },
{ isin: { mode: 'insensitive', startsWith: searchQuery } },
{ name: { mode: 'insensitive', startsWith: searchQuery } },
{ symbol: { mode: 'insensitive', startsWith: searchQuery } }
@ -180,101 +213,129 @@ export class AdminService {
}
}
let [assetProfiles, count] = await Promise.all([
this.prismaService.symbolProfile.findMany({
orderBy,
skip,
take,
where,
select: {
_count: {
select: { Order: true }
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
name: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
scraperConfiguration: true,
sectors: true,
symbol: true
const extendedPrismaClient = this.getExtendedPrismaClient();
try {
let [assetProfiles, count] = await Promise.all([
extendedPrismaClient.symbolProfile.findMany({
orderBy,
skip,
take,
where,
select: {
_count: {
select: { Order: true }
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
id: true,
isUsedByUsersWithSubscription: true,
name: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
scraperConfiguration: true,
sectors: true,
symbol: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
let marketData: AdminMarketDataItem[] = await Promise.all(
assetProfiles.map(
async ({
_count,
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
id,
isUsedByUsersWithSubscription,
name,
Order,
sectors,
symbol
}) => {
const countriesCount = countries
? Object.keys(countries).length
: 0;
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
return {
assetClass,
assetSubClass,
comment,
currency,
countriesCount,
dataSource,
id,
name,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.Order,
date: Order?.[0]?.date,
isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription
};
}
)
);
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
}
}),
this.prismaService.symbolProfile.count({ where })
]);
let marketData = assetProfiles.map(
({
_count,
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
name,
Order,
sectors,
symbol
}) => {
const countriesCount = countries ? Object.keys(countries).length : 0;
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
return {
assetClass,
assetSubClass,
comment,
currency,
countriesCount,
dataSource,
name,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.Order,
date: Order?.[0]?.date
};
}
);
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
count = marketData.length;
}
count = marketData.length;
return {
count,
marketData
};
} finally {
await extendedPrismaClient.$disconnect();
Logger.debug('Disconnect extended prisma client', 'AdminService');
}
return {
count,
marketData
};
}
public async getMarketDataBySymbol({
dataSource,
symbol
}: UniqueAsset): Promise<AdminMarketDataDetails> {
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
let currency: EnhancedSymbolProfile['currency'] = '-';
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
if (isCurrency(getCurrencyFromSymbol(symbol))) {
currency = getCurrencyFromSymbol(symbol);
({ activitiesCount, dateOfFirstActivity } =
await this.orderService.getStatisticsByCurrency(currency));
}
const [[assetProfile], marketData] = await Promise.all([
this.symbolProfileService.getSymbolProfiles([
{
@ -293,11 +354,20 @@ export class AdminService {
})
]);
if (assetProfile) {
assetProfile.dataProviderInfo = this.dataProviderService
.getDataProvider(assetProfile.dataSource)
.getDataProviderInfo();
}
return {
marketData,
assetProfile: assetProfile ?? {
symbol,
currency: '-'
activitiesCount,
currency,
dataSource,
dateOfFirstActivity,
symbol
}
};
}
@ -306,22 +376,48 @@ export class AdminService {
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
holdings,
name,
scraperConfiguration,
sectors,
symbol,
symbolMapping
symbolMapping,
url
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
await this.symbolProfileService.updateSymbolProfile({
assetClass,
assetSubClass,
comment,
dataSource,
name,
scraperConfiguration,
symbol,
symbolMapping
});
const symbolProfileOverrides = {
assetClass: assetClass as AssetClass,
assetSubClass: assetSubClass as AssetSubClass,
name: name as string,
url: url as string
};
const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput & UniqueAsset =
{
comment,
countries,
currency,
dataSource,
holdings,
scraperConfiguration,
sectors,
symbol,
symbolMapping,
...(dataSource === 'MANUAL'
? { assetClass, assetSubClass, name, url }
: {
SymbolProfileOverrides: {
upsert: {
create: symbolProfileOverrides,
update: symbolProfileOverrides
}
}
})
};
await this.symbolProfileService.updateSymbolProfile(updatedSymbolProfile);
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
{
@ -351,35 +447,97 @@ export class AdminService {
return response;
}
private getExtendedPrismaClient() {
Logger.debug('Connect extended prisma client', 'AdminService');
const symbolProfileExtension = Prisma.defineExtension((client) => {
return client.$extends({
result: {
symbolProfile: {
isUsedByUsersWithSubscription: {
compute: async ({ id }) => {
const { _count } =
await this.prismaService.symbolProfile.findUnique({
select: {
_count: {
select: {
Order: {
where: {
User: {
Subscription: {
some: {
expiresAt: {
gt: new Date()
}
}
}
}
}
}
}
}
},
where: {
id
}
});
return _count.Order > 0;
}
}
}
}
});
});
return new PrismaClient().$extends(symbolProfileExtension);
}
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
const marketDataItems = await this.prismaService.marketData.groupBy({
_count: true,
by: ['dataSource', 'symbol']
});
const marketData: AdminMarketDataItem[] = this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
const marketDataPromise: Promise<AdminMarketDataItem>[] =
this.exchangeRateDataService
.getCurrencyPairs()
.map(async ({ dataSource, symbol }) => {
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
let currency: EnhancedSymbolProfile['currency'] = '-';
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
return {
dataSource,
marketDataItemCount,
symbol,
assetClass: 'CASH',
countriesCount: 0,
currency: symbol.replace(DEFAULT_CURRENCY, ''),
name: symbol,
sectorsCount: 0
};
});
if (isCurrency(getCurrencyFromSymbol(symbol))) {
currency = getCurrencyFromSymbol(symbol);
({ activitiesCount, dateOfFirstActivity } =
await this.orderService.getStatisticsByCurrency(currency));
}
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
return {
activitiesCount,
currency,
dataSource,
marketDataItemCount,
symbol,
assetClass: AssetClass.LIQUIDITY,
assetSubClass: AssetSubClass.CASH,
countriesCount: 0,
date: dateOfFirstActivity,
id: undefined,
name: symbol,
sectorsCount: 0
};
});
const marketData = await Promise.all(marketDataPromise);
return { marketData, count: marketData.length };
}
@ -418,13 +576,14 @@ export class AdminService {
},
createdAt: true,
id: true,
role: true,
Subscription: true
},
take: 30
});
return usersWithAnalytics.map(
({ _count, Analytics, createdAt, id, Subscription }) => {
({ _count, Analytics, createdAt, id, role, Subscription }) => {
const daysSinceRegistration =
differenceInDays(new Date(), createdAt) + 1;
const engagement = Analytics
@ -434,13 +593,17 @@ export class AdminService {
const subscription = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
)
? this.subscriptionService.getSubscription(Subscription)
? this.subscriptionService.getSubscription({
createdAt,
subscriptions: Subscription
})
: undefined;
return {
createdAt,
engagement,
id,
role,
subscription,
accountCount: _count.Account || 0,
country: Analytics?.country,

View File

@ -1,87 +1,56 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AdminJobs } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { permissions } from '@ghostfolio/common/permissions';
import {
Controller,
Delete,
Get,
HttpException,
Inject,
Param,
Query,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { JobStatus } from 'bull';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { QueueService } from './queue.service';
@Controller('admin/queue')
export class QueueController {
public constructor(
private readonly queueService: QueueService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
public constructor(private readonly queueService: QueueService) {}
@Delete('job')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteJobs(
@Query('status') filterByStatus?: string
): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined;
return this.queueService.deleteJobs({ status });
}
@Get('job')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getJobs(
@Query('status') filterByStatus?: string
): Promise<AdminJobs> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined;
return this.queueService.getJobs({ status });
}
@Delete('job/:id')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteJob(@Param('id') id: string): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.queueService.deleteJob(id);
}
@Get('job/:id/execute')
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async executeJob(@Param('id') id: string): Promise<void> {
return this.queueService.executeJob(id);
}
}

View File

@ -1,4 +1,5 @@
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { Module } from '@nestjs/common';
import { QueueController } from './queue.controller';

View File

@ -3,6 +3,7 @@ import {
QUEUE_JOB_STATUS_LIST
} from '@ghostfolio/common/config';
import { AdminJobs } from '@ghostfolio/common/interfaces';
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { JobStatus, Queue } from 'bull';
@ -31,6 +32,10 @@ export class QueueService {
}
}
public async executeJob(aId: string) {
return (await this.dataGatheringQueue.getJob(aId))?.promote();
}
public async getJobs({
limit = 1000,
status = QUEUE_JOB_STATUS_LIST
@ -53,6 +58,7 @@ export class QueueService {
finishedOn: job.finishedOn,
id: job.id,
name: job.name,
opts: job.opts,
stacktrace: job.stacktrace,
state: await job.getState(),
timestamp: job.timestamp

View File

@ -1,5 +1,14 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
import { IsEnum, IsObject, IsOptional, IsString } from 'class-validator';
import {
IsArray,
IsEnum,
IsObject,
IsOptional,
IsString,
IsUrl
} from 'class-validator';
export class UpdateAssetProfileDto {
@IsEnum(AssetClass, { each: true })
@ -14,6 +23,14 @@ export class UpdateAssetProfileDto {
@IsOptional()
comment?: string;
@IsArray()
@IsOptional()
countries?: Prisma.InputJsonArray;
@IsCurrencyCode()
@IsOptional()
currency?: string;
@IsString()
@IsOptional()
name?: string;
@ -22,9 +39,20 @@ export class UpdateAssetProfileDto {
@IsOptional()
scraperConfiguration?: Prisma.InputJsonObject;
@IsArray()
@IsOptional()
sectors?: Prisma.InputJsonArray;
@IsObject()
@IsOptional()
symbolMapping?: {
[dataProvider: string]: string;
};
@IsOptional()
@IsUrl({
protocols: ['https'],
require_protocol: true
})
url?: string;
}

View File

@ -1,9 +1,9 @@
import { IsDate, IsNumber, IsOptional } from 'class-validator';
import { IsISO8601, IsNumber, IsOptional } from 'class-validator';
export class UpdateMarketDataDto {
@IsDate()
@IsISO8601()
@IsOptional()
date?: Date;
date?: string;
@IsNumber()
marketPrice: number;

View File

@ -1,4 +1,5 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { Controller } from '@nestjs/common';
@Controller()

View File

@ -1,27 +1,31 @@
import { join } from 'path';
import { EventsModule } from '@ghostfolio/api/events/events.module';
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 { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import {
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { StatusCodes } from 'http-status-codes';
import { join } from 'path';
import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module';
import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller';
import { AssetModule } from './asset/asset.module';
import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module';
import { BenchmarkModule } from './benchmark/benchmark.module';
@ -43,15 +47,18 @@ import { TagModule } from './tag/tag.module';
import { UserModule } from './user/user.module';
@Module({
controllers: [AppController],
imports: [
AdminModule,
AccessModule,
AccountModule,
AssetModule,
AuthDeviceModule,
AuthModule,
BenchmarkModule,
BullModule.forRoot({
redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
password: process.env.REDIS_PASSWORD
@ -62,6 +69,8 @@ import { UserModule } from './user/user.module';
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
EventEmitterModule.forRoot(),
EventsModule,
ExchangeRateModule,
ExchangeRateDataModule,
ExportModule,
@ -73,6 +82,7 @@ import { UserModule } from './user/user.module';
PlatformModule,
PortfolioModule,
PrismaModule,
PropertyModule,
RedisCacheModule,
ScheduleModule.forRoot(),
ServeStaticModule.forRoot({
@ -106,7 +116,6 @@ import { UserModule } from './user/user.module';
TwitterBotModule,
UserModule
],
controllers: [AppController],
providers: [CronService]
})
export class AppModule {}

View File

@ -0,0 +1,29 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { pick } from 'lodash';
@Controller('asset')
export class AssetController {
public constructor(private readonly adminService: AdminService) {}
@Get(':dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAsset(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
const { assetProfile, marketData } =
await this.adminService.getMarketDataBySymbol({ dataSource, symbol });
return {
marketData,
assetProfile: pick(assetProfile, ['dataSource', 'name', 'symbol'])
};
}
}

View File

@ -0,0 +1,17 @@
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { Module } from '@nestjs/common';
import { AssetController } from './asset.controller';
@Module({
controllers: [AssetController],
imports: [
AdminModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
]
})
export class AssetModule {}

View File

@ -1,40 +1,19 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
Delete,
HttpException,
Inject,
Param,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions';
import { Controller, Delete, Param, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Controller('auth-device')
export class AuthDeviceController {
public constructor(
private readonly authDeviceService: AuthDeviceService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
public constructor(private readonly authDeviceService: AuthDeviceService) {}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.deleteAuthDevice)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAuthDevice(@Param('id') id: string): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.deleteAuthDevice
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
await this.authDeviceService.deleteAuthDevice({ id });
}
}

View File

@ -1,14 +1,13 @@
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@Module({
controllers: [AuthDeviceController],
imports: [
ConfigurationModule,
JwtModule.register({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' }

View File

@ -1,14 +1,11 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { AuthDevice, Prisma } from '@prisma/client';
@Injectable()
export class AuthDeviceService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService
) {}
public constructor(private readonly prismaService: PrismaService) {}
public async authDevice(
where: Prisma.AuthDeviceWhereUniqueInput

View File

@ -1,7 +1,9 @@
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { OAuthResponse } from '@ghostfolio/common/interfaces';
import {
Body,
Controller,
@ -118,13 +120,13 @@ export class AuthController {
}
@Get('webauthn/generate-registration-options')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async generateRegistrationOptions() {
return this.webAuthService.generateRegistrationOptions();
}
@Post('webauthn/verify-attestation')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async verifyAttestation(
@Body() body: { deviceName: string; credential: AttestationCredentialJSON }
) {

View File

@ -5,6 +5,7 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';

View File

@ -1,6 +1,7 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Provider } from '@prisma/client';

View File

@ -1,8 +1,9 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client';
import { Strategy } from 'passport-google-oauth20';
import { Profile, Strategy } from 'passport-google-oauth20';
import { AuthService } from './auth.service';
@ -10,7 +11,7 @@ import { AuthService } from './auth.service';
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
public constructor(
private readonly authService: AuthService,
readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService
) {
super({
callbackURL: `${configurationService.get(
@ -19,7 +20,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
clientID: configurationService.get('GOOGLE_CLIENT_ID'),
clientSecret: configurationService.get('GOOGLE_SECRET'),
passReqToCallback: true,
scope: ['email', 'profile']
scope: ['profile']
});
}
@ -27,20 +28,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
request: any,
token: string,
refreshToken: string,
profile,
profile: Profile,
done: Function,
done2: Function
) {
try {
const jwt: string = await this.authService.validateOAuthLogin({
const jwt = await this.authService.validateOAuthLogin({
provider: Provider.GOOGLE,
thirdPartyId: profile.id
});
const user = {
jwt
};
done(null, user);
done(null, { jwt });
} catch (error) {
Logger.error(error, 'GoogleStrategy');
done(error, false);

View File

@ -1,4 +1,5 @@
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
import { Provider } from '@prisma/client';
export interface AuthDeviceDialogParams {

View File

@ -2,9 +2,12 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { hasRole } from '@ghostfolio/common/permissions';
import { HttpException, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import * as countriesAndTimezones from 'countries-and-timezones';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
@ -28,6 +31,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
if (user) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (hasRole(user, 'INACTIVE')) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
const country =
countriesAndTimezones.getCountryForTimezone(timezone)?.id;
@ -44,10 +54,20 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
return user;
} else {
throw '';
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
} catch (error) {
if (error?.getStatus() === StatusCodes.TOO_MANY_REQUESTS) {
throw error;
} else {
throw new HttpException(
getReasonPhrase(StatusCodes.UNAUTHORIZED),
StatusCodes.UNAUTHORIZED
);
}
} catch (err) {
throw new UnauthorizedException('unauthorized', err.message);
}
}
}

View File

@ -3,6 +3,7 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.s
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Inject,
Injectable,
@ -40,7 +41,7 @@ export class WebAuthService {
) {}
get rpID() {
return this.configurationService.get('WEB_AUTH_RP_ID');
return new URL(this.configurationService.get('ROOT_URL')).hostname;
}
get expectedOrigin() {

View File

@ -1,12 +1,16 @@
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 { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type {
BenchmarkMarketDataDetails,
BenchmarkResponse,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -16,6 +20,7 @@ import {
Inject,
Param,
Post,
Query,
UseGuards,
UseInterceptors
} from '@nestjs/common';
@ -33,21 +38,10 @@ export class BenchmarkController {
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@HasPermission(permissions.accessAdminControl)
@Post()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
try {
const benchmark = await this.benchmarkService.addBenchmark({
dataSource,
@ -71,23 +65,12 @@ export class BenchmarkController {
}
@Delete(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteBenchmark(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
try {
const benchmark = await this.benchmarkService.deleteBenchmark({
dataSource,
@ -120,19 +103,26 @@ export class BenchmarkController {
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataBySymbol(
public async getBenchmarkMarketDataForUser(
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string
@Param('symbol') symbol: string,
@Query('range') dateRange: DateRange = 'max'
): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString);
const { endDate, startDate } = getInterval(
dateRange,
new Date(startDateString)
);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
return this.benchmarkService.getMarketDataBySymbol({
return this.benchmarkService.getMarketDataForUser({
dataSource,
endDate,
startDate,
symbol
symbol,
userCurrency
});
}
}

View File

@ -1,11 +1,14 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.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 { 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 { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
import { BenchmarkController } from './benchmark.controller';
@ -15,14 +18,16 @@ import { BenchmarkService } from './benchmark.service';
controllers: [BenchmarkController],
exports: [BenchmarkService],
imports: [
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
MarketDataModule,
PrismaModule,
PropertyModule,
RedisCacheModule,
SymbolModule,
SymbolProfileModule
SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
providers: [BenchmarkService]
})

View File

@ -11,6 +11,7 @@ describe('BenchmarkService', () => {
null,
null,
null,
null,
null
);
});

View File

@ -1,6 +1,7 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -9,18 +10,32 @@ import {
MAX_CHART_ITEMS,
PROPERTY_BENCHMARKS
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
DATE_FORMAT,
calculateBenchmarkTrend,
parseDate,
resetHours
} from '@ghostfolio/common/helper';
import {
Benchmark,
BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { BenchmarkTrend } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import Big from 'big.js';
import { format } from 'date-fns';
import { uniqBy } from 'lodash';
import { Big } from 'big.js';
import {
differenceInDays,
eachDayOfInterval,
format,
isSameDay,
subDays
} from 'date-fns';
import { isNumber, last, uniqBy } from 'lodash';
import ms from 'ms';
@Injectable()
@ -29,6 +44,7 @@ export class BenchmarkService {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
@ -45,9 +61,34 @@ export class BenchmarkService {
return 0;
}
public async getBenchmarks({ useCache = true } = {}): Promise<
BenchmarkResponse['benchmarks']
> {
public async getBenchmarkTrends({ dataSource, symbol }: UniqueAsset) {
const historicalData = await this.marketDataService.marketDataItems({
orderBy: {
date: 'desc'
},
where: {
dataSource,
symbol,
date: { gte: subDays(new Date(), 400) }
}
});
const fiftyDayAverage = calculateBenchmarkTrend({
historicalData,
days: 50
});
const twoHundredDayAverage = calculateBenchmarkTrend({
historicalData,
days: 200
});
return { trend50d: fiftyDayAverage, trend200d: twoHundredDayAverage };
}
public async getBenchmarks({
enableSharing = false,
useCache = true
} = {}): Promise<BenchmarkResponse['benchmarks']> {
let benchmarks: BenchmarkResponse['benchmarks'];
if (useCache) {
@ -62,22 +103,39 @@ export class BenchmarkService {
} catch {}
}
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles();
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({
enableSharing
});
const promises: Promise<{ date: Date; marketPrice: number }>[] = [];
const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] =
[];
const promisesBenchmarkTrends: Promise<{
trend50d: BenchmarkTrend;
trend200d: BenchmarkTrend;
}>[] = [];
const quotes = await this.dataProviderService.getQuotes({
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
})
}),
requestTimeout: ms('30 seconds'),
useCache: false
});
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
promisesAllTimeHighs.push(
this.marketDataService.getMax({ dataSource, symbol })
);
promisesBenchmarkTrends.push(
this.getBenchmarkTrends({ dataSource, symbol })
);
}
const allTimeHighs = await Promise.all(promises);
let storeInCache = true;
const [allTimeHighs, benchmarkTrends] = await Promise.all([
Promise.all(promisesAllTimeHighs),
Promise.all(promisesBenchmarkTrends)
]);
let storeInCache = useCache;
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
const { marketPrice } =
@ -93,17 +151,25 @@ export class BenchmarkService {
} else {
storeInCache = false;
}
return {
dataSource: benchmarkAssetProfiles[index].dataSource,
marketCondition: this.getMarketCondition(
performancePercentFromAllTimeHigh
),
name: benchmarkAssetProfiles[index].name,
performances: {
allTimeHigh: {
date: allTimeHigh.date,
performancePercent: performancePercentFromAllTimeHigh
date: allTimeHigh?.date,
performancePercent:
performancePercentFromAllTimeHigh >= 0
? 0
: performancePercentFromAllTimeHigh
}
}
},
symbol: benchmarkAssetProfiles[index].symbol,
trend50d: benchmarkTrends[index].trend50d,
trend200d: benchmarkTrends[index].trend200d
};
});
@ -111,21 +177,31 @@ export class BenchmarkService {
await this.redisCacheService.set(
this.CACHE_KEY_BENCHMARKS,
JSON.stringify(benchmarks),
ms('4 hours') / 1000
ms('2 hours') / 1000
);
}
return benchmarks;
}
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
public async getBenchmarkAssetProfiles({
enableSharing = false
} = {}): Promise<Partial<SymbolProfile>[]> {
const symbolProfileIds: string[] = (
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? []
).map(({ symbolProfileId }) => {
return symbolProfileId;
});
)
.filter((benchmark) => {
if (enableSharing) {
return benchmark.enableSharing;
}
return true;
})
.map(({ symbolProfileId }) => {
return symbolProfileId;
});
const assetProfiles =
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);
@ -142,11 +218,30 @@ export class BenchmarkService {
.sort((a, b) => a.name.localeCompare(b.name));
}
public async getMarketDataBySymbol({
public async getMarketDataForUser({
dataSource,
endDate = new Date(),
startDate,
symbol
}: { startDate: Date } & UniqueAsset): Promise<BenchmarkMarketDataDetails> {
symbol,
userCurrency
}: {
endDate?: Date;
startDate: Date;
userCurrency: string;
} & UniqueAsset): Promise<BenchmarkMarketDataDetails> {
const marketData: { date: string; value: number }[] = [];
const days = differenceInDays(endDate, startDate) + 1;
const dates = eachDayOfInterval(
{
start: startDate,
end: endDate
},
{ step: Math.round(days / Math.min(days, MAX_CHART_ITEMS)) }
).map((date) => {
return resetHours(date);
});
const [currentSymbolItem, marketDataItems] = await Promise.all([
this.symbolService.get({
dataGatheringItem: {
@ -162,50 +257,92 @@ export class BenchmarkService {
dataSource,
symbol,
date: {
gte: startDate
in: dates
}
}
})
]);
const step = Math.round(
marketDataItems.length / Math.min(marketDataItems.length, MAX_CHART_ITEMS)
const exchangeRates =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
startDate,
currencies: [currentSymbolItem.currency],
targetCurrency: userCurrency
});
const exchangeRateAtStartDate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(startDate, DATE_FORMAT)
];
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
return isSameDay(date, startDate);
})?.marketPrice;
if (!marketPriceAtStartDate) {
Logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) at ${format(
startDate,
DATE_FORMAT
)}`,
'BenchmarkService'
);
return { marketData };
}
for (let marketDataItem of marketDataItems) {
const exchangeRate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(marketDataItem.date, DATE_FORMAT)
];
const exchangeRateFactor =
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
? exchangeRate / exchangeRateAtStartDate
: 1;
marketData.push({
date: format(marketDataItem.date, DATE_FORMAT),
value:
marketPriceAtStartDate === 0
? 0
: this.calculateChangeInPercentage(
marketPriceAtStartDate,
marketDataItem.marketPrice * exchangeRateFactor
) * 100
});
}
const includesEndDate = isSameDay(
parseDate(last(marketData).date),
endDate
);
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
const response = {
marketData: [
...marketDataItems
.filter((marketDataItem, index) => {
return index % step === 0;
})
.map((marketDataItem) => {
return {
date: format(marketDataItem.date, DATE_FORMAT),
value:
marketPriceAtStartDate === 0
? 0
: this.calculateChangeInPercentage(
marketPriceAtStartDate,
marketDataItem.marketPrice
) * 100
};
})
]
};
if (currentSymbolItem?.marketPrice && !includesEndDate) {
const exchangeRate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(endDate, DATE_FORMAT)
];
if (currentSymbolItem?.marketPrice) {
response.marketData.push({
date: format(new Date(), DATE_FORMAT),
const exchangeRateFactor =
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
? exchangeRate / exchangeRateAtStartDate
: 1;
marketData.push({
date: format(endDate, DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
currentSymbolItem.marketPrice * exchangeRateFactor
) * 100
});
}
return response;
return {
marketData
};
}
public async addBenchmark({
@ -282,7 +419,15 @@ export class BenchmarkService {
};
}
private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
private getMarketCondition(
aPerformanceInPercent: number
): Benchmark['marketCondition'] {
if (aPerformanceInPercent >= 0) {
return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) {
return 'BEAR_MARKET';
} else {
return 'NEUTRAL_MARKET';
}
}
}

View File

@ -1,39 +1,19 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
HttpException,
Inject,
Post,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions';
import { Controller, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Controller('cache')
export class CacheController {
public constructor(
private readonly redisCacheService: RedisCacheService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
public constructor(private readonly redisCacheService: RedisCacheService) {}
@HasPermission(permissions.accessAdminControl)
@Post('flush')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async flushCache(): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.redisCacheService.reset();
}
}

View File

@ -1,24 +1,11 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
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 { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
import { CacheController } from './cache.controller';
@Module({
controllers: [CacheController],
imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule
]
imports: [RedisCacheModule]
})
export class CacheModule {}

View File

@ -1,4 +1,6 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import {
Controller,
Get,
@ -19,7 +21,7 @@ export class ExchangeRateController {
) {}
@Get(':symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getExchangeRate(
@Param('dateString') dateString: string,
@Param('symbol') symbol: string

View File

@ -1,4 +1,5 @@
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { Module } from '@nestjs/common';
import { ExchangeRateController } from './exchange-rate.controller';

View File

@ -1,4 +1,5 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { Injectable } from '@nestjs/common';
@Injectable()

View File

@ -1,5 +1,8 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { Export } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
@ -9,17 +12,29 @@ import { ExportService } from './export.service';
@Controller('export')
export class ExportController {
public constructor(
private readonly apiService: ApiService,
private readonly exportService: ExportService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async export(
@Query('activityIds') activityIds?: string[]
@Query('accounts') filterByAccounts?: string,
@Query('activityIds') activityIds?: string[],
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise<Export> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
return this.exportService.export({
activityIds,
filters,
userCurrency: this.request.user.Settings.settings.baseCurrency,
userId: this.request.user.id
});
}

View File

@ -1,23 +1,14 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { Module } from '@nestjs/common';
import { ExportController } from './export.controller';
import { ExportService } from './export.service';
@Module({
imports: [
AccountModule,
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
OrderModule,
RedisCacheModule
],
imports: [AccountModule, ApiModule, OrderModule],
controllers: [ExportController],
providers: [ExportService]
})

View File

@ -1,7 +1,8 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment';
import { Export } from '@ghostfolio/common/interfaces';
import { Filter, Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
@Injectable()
@ -13,9 +14,13 @@ export class ExportService {
public async export({
activityIds,
filters,
userCurrency,
userId
}: {
activityIds?: string[];
filters?: Filter[];
userCurrency: string;
userId: string;
}): Promise<Export> {
const accounts = (
@ -39,10 +44,14 @@ export class ExportService {
}
);
let activities = await this.orderService.orders({
include: { SymbolProfile: true },
orderBy: { date: 'desc' },
where: { userId }
let { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId,
includeDrafts: true,
sortColumn: 'date',
sortDirection: 'asc',
withExcludedAccounts: true
});
if (activityIds) {
@ -86,7 +95,10 @@ export class ExportService {
: SymbolProfile.symbol
};
}
)
),
user: {
settings: { currency: userCurrency }
}
};
}
}

View File

@ -1,4 +1,5 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import {
Controller,
Get,

View File

@ -1,6 +1,7 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.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 { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@ -8,7 +9,11 @@ import { HealthService } from './health.service';
@Module({
controllers: [HealthController],
imports: [ConfigurationModule, DataEnhancerModule, DataProviderModule],
imports: [
DataEnhancerModule,
DataProviderModule,
TransformDataSourceInRequestModule
],
providers: [HealthService]
})
export class HealthModule {}

View File

@ -1,5 +1,6 @@
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';

View File

@ -1,5 +1,6 @@
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Type } from 'class-transformer';
import { IsArray, IsOptional, ValidateNested } from 'class-validator';

View File

@ -1,9 +1,12 @@
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 { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImportResponse } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -34,19 +37,18 @@ export class ImportController {
) {}
@Post()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@HasPermission(permissions.createOrder)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async import(
@Body() importData: ImportDataDto,
@Query('dryRun') isDryRun?: boolean
@Query('dryRun') isDryRunParam = 'false'
): Promise<ImportResponse> {
const isDryRun = isDryRunParam === 'true';
if (
!hasPermission(
this.request.user.permissions,
permissions.createAccount
) ||
!hasPermission(this.request.user.permissions, permissions.createOrder)
!hasPermission(this.request.user.permissions, permissions.createAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -65,16 +67,13 @@ export class ImportController {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
}
const userCurrency = this.request.user.Settings.settings.baseCurrency;
try {
const activities = await this.importService.import({
isDryRun,
maxActivitiesToImport,
userCurrency,
accountsDto: importData.accounts ?? [],
activitiesDto: importData.activities,
userId: this.request.user.id
user: this.request.user
});
return { activities };
@ -92,7 +91,7 @@ export class ImportController {
}
@Get('dividends/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async gatherDividends(

View File

@ -4,12 +4,15 @@ import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
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 { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
import { ImportController } from './import.controller';
@ -29,7 +32,9 @@ import { ImportService } from './import.service';
PortfolioModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule
SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
providers: [ImportService]
})

View File

@ -8,10 +8,12 @@ import {
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
@ -20,12 +22,14 @@ import {
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import {
AccountWithPlatform,
OrderWithAccount
OrderWithAccount,
UserWithSettings
} from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import Big from 'big.js';
import { endOfToday, format, isAfter, isSameDay, parseISO } from 'date-fns';
import { Big } from 'big.js';
import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns';
import { uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
@ -33,6 +37,7 @@ import { v4 as uuidv4 } from 'uuid';
export class ImportService {
public constructor(
private readonly accountService: AccountService,
private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
@ -67,9 +72,13 @@ export class ImportService {
})
]);
const accounts = orders.map((order) => {
return order.Account;
});
const accounts = orders
.filter(({ Account }) => {
return !!Account;
})
.map(({ Account }) => {
return Account;
});
const Account = this.isUniqueAccount(accounts) ? accounts[0] : undefined;
@ -81,11 +90,13 @@ export class ImportService {
const value = new Big(quantity).mul(marketPrice).toNumber();
const date = parseDate(dateString);
const isDuplicate = orders.some((activity) => {
return (
activity.accountId === Account?.id &&
activity.SymbolProfile.currency === assetProfile.currency &&
activity.SymbolProfile.dataSource === assetProfile.dataSource &&
isSameDay(activity.date, parseDate(dateString)) &&
isSameSecond(activity.date, date) &&
activity.quantity === quantity &&
activity.SymbolProfile.symbol === assetProfile.symbol &&
activity.type === 'DIVIDEND' &&
@ -99,19 +110,20 @@ export class ImportService {
return {
Account,
date,
error,
quantity,
value,
accountId: Account?.id,
accountUserId: undefined,
comment: undefined,
currency: undefined,
createdAt: undefined,
date: parseDate(dateString),
fee: 0,
feeInBaseCurrency: 0,
id: assetProfile.id,
isDraft: false,
SymbolProfile: <SymbolProfile>(<unknown>assetProfile),
SymbolProfile: assetProfile,
symbolProfileId: assetProfile.id,
type: 'DIVIDEND',
unitPrice: marketPrice,
@ -134,17 +146,16 @@ export class ImportService {
activitiesDto,
isDryRun = false,
maxActivitiesToImport,
userCurrency,
userId
user
}: {
accountsDto: Partial<CreateAccountDto>[];
activitiesDto: Partial<CreateOrderDto>[];
isDryRun?: boolean;
maxActivitiesToImport: number;
userCurrency: string;
userId: string;
user: UserWithSettings;
}): Promise<Activity[]> {
const accountIdMapping: { [oldAccountId: string]: string } = {};
const userCurrency = user.Settings.settings.baseCurrency;
if (!isDryRun && accountsDto?.length) {
const [existingAccounts, existingPlatforms] = await Promise.all([
@ -167,7 +178,7 @@ export class ImportService {
);
// If there is no account or if the account belongs to a different user then create a new account
if (!accountWithSameId || accountWithSameId.userId !== userId) {
if (!accountWithSameId || accountWithSameId.userId !== user.id) {
let oldAccountId: string;
const platformId = account.platformId;
@ -180,7 +191,7 @@ export class ImportService {
let accountObject: Prisma.AccountCreateInput = {
...account,
User: { connect: { id: userId } }
User: { connect: { id: user.id } }
};
if (
@ -196,7 +207,7 @@ export class ImportService {
const newAccount = await this.accountService.createAccount(
accountObject,
userId
user.id
);
// Store the new to old account ID mappings for updating activities
@ -227,15 +238,17 @@ export class ImportService {
const assetProfiles = await this.validateActivities({
activitiesDto,
maxActivitiesToImport
maxActivitiesToImport,
user
});
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
activitiesDto,
userId
userCurrency,
userId: user.id
});
const accounts = (await this.accountService.getAccounts(userId)).map(
const accounts = (await this.accountService.getAccounts(user.id)).map(
({ id, name }) => {
return { id, name };
}
@ -254,6 +267,7 @@ export class ImportService {
{
accountId,
comment,
currency,
date,
error,
fee,
@ -278,11 +292,11 @@ export class ImportService {
assetSubClass,
countries,
createdAt,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
holdings,
id,
isin,
name,
@ -335,12 +349,12 @@ export class ImportService {
if (isDryRun) {
order = {
comment,
currency,
date,
fee,
quantity,
type,
unitPrice,
userId,
accountId: validatedAccount?.id,
accountUserId: undefined,
createdAt: new Date(),
@ -351,11 +365,11 @@ export class ImportService {
assetSubClass,
countries,
createdAt,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
holdings,
id,
isin,
name,
@ -365,11 +379,13 @@ export class ImportService {
symbolMapping,
updatedAt,
url,
currency: assetProfile.currency,
comment: assetProfile.comment
},
Account: validatedAccount,
symbolProfileId: undefined,
updatedAt: new Date()
updatedAt: new Date(),
userId: user.id
};
} else {
if (error) {
@ -383,14 +399,13 @@ export class ImportService {
quantity,
type,
unitPrice,
userId,
accountId: validatedAccount?.id,
SymbolProfile: {
connectOrCreate: {
create: {
currency,
dataSource,
symbol
symbol,
currency: assetProfile.currency
},
where: {
dataSource_symbol: {
@ -401,8 +416,14 @@ export class ImportService {
}
},
updateAccountBalance: false,
User: { connect: { id: userId } }
User: { connect: { id: user.id } },
userId: user.id
});
if (order.SymbolProfile?.symbol) {
// Update symbol that may have been assigned in createOrder()
assetProfile.symbol = order.SymbolProfile.symbol;
}
}
const value = new Big(quantity).mul(unitPrice).toNumber();
@ -413,14 +434,14 @@ export class ImportService {
value,
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee,
currency,
assetProfile.currency,
userCurrency
),
// @ts-ignore
SymbolProfile: assetProfile,
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
currency,
assetProfile.currency,
userCurrency
)
});
@ -439,15 +460,16 @@ export class ImportService {
});
});
this.dataGatheringService.gatherSymbols(
uniqueActivities.map(({ date, SymbolProfile }) => {
this.dataGatheringService.gatherSymbols({
dataGatheringItems: uniqueActivities.map(({ date, SymbolProfile }) => {
return {
date,
dataSource: SymbolProfile.dataSource,
symbol: SymbolProfile.symbol
};
})
);
}),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
});
}
return activities;
@ -455,15 +477,18 @@ export class ImportService {
private async extendActivitiesWithErrors({
activitiesDto,
userCurrency,
userId
}: {
activitiesDto: Partial<CreateOrderDto>[];
userCurrency: string;
userId: string;
}): Promise<Partial<Activity>[]> {
const existingActivities = await this.orderService.orders({
include: { SymbolProfile: true },
orderBy: { date: 'desc' },
where: { userId }
let { activities: existingActivities } = await this.orderService.getOrders({
userCurrency,
userId,
includeDrafts: true,
withExcludedAccounts: true
});
return activitiesDto.map(
@ -479,12 +504,13 @@ export class ImportService {
type,
unitPrice
}) => {
const date = parseISO(<string>(<unknown>dateString));
const date = parseISO(dateString);
const isDuplicate = existingActivities.some((activity) => {
return (
activity.accountId === accountId &&
activity.SymbolProfile.currency === currency &&
activity.SymbolProfile.dataSource === dataSource &&
isSameDay(activity.date, date) &&
isSameSecond(activity.date, date) &&
activity.fee === fee &&
activity.quantity === quantity &&
activity.SymbolProfile.symbol === symbol &&
@ -510,22 +536,15 @@ export class ImportService {
currency,
dataSource,
symbol,
assetClass: null,
assetSubClass: null,
comment: null,
countries: null,
activitiesCount: undefined,
assetClass: undefined,
assetSubClass: undefined,
countries: undefined,
createdAt: undefined,
figi: null,
figiComposite: null,
figiShareClass: null,
holdings: undefined,
id: undefined,
isin: null,
name: null,
scraperConfiguration: null,
sectors: null,
symbolMapping: null,
updatedAt: undefined,
url: null
sectors: undefined,
updatedAt: undefined
}
};
}
@ -544,10 +563,12 @@ export class ImportService {
private async validateActivities({
activitiesDto,
maxActivitiesToImport
maxActivitiesToImport,
user
}: {
activitiesDto: Partial<CreateOrderDto>[];
maxActivitiesToImport: number;
user: UserWithSettings;
}) {
if (activitiesDto?.length > maxActivitiesToImport) {
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
@ -557,40 +578,53 @@ export class ImportService {
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
} = {};
const uniqueActivitiesDto = uniqBy(
activitiesDto,
({ dataSource, symbol }) => {
return getAssetProfileIdentifier({ dataSource, symbol });
}
);
for (const [
index,
{ currency, dataSource, symbol }
] of uniqueActivitiesDto.entries()) {
if (dataSource !== 'MANUAL') {
const assetProfile = (
await this.dataProviderService.getAssetProfiles([
{ dataSource, symbol }
])
)?.[symbol];
{ currency, dataSource, symbol, type }
] of activitiesDto.entries()) {
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
if (!assetProfile?.name) {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
) {
const dataProvider = this.dataProviderService.getDataProvider(
DataSource[dataSource]
);
if (dataProvider.getDataProviderInfo().isPremium) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
}
if (
assetProfile.currency !== currency &&
!this.exchangeRateDataService.hasCurrencyPair(
currency,
assetProfile.currency
)
) {
throw new Error(
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
);
if (!assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })]) {
const assetProfile = {
currency,
...(
await this.dataProviderService.getAssetProfiles([
{ dataSource, symbol }
])
)?.[symbol]
};
if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
if (!assetProfile?.name) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
);
}
if (assetProfile.currency !== currency) {
throw new Error(
`activities.${index}.currency ("${currency}") does not match with currency of ${assetProfile.symbol} ("${assetProfile.currency}")`
);
}
}
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =

View File

@ -1,5 +1,6 @@
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { InfoService } from './info.service';

View File

@ -2,14 +2,15 @@ import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
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 { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@ -33,6 +34,7 @@ import { InfoService } from './info.service';
RedisCacheModule,
SymbolProfileModule,
TagModule,
TransformDataSourceInResponseModule,
UserModule
],
providers: [InfoService]

View File

@ -8,14 +8,12 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT,
PROPERTY_BETTER_UPTIME_MONITOR_ID,
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
PROPERTY_DEMO_USER_ID,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SLACK_COMMUNITY_USERS,
PROPERTY_STRIPE_CONFIG,
PROPERTY_SYSTEM_MESSAGE,
ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config';
import {
@ -30,6 +28,7 @@ import {
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOffer } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as cheerio from 'cheerio';
@ -58,14 +57,9 @@ export class InfoService {
const platforms = await this.platformService.getPlatforms({
orderBy: { name: 'asc' }
});
let systemMessage: string;
const globalPermissions: string[] = [];
if (this.configurationService.get('ENABLE_FEATURE_BLOG')) {
globalPermissions.push(permissions.enableBlog);
}
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
info.fearAndGreedDataSource = encodeDataSource(
@ -104,10 +98,6 @@ export class InfoService {
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {
globalPermissions.push(permissions.enableSystemMessage);
systemMessage = (await this.propertyService.getByKey(
PROPERTY_SYSTEM_MESSAGE
)) as string;
}
const isUserSignupEnabled =
@ -135,7 +125,6 @@ export class InfoService {
platforms,
statistics,
subscriptions,
systemMessage,
tags,
baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies()
@ -169,7 +158,7 @@ export class InfoService {
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
}, this.configurationService.get('REQUEST_TIMEOUT'));
const { pull_count } = await got(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
@ -194,7 +183,7 @@ export class InfoService {
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
}, this.configurationService.get('REQUEST_TIMEOUT'));
const { body } = await got('https://github.com/ghostfolio/ghostfolio', {
// @ts-ignore
@ -203,11 +192,11 @@ export class InfoService {
const $ = cheerio.load(body);
return extractNumberFromString(
$(
return extractNumberFromString({
value: $(
`a[href="/ghostfolio/ghostfolio/graphs/contributors"] .Counter`
).text()
);
});
} catch (error) {
Logger.error(error, 'InfoService - GitHub');
@ -221,7 +210,7 @@ export class InfoService {
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
}, this.configurationService.get('REQUEST_TIMEOUT'));
const { stargazers_count } = await got(
`https://api.github.com/repos/ghostfolio/ghostfolio`,
@ -349,7 +338,7 @@ export class InfoService {
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
}, this.configurationService.get('REQUEST_TIMEOUT'));
const { data } = await got(
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
@ -359,7 +348,7 @@ export class InfoService {
{
headers: {
Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY'
'API_KEY_BETTER_UPTIME'
)}`
},
// @ts-ignore

View File

@ -1,4 +1,5 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import {
Controller,
Get,

View File

@ -1,5 +1,7 @@
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
import { LogoController } from './logo.controller';
@ -7,7 +9,11 @@ import { LogoService } from './logo.service';
@Module({
controllers: [LogoController],
imports: [ConfigurationModule, SymbolProfileModule],
imports: [
ConfigurationModule,
SymbolProfileModule,
TransformDataSourceInRequestModule
],
providers: [LogoService]
})
export class LogoModule {}

View File

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import got from 'got';
@ -9,6 +10,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable()
export class LogoService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -46,7 +48,7 @@ export class LogoService {
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
}, this.configurationService.get('REQUEST_TIMEOUT'));
return got(
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,

View File

@ -1,3 +1,6 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import {
AssetClass,
AssetSubClass,
@ -13,9 +16,9 @@ import {
IsISO8601,
IsNumber,
IsOptional,
IsPositive,
IsString,
Min
Min,
Validate
} from 'class-validator';
import { isString } from 'lodash';
@ -39,14 +42,19 @@ export class CreateOrderDto {
)
comment?: string;
@IsString()
@IsCurrencyCode()
currency: string;
@IsCurrencyCode()
@IsOptional()
customCurrency?: string;
@IsOptional()
@IsEnum(DataSource, { each: true })
dataSource?: DataSource;
@IsISO8601()
@Validate(IsAfter1970Constraint)
date: string;
@IsNumber()
@ -54,7 +62,7 @@ export class CreateOrderDto {
fee: number;
@IsNumber()
@IsPositive()
@Min(0)
quantity: number;
@IsString()
@ -68,7 +76,7 @@ export class CreateOrderDto {
type: Type;
@IsNumber()
@IsPositive()
@Min(0)
unitPrice: number;
@IsBoolean()

View File

@ -1,12 +1,19 @@
import { OrderWithAccount } from '@ghostfolio/common/types';
import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import { Order, Tag } from '@prisma/client';
export interface Activities {
activities: Activity[];
count: number;
}
export interface Activity extends OrderWithAccount {
export interface Activity extends Order {
Account?: AccountWithPlatform;
error?: ActivityError;
feeInBaseCurrency: number;
SymbolProfile?: EnhancedSymbolProfile;
tags?: Tag[];
updateAccountBalance?: boolean;
value: number;
valueInBaseCurrency: number;

View File

@ -1,12 +1,19 @@
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.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 { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -24,7 +31,7 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Order as OrderModel } from '@prisma/client';
import { Order as OrderModel, Prisma } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -44,32 +51,35 @@ export class OrderController {
) {}
@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
);
}
@HasPermission(permissions.deleteOrder)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteOrders(
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise<number> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
return this.orderService.deleteOrders({
filters,
userId: this.request.user.id
});
}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.deleteOrder)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteOrder(@Param('id') id: string): Promise<OrderModel> {
const order = await this.orderService.order({ id });
const order = await this.orderService.order({
id,
userId: this.request.user.id
});
if (
!hasPermission(this.request.user.permissions, permissions.deleteOrder) ||
!order ||
order.userId !== this.request.user.id
) {
if (!order) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
@ -82,17 +92,27 @@ export class OrderController {
}
@Get()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllOrders(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange?: DateRange,
@Query('skip') skip?: number,
@Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
@Query('tags') filterByTags?: string,
@Query('take') take?: number
): Promise<Activities> {
let endDate: Date;
let startDate: Date;
if (dateRange) {
({ endDate, startDate } = getInterval(dateRange));
}
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
@ -103,8 +123,12 @@ export class OrderController {
await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const activities = await this.orderService.getOrders({
const { activities, count } = await this.orderService.getOrders({
endDate,
filters,
sortColumn,
sortDirection,
startDate,
userCurrency,
includeDrafts: true,
skip: isNaN(skip) ? undefined : skip,
@ -113,20 +137,21 @@ export class OrderController {
withExcludedAccounts: true
});
return { activities };
return { activities, count };
}
@HasPermission(permissions.createOrder)
@Post()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> {
if (
!hasPermission(this.request.user.permissions, permissions.createOrder)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
const currency = data.currency;
const customCurrency = data.customCurrency;
if (customCurrency) {
data.currency = customCurrency;
delete data.customCurrency;
}
const order = await this.orderService.createOrder({
@ -135,7 +160,7 @@ export class OrderController {
SymbolProfile: {
connectOrCreate: {
create: {
currency: data.currency,
currency,
dataSource: data.dataSource,
symbol: data.symbol
},
@ -154,31 +179,31 @@ export class OrderController {
if (data.dataSource && !order.isDraft) {
// Gather symbol data in the background, if data source is set
// (not MANUAL) and not draft
this.dataGatheringService.gatherSymbols([
{
dataSource: data.dataSource,
date: order.date,
symbol: data.symbol
}
]);
this.dataGatheringService.gatherSymbols({
dataGatheringItems: [
{
dataSource: data.dataSource,
date: order.date,
symbol: data.symbol
}
],
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
});
}
return order;
}
@HasPermission(permissions.updateOrder)
@Put(':id')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) {
const originalOrder = await this.orderService.order({
id
});
if (
!hasPermission(this.request.user.permissions, permissions.updateOrder) ||
!originalOrder ||
originalOrder.userId !== this.request.user.id
) {
if (!originalOrder || originalOrder.userId !== this.request.user.id) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
@ -188,8 +213,16 @@ export class OrderController {
const date = parseISO(data.date);
const accountId = data.accountId;
const customCurrency = data.customCurrency;
delete data.accountId;
if (customCurrency) {
data.currency = customCurrency;
delete data.customCurrency;
}
return this.orderService.updateOrder({
data: {
...data,

View File

@ -1,16 +1,18 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
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 { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.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 { OrderController } from './order.controller';
@ -22,15 +24,16 @@ import { OrderService } from './order.service';
imports: [
ApiModule,
CacheModule,
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule,
PrismaModule,
RedactValuesInResponseModule,
RedisCacheModule,
SymbolProfileModule,
UserModule
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
providers: [AccountBalanceService, AccountService, OrderService]
})

View File

@ -1,16 +1,24 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
GATHER_ASSET_PROFILE_PROCESS,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
import {
EnhancedSymbolProfile,
Filter,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
AssetClass,
AssetSubClass,
@ -18,51 +26,57 @@ import {
Order,
Prisma,
Tag,
Type as TypeOfOrder
Type as ActivityType
} from '@prisma/client';
import Big from 'big.js';
import { Big } from 'big.js';
import { endOfToday, isAfter } from 'date-fns';
import { groupBy } from 'lodash';
import { groupBy, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { Activity } from './interfaces/activities.interface';
import { Activities } from './interfaces/activities.interface';
@Injectable()
export class OrderService {
public constructor(
private readonly accountService: AccountService,
private readonly dataGatheringService: DataGatheringService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
) {}
public async order(
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
): Promise<Order | null> {
return this.prismaService.order.findUnique({
where: orderWhereUniqueInput
public async assignTags({
dataSource,
symbol,
tags,
userId
}: { tags: Tag[]; userId: string } & UniqueAsset) {
const orders = await this.prismaService.order.findMany({
where: {
userId,
SymbolProfile: {
dataSource,
symbol
}
}
});
}
public async orders(params: {
include?: Prisma.OrderInclude;
skip?: number;
take?: number;
cursor?: Prisma.OrderWhereUniqueInput;
where?: Prisma.OrderWhereInput;
orderBy?: Prisma.OrderOrderByWithRelationInput;
}): Promise<OrderWithAccount[]> {
const { include, skip, take, cursor, where, orderBy } = params;
return this.prismaService.order.findMany({
cursor,
include,
orderBy,
skip,
take,
where
});
return Promise.all(
orders.map(({ id }) =>
this.prismaService.order.update({
data: {
tags: {
// The set operation replaces all existing connections with the provided ones
set: tags.map(({ id }) => {
return { id };
})
}
},
where: { id }
})
)
);
}
public async createOrder(
@ -92,20 +106,13 @@ export class OrderService {
}
const accountId = data.accountId;
let currency = data.currency;
const tags = data.tags ?? [];
const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId;
if (
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) {
const assetClass = data.assetClass;
const assetSubClass = data.assetSubClass;
currency = data.SymbolProfile.connectOrCreate.create.currency;
const dataSource: DataSource = 'MANUAL';
const id = uuidv4();
const name = data.SymbolProfile.connectOrCreate.create.symbol;
@ -113,7 +120,6 @@ export class OrderService {
data.id = id;
data.SymbolProfile.connectOrCreate.create.assetClass = assetClass;
data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass;
data.SymbolProfile.connectOrCreate.create.currency = currency;
data.SymbolProfile.connectOrCreate.create.dataSource = dataSource;
data.SymbolProfile.connectOrCreate.create.name = name;
data.SymbolProfile.connectOrCreate.create.symbol = id;
@ -135,7 +141,8 @@ export class OrderService {
jobId: getAssetProfileIdentifier({
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
})
}),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
}
});
}
@ -148,7 +155,6 @@ export class OrderService {
delete data.comment;
}
delete data.currency;
delete data.dataSource;
delete data.symbol;
delete data.tags;
@ -157,13 +163,9 @@ export class OrderService {
const orderData: Prisma.OrderCreateInput = data;
const isDraft =
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
? false
: isAfter(data.date as Date, endOfToday());
const isDraft = ['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)
? false
: isAfter(data.date as Date, endOfToday());
const order = await this.prismaService.order.create({
data: {
@ -175,7 +177,8 @@ export class OrderService {
return { id };
})
}
}
},
include: { SymbolProfile: true }
});
if (updateAccountBalance === true) {
@ -184,19 +187,26 @@ export class OrderService {
.plus(data.fee)
.toNumber();
if (data.type === 'BUY') {
if (['BUY', 'FEE'].includes(data.type)) {
amount = new Big(amount).mul(-1).toNumber();
}
await this.accountService.updateAccountBalance({
accountId,
amount,
currency,
userId,
currency: data.SymbolProfile.connectOrCreate.create.currency,
date: data.date as Date
});
}
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: order.userId
})
);
return order;
}
@ -207,55 +217,142 @@ export class OrderService {
where
});
const [symbolProfile] =
await this.symbolProfileService.getSymbolProfilesByIds([
order.symbolProfileId
]);
if (
order.type === 'FEE' ||
order.type === 'INTEREST' ||
order.type === 'ITEM' ||
order.type === 'LIABILITY'
['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type) ||
symbolProfile.activitiesCount === 0
) {
await this.symbolProfileService.deleteById(order.symbolProfileId);
}
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: order.userId
})
);
return order;
}
public async deleteOrders(where: Prisma.OrderWhereInput): Promise<number> {
const { count } = await this.prismaService.order.deleteMany({
where
public async deleteOrders({
filters,
userId
}: {
filters?: Filter[];
userId: string;
}): Promise<number> {
const { activities } = await this.getOrders({
filters,
userId,
includeDrafts: true,
userCurrency: undefined,
withExcludedAccounts: true
});
const { count } = await this.prismaService.order.deleteMany({
where: {
id: {
in: activities.map(({ id }) => {
return id;
})
}
}
});
const symbolProfiles =
await this.symbolProfileService.getSymbolProfilesByIds(
activities.map(({ symbolProfileId }) => {
return symbolProfileId;
})
);
for (const { activitiesCount, id } of symbolProfiles) {
if (activitiesCount === 0) {
await this.symbolProfileService.deleteById(id);
}
}
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({ userId })
);
return count;
}
public async getLatestOrder({ dataSource, symbol }: UniqueAsset) {
return this.prismaService.order.findFirst({
orderBy: {
date: 'desc'
},
where: {
SymbolProfile: { dataSource, symbol }
}
});
}
public async getOrders({
endDate,
filters,
includeDrafts = false,
skip,
sortColumn,
sortDirection,
startDate,
take = Number.MAX_SAFE_INTEGER,
types,
userCurrency,
userId,
withExcludedAccounts = false
}: {
endDate?: Date;
filters?: Filter[];
includeDrafts?: boolean;
skip?: number;
sortColumn?: string;
sortDirection?: Prisma.SortOrder;
startDate?: Date;
take?: number;
types?: TypeOfOrder[];
types?: ActivityType[];
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<Activity[]> {
}): Promise<Activities> {
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
{ date: 'asc' },
{ id: 'asc' }
];
const where: Prisma.OrderWhereInput = { userId };
if (endDate || startDate) {
where.AND = [];
if (endDate) {
where.AND.push({ date: { lte: endDate } });
}
if (startDate) {
where.AND.push({ date: { gt: startDate } });
}
}
const {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, (filter) => {
return filter.type;
} = groupBy(filters, ({ type }) => {
return type;
});
const searchQuery = filters?.find(({ type }) => {
return type === 'SEARCH_QUERY';
})?.id;
if (filtersByAccount?.length > 0) {
where.accountId = {
in: filtersByAccount.map(({ id }) => {
@ -297,6 +394,30 @@ export class OrderService {
};
}
if (searchQuery) {
const searchQueryWhereInput: Prisma.SymbolProfileWhereInput[] = [
{ id: { mode: 'insensitive', startsWith: searchQuery } },
{ isin: { mode: 'insensitive', startsWith: searchQuery } },
{ name: { mode: 'insensitive', startsWith: searchQuery } },
{ symbol: { mode: 'insensitive', startsWith: searchQuery } }
];
if (where.SymbolProfile) {
where.SymbolProfile = {
AND: [
where.SymbolProfile,
{
OR: searchQueryWhereInput
}
]
};
} else {
where.SymbolProfile = {
OR: searchQueryWhereInput
};
}
}
if (filtersByTag?.length > 0) {
where.tags = {
some: {
@ -307,18 +428,24 @@ export class OrderService {
};
}
if (types) {
where.OR = types.map((type) => {
return {
type: {
equals: type
}
};
});
if (sortColumn) {
orderBy = [{ [sortColumn]: sortDirection }, { id: sortDirection }];
}
return (
await this.orders({
if (types) {
where.type = { in: types };
}
if (withExcludedAccounts === false) {
where.OR = [
{ Account: null },
{ Account: { NOT: { isExcluded: true } } }
];
}
const [orders, count] = await Promise.all([
this.orders({
orderBy,
skip,
take,
where,
@ -332,35 +459,87 @@ export class OrderService {
// eslint-disable-next-line @typescript-eslint/naming-convention
SymbolProfile: true,
tags: true
},
orderBy: { date: 'asc' }
})
)
.filter((order) => {
return (
withExcludedAccounts ||
!order.Account ||
order.Account?.isExcluded === false
);
})
.map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
}
}),
this.prismaService.order.count({ where })
]);
const uniqueAssets = uniqBy(
orders.map(({ SymbolProfile }) => {
return {
...order,
value,
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,
userCurrency
)
dataSource: SymbolProfile.dataSource,
symbol: SymbolProfile.symbol
};
}),
({ dataSource, symbol }) => {
return getAssetProfileIdentifier({
dataSource,
symbol
});
}
);
const assetProfiles =
await this.symbolProfileService.getSymbolProfiles(uniqueAssets);
const activities = orders.map((order) => {
const assetProfile = assetProfiles.find(({ dataSource, symbol }) => {
return (
dataSource === order.SymbolProfile.dataSource &&
symbol === order.SymbolProfile.symbol
);
});
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
return {
...order,
value,
// TODO: Use exchange rate of date
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
SymbolProfile: assetProfile,
// TODO: Use exchange rate of date
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
return { activities, count };
}
public async getStatisticsByCurrency(
currency: EnhancedSymbolProfile['currency']
): Promise<{
activitiesCount: EnhancedSymbolProfile['activitiesCount'];
dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
}> {
const { _count, _min } = await this.prismaService.order.aggregate({
_count: true,
_min: {
date: true
},
where: { SymbolProfile: { currency } }
});
return {
activitiesCount: _count as number,
dateOfFirstActivity: _min.date
};
}
public async order(
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
): Promise<Order | null> {
return this.prismaService.order.findUnique({
where: orderWhereUniqueInput
});
}
public async updateOrder({
@ -374,13 +553,10 @@ export class OrderService {
dataSource?: DataSource;
symbol?: string;
tags?: Tag[];
type?: ActivityType;
};
where: Prisma.OrderWhereUniqueInput;
}): Promise<Order> {
if (data.Account.connect.id_userId.id === null) {
delete data.Account;
}
if (!data.comment) {
data.comment = null;
}
@ -389,13 +565,12 @@ export class OrderService {
let isDraft = false;
if (
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) {
delete data.SymbolProfile.connect;
if (data.Account?.connect?.id_userId?.id === null) {
data.Account = { disconnect: true };
}
} else {
delete data.SymbolProfile.update;
@ -403,19 +578,22 @@ export class OrderService {
if (!isDraft) {
// Gather symbol data of order in the background, if not draft
this.dataGatheringService.gatherSymbols([
{
dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource,
date: <Date>data.date,
symbol: data.SymbolProfile.connect.dataSource_symbol.symbol
}
]);
this.dataGatheringService.gatherSymbols({
dataGatheringItems: [
{
dataSource:
data.SymbolProfile.connect.dataSource_symbol.dataSource,
date: <Date>data.date,
symbol: data.SymbolProfile.connect.dataSource_symbol.symbol
}
],
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
});
}
}
delete data.assetClass;
delete data.assetSubClass;
delete data.currency;
delete data.dataSource;
delete data.symbol;
delete data.tags;
@ -426,7 +604,7 @@ export class OrderService {
where
});
return this.prismaService.order.update({
const order = await this.prismaService.order.update({
data: {
...data,
isDraft,
@ -438,5 +616,34 @@ export class OrderService {
},
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: order.userId
})
);
return order;
}
private async orders(params: {
include?: Prisma.OrderInclude;
skip?: number;
take?: number;
cursor?: Prisma.OrderWhereUniqueInput;
where?: Prisma.OrderWhereInput;
orderBy?: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput>;
}): Promise<OrderWithAccount[]> {
const { include, skip, take, cursor, where, orderBy } = params;
return this.prismaService.order.findMany({
cursor,
include,
orderBy,
skip,
take,
where
});
}
}

View File

@ -1,3 +1,6 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import {
AssetClass,
AssetSubClass,
@ -8,14 +11,13 @@ import {
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEnum,
IsISO8601,
IsNumber,
IsOptional,
IsPositive,
IsString,
Min
Min,
Validate
} from 'class-validator';
import { isString } from 'lodash';
@ -39,13 +41,18 @@ export class UpdateOrderDto {
)
comment?: string;
@IsString()
@IsCurrencyCode()
currency: string;
@IsCurrencyCode()
@IsOptional()
customCurrency?: string;
@IsString()
dataSource: DataSource;
@IsISO8601()
@Validate(IsAfter1970Constraint)
date: string;
@IsNumber()
@ -56,7 +63,7 @@ export class UpdateOrderDto {
id: string;
@IsNumber()
@IsPositive()
@Min(0)
quantity: number;
@IsString()
@ -70,6 +77,6 @@ export class UpdateOrderDto {
type: Type;
@IsNumber()
@IsPositive()
@Min(0)
unitPrice: number;
}

View File

@ -1,9 +1,12 @@
import { IsString } from 'class-validator';
import { IsString, IsUrl } from 'class-validator';
export class CreatePlatformDto {
@IsString()
name: string;
@IsString()
@IsUrl({
protocols: ['https'],
require_protocol: true
})
url: string;
}

View File

@ -1,18 +1,18 @@
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions';
import {
Body,
Controller,
Delete,
Get,
HttpException,
Inject,
Param,
Post,
Put,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Platform } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -23,49 +23,30 @@ import { UpdatePlatformDto } from './update-platform.dto';
@Controller('platform')
export class PlatformController {
public constructor(
private readonly platformService: PlatformService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
public constructor(private readonly platformService: PlatformService) {}
@Get()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPlatforms() {
return this.platformService.getPlatformsWithAccountCount();
}
@HasPermission(permissions.createPlatform)
@Post()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createPlatform(
@Body() data: CreatePlatformDto
): Promise<Platform> {
if (
!hasPermission(this.request.user.permissions, permissions.createPlatform)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.platformService.createPlatform(data);
}
@HasPermission(permissions.updatePlatform)
@Put(':id')
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updatePlatform(
@Param('id') id: string,
@Body() data: UpdatePlatformDto
) {
if (
!hasPermission(this.request.user.permissions, permissions.updatePlatform)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const originalPlatform = await this.platformService.getPlatform({
id
});
@ -88,17 +69,9 @@ export class PlatformController {
}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
@HasPermission(permissions.deletePlatform)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deletePlatform(@Param('id') id: string) {
if (
!hasPermission(this.request.user.permissions, permissions.deletePlatform)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const originalPlatform = await this.platformService.getPlatform({
id
});

View File

@ -1,4 +1,5 @@
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { PlatformController } from './platform.controller';

View File

@ -1,4 +1,5 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { Platform, Prisma } from '@prisma/client';

View File

@ -1,4 +1,4 @@
import { IsString } from 'class-validator';
import { IsString, IsUrl } from 'class-validator';
export class UpdatePlatformDto {
@IsString()
@ -7,6 +7,9 @@ export class UpdatePlatformDto {
@IsString()
name: string;
@IsString()
@IsUrl({
protocols: ['https'],
require_protocol: true
})
url: string;
}

View File

@ -0,0 +1,33 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
export class MWRPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance(
positions: TimelinePosition[]
): PortfolioSnapshot {
throw new Error('Method not implemented.');
}
protected getSymbolMetrics({
dataSource,
end,
exchangeRates,
isChartMode = false,
marketSymbolMap,
start,
step = 1,
symbol
}: {
end: Date;
exchangeRates: { [dateString: string]: number };
isChartMode?: boolean;
marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
};
start: Date;
step?: number;
} & UniqueAsset): SymbolMetrics {
throw new Error('Method not implemented.');
}
}

View File

@ -0,0 +1,31 @@
export const activityDummyData = {
accountId: undefined,
accountUserId: undefined,
comment: undefined,
createdAt: new Date(),
currency: undefined,
feeInBaseCurrency: undefined,
id: undefined,
isDraft: false,
symbolProfileId: undefined,
updatedAt: new Date(),
userId: undefined,
value: undefined,
valueInBaseCurrency: undefined
};
export const symbolProfileDummyData = {
activitiesCount: undefined,
assetClass: undefined,
assetSubClass: undefined,
countries: [],
createdAt: undefined,
holdings: [],
id: undefined,
sectors: [],
updatedAt: undefined
};
export const userDummyData = {
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
};

View File

@ -0,0 +1,81 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { DateRange, UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { MWRPortfolioCalculator } from './mwr/portfolio-calculator';
import { PortfolioCalculator } from './portfolio-calculator';
import { TWRPortfolioCalculator } from './twr/portfolio-calculator';
export enum PerformanceCalculationType {
MWR = 'MWR', // Money-Weighted Rate of Return
TWR = 'TWR' // Time-Weighted Rate of Return
}
@Injectable()
export class PortfolioCalculatorFactory {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly currentRateService: CurrentRateService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly redisCacheService: RedisCacheService
) {}
public createCalculator({
accountBalanceItems = [],
activities,
calculationType,
currency,
dateRange = 'max',
hasFilters,
isExperimentalFeatures = false,
userId
}: {
accountBalanceItems?: HistoricalDataItem[];
activities: Activity[];
calculationType: PerformanceCalculationType;
currency: string;
dateRange?: DateRange;
hasFilters: boolean;
isExperimentalFeatures?: boolean;
userId: string;
}): PortfolioCalculator {
const useCache = !hasFilters && isExperimentalFeatures;
switch (calculationType) {
case PerformanceCalculationType.MWR:
return new MWRPortfolioCalculator({
accountBalanceItems,
activities,
currency,
dateRange,
useCache,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
redisCacheService: this.redisCacheService
});
case PerformanceCalculationType.TWR:
return new TWRPortfolioCalculator({
accountBalanceItems,
activities,
currency,
currentRateService: this.currentRateService,
dateRange,
useCache,
userId,
configurationService: this.configurationService,
exchangeRateDataService: this.exchangeRateDataService,
redisCacheService: this.redisCacheService
});
default:
throw new Error('Invalid calculation type');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,219 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BALN.SW buy and sell in two activities', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2021-11-22'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPrice: 142.9
},
{
...activityDummyData,
date: new Date('2021-11-30'),
fee: 1.65,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'SELL',
unitPrice: 136.6
},
{
...activityDummyData,
date: new Date('2021-11-30'),
fee: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'SELL',
unitPrice: 136.6
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'CHF',
hasFilters: false,
userId: userDummyData.id
});
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-22')
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-22')
);
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: chartData,
groupBy: 'month'
});
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.04408677396780965649'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.04408677396780965649'
),
grossPerformanceWithCurrencyEffect: new Big('-12.6'),
hasErrors: false,
netPerformance: new Big('-15.8'),
netPerformancePercentage: new Big('-0.05528341497550734703'),
netPerformancePercentageWithCurrencyEffect: new Big(
'-0.05528341497550734703'
),
netPerformanceWithCurrencyEffect: new Big('-15.8'),
positions: [
{
averagePrice: new Big('0'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.04408677396780965649'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.04408677396780965649'
),
grossPerformanceWithCurrencyEffect: new Big('-12.6'),
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('-15.8'),
netPerformancePercentage: new Big('-0.05528341497550734703'),
netPerformancePercentageWithCurrencyEffect: new Big(
'-0.05528341497550734703'
),
netPerformanceWithCurrencyEffect: new Big('-15.8'),
marketPrice: 148.9,
marketPriceInBaseCurrency: 148.9,
quantity: new Big('0'),
symbol: 'BALN.SW',
tags: [],
timeWeightedInvestment: new Big('285.80000000000000396627'),
timeWeightedInvestmentWithCurrencyEffect: new Big(
'285.80000000000000396627'
),
transactionCount: 3,
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2021-11-22', investment: new Big('285.8') },
{ date: '2021-11-30', investment: new Big('0') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 }
]);
});
});
});

View File

@ -0,0 +1,202 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PerformanceCalculationType,
PortfolioCalculatorFactory
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BALN.SW buy and sell', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2021-11-22'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPrice: 142.9
},
{
...activityDummyData,
date: new Date('2021-11-30'),
fee: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'SELL',
unitPrice: 136.6
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'CHF',
hasFilters: false,
userId: userDummyData.id
});
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-22')
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-22')
);
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: chartData,
groupBy: 'month'
});
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.0440867739678096571'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.0440867739678096571'
),
grossPerformanceWithCurrencyEffect: new Big('-12.6'),
hasErrors: false,
netPerformance: new Big('-15.8'),
netPerformancePercentage: new Big('-0.0552834149755073478'),
netPerformancePercentageWithCurrencyEffect: new Big(
'-0.0552834149755073478'
),
netPerformanceWithCurrencyEffect: new Big('-15.8'),
positions: [
{
averagePrice: new Big('0'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.0440867739678096571'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.0440867739678096571'
),
grossPerformanceWithCurrencyEffect: new Big('-12.6'),
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('-15.8'),
netPerformancePercentage: new Big('-0.0552834149755073478'),
netPerformancePercentageWithCurrencyEffect: new Big(
'-0.0552834149755073478'
),
netPerformanceWithCurrencyEffect: new Big('-15.8'),
marketPrice: 148.9,
marketPriceInBaseCurrency: 148.9,
quantity: new Big('0'),
symbol: 'BALN.SW',
tags: [],
timeWeightedInvestment: new Big('285.8'),
timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'),
transactionCount: 2,
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2021-11-22', investment: new Big('285.8') },
{ date: '2021-11-30', investment: new Big('0') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 }
]);
});
});
});

View File

@ -0,0 +1,186 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BALN.SW buy', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2021-11-30'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPrice: 136.6
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'CHF',
hasFilters: false,
userId: userDummyData.id
});
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-30')
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-30')
);
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: chartData,
groupBy: 'month'
});
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('297.8'),
errors: [],
grossPerformance: new Big('24.6'),
grossPerformancePercentage: new Big('0.09004392386530014641'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.09004392386530014641'
),
grossPerformanceWithCurrencyEffect: new Big('24.6'),
hasErrors: false,
netPerformance: new Big('23.05'),
netPerformancePercentage: new Big('0.08437042459736456808'),
netPerformancePercentageWithCurrencyEffect: new Big(
'0.08437042459736456808'
),
netPerformanceWithCurrencyEffect: new Big('23.05'),
positions: [
{
averagePrice: new Big('136.6'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1.55'),
feeInBaseCurrency: new Big('1.55'),
firstBuyDate: '2021-11-30',
grossPerformance: new Big('24.6'),
grossPerformancePercentage: new Big('0.09004392386530014641'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.09004392386530014641'
),
grossPerformanceWithCurrencyEffect: new Big('24.6'),
investment: new Big('273.2'),
investmentWithCurrencyEffect: new Big('273.2'),
netPerformance: new Big('23.05'),
netPerformancePercentage: new Big('0.08437042459736456808'),
netPerformancePercentageWithCurrencyEffect: new Big(
'0.08437042459736456808'
),
netPerformanceWithCurrencyEffect: new Big('23.05'),
marketPrice: 148.9,
marketPriceInBaseCurrency: 148.9,
quantity: new Big('2'),
symbol: 'BALN.SW',
tags: [],
timeWeightedInvestment: new Big('273.2'),
timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'),
transactionCount: 1,
valueInBaseCurrency: new Big('297.8')
}
],
totalFeesWithCurrencyEffect: new Big('1.55'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('273.2'),
totalInvestmentWithCurrencyEffect: new Big('273.2'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2021-11-30', investment: new Big('273.2') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: 273.2 },
{ date: '2021-12-01', investment: 0 }
]);
});
});
});

View File

@ -0,0 +1,256 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BTCUSD buy and sell partially', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2018-01-01').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2015-01-01'),
fee: 0,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: 'YAHOO',
name: 'Bitcoin USD',
symbol: 'BTCUSD'
},
type: 'BUY',
unitPrice: 320.43
},
{
...activityDummyData,
date: new Date('2017-12-31'),
fee: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: 'YAHOO',
name: 'Bitcoin USD',
symbol: 'BTCUSD'
},
type: 'SELL',
unitPrice: 14156.4
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'CHF',
hasFilters: false,
userId: userDummyData.id
});
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2015-01-01')
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2015-01-01')
);
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: chartData,
groupBy: 'month'
});
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('13298.425356'),
errors: [],
grossPerformance: new Big('27172.74'),
grossPerformancePercentage: new Big('42.41978276196153750666'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'41.6401219622042072686'
),
grossPerformanceWithCurrencyEffect: new Big('26516.208701400000064086'),
hasErrors: false,
netPerformance: new Big('27172.74'),
netPerformancePercentage: new Big('42.41978276196153750666'),
netPerformancePercentageWithCurrencyEffect: new Big(
'41.6401219622042072686'
),
netPerformanceWithCurrencyEffect: new Big('26516.208701400000064086'),
positions: [
{
averagePrice: new Big('320.43'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'),
feeInBaseCurrency: new Big('0'),
firstBuyDate: '2015-01-01',
grossPerformance: new Big('27172.74'),
grossPerformancePercentage: new Big('42.41978276196153750666'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'41.6401219622042072686'
),
grossPerformanceWithCurrencyEffect: new Big(
'26516.208701400000064086'
),
investment: new Big('320.43'),
investmentWithCurrencyEffect: new Big('318.542667299999967957'),
marketPrice: 13657.2,
marketPriceInBaseCurrency: 13298.425356,
netPerformance: new Big('27172.74'),
netPerformancePercentage: new Big('42.41978276196153750666'),
netPerformancePercentageWithCurrencyEffect: new Big(
'41.6401219622042072686'
),
netPerformanceWithCurrencyEffect: new Big(
'26516.208701400000064086'
),
quantity: new Big('1'),
symbol: 'BTCUSD',
tags: [],
timeWeightedInvestment: new Big('640.56763686131386861314'),
timeWeightedInvestmentWithCurrencyEffect: new Big(
'636.79469348020066587024'
),
transactionCount: 2,
valueInBaseCurrency: new Big('13298.425356')
}
],
totalFeesWithCurrencyEffect: new Big('0'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('320.43'),
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2015-01-01', investment: new Big('640.86') },
{ date: '2017-12-31', investment: new Big('320.43') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2015-01-01', investment: 637.0853345999999 },
{ date: '2015-02-01', investment: 0 },
{ date: '2015-03-01', investment: 0 },
{ date: '2015-04-01', investment: 0 },
{ date: '2015-05-01', investment: 0 },
{ date: '2015-06-01', investment: 0 },
{ date: '2015-07-01', investment: 0 },
{ date: '2015-08-01', investment: 0 },
{ date: '2015-09-01', investment: 0 },
{ date: '2015-10-01', investment: 0 },
{ date: '2015-11-01', investment: 0 },
{ date: '2015-12-01', investment: 0 },
{ date: '2016-01-01', investment: 0 },
{ date: '2016-02-01', investment: 0 },
{ date: '2016-03-01', investment: 0 },
{ date: '2016-04-01', investment: 0 },
{ date: '2016-05-01', investment: 0 },
{ date: '2016-06-01', investment: 0 },
{ date: '2016-07-01', investment: 0 },
{ date: '2016-08-01', investment: 0 },
{ date: '2016-09-01', investment: 0 },
{ date: '2016-10-01', investment: 0 },
{ date: '2016-11-01', investment: 0 },
{ date: '2016-12-01', investment: 0 },
{ date: '2017-01-01', investment: 0 },
{ date: '2017-02-01', investment: 0 },
{ date: '2017-03-01', investment: 0 },
{ date: '2017-04-01', investment: 0 },
{ date: '2017-05-01', investment: 0 },
{ date: '2017-06-01', investment: 0 },
{ date: '2017-07-01', investment: 0 },
{ date: '2017-08-01', investment: 0 },
{ date: '2017-09-01', investment: 0 },
{ date: '2017-10-01', investment: 0 },
{ date: '2017-11-01', investment: 0 },
{ date: '2017-12-01', investment: -318.54266729999995 },
{ date: '2018-01-01', investment: 0 }
]);
});
});
});

View File

@ -0,0 +1,158 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('compute portfolio snapshot', () => {
it.only('with fee activity', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2021-09-01'),
fee: 49,
quantity: 0,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: 'MANUAL',
name: 'Account Opening Fee',
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141'
},
type: 'FEE',
unitPrice: 0
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'USD',
hasFilters: false,
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-30')
);
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('0'),
grossPerformancePercentage: new Big('0'),
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
grossPerformanceWithCurrencyEffect: new Big('0'),
hasErrors: true,
netPerformance: new Big('0'),
netPerformancePercentage: new Big('0'),
netPerformancePercentageWithCurrencyEffect: new Big('0'),
netPerformanceWithCurrencyEffect: new Big('0'),
positions: [
{
averagePrice: new Big('0'),
currency: 'USD',
dataSource: 'MANUAL',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('49'),
feeInBaseCurrency: new Big('49'),
firstBuyDate: '2021-09-01',
grossPerformance: null,
grossPerformancePercentage: null,
grossPerformancePercentageWithCurrencyEffect: null,
grossPerformanceWithCurrencyEffect: null,
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
marketPrice: null,
marketPriceInBaseCurrency: 0,
netPerformance: null,
netPerformancePercentage: null,
netPerformancePercentageWithCurrencyEffect: null,
netPerformanceWithCurrencyEffect: null,
quantity: new Big('0'),
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141',
tags: [],
timeWeightedInvestment: new Big('0'),
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
transactionCount: 1,
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('49'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
});
});
});

View File

@ -0,0 +1,222 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with GOOGL buy', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2023-01-03'),
fee: 1,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: 'YAHOO',
name: 'Alphabet Inc.',
symbol: 'GOOGL'
},
type: 'BUY',
unitPrice: 89.12
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'CHF',
hasFilters: false,
userId: userDummyData.id
});
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2023-01-03')
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2023-01-03')
);
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: chartData,
groupBy: 'month'
});
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('103.10483'),
errors: [],
grossPerformance: new Big('27.33'),
grossPerformancePercentage: new Big('0.3066651705565529623'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.25235044599563974109'
),
grossPerformanceWithCurrencyEffect: new Big('20.775774'),
hasErrors: false,
netPerformance: new Big('26.33'),
netPerformancePercentage: new Big('0.29544434470377019749'),
netPerformancePercentageWithCurrencyEffect: new Big(
'0.24112962014285697628'
),
netPerformanceWithCurrencyEffect: new Big('19.851974'),
positions: [
{
averagePrice: new Big('89.12'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1'),
feeInBaseCurrency: new Big('0.9238'),
firstBuyDate: '2023-01-03',
grossPerformance: new Big('27.33'),
grossPerformancePercentage: new Big('0.3066651705565529623'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.25235044599563974109'
),
grossPerformanceWithCurrencyEffect: new Big('20.775774'),
investment: new Big('89.12'),
investmentWithCurrencyEffect: new Big('82.329056'),
netPerformance: new Big('26.33'),
netPerformancePercentage: new Big('0.29544434470377019749'),
netPerformancePercentageWithCurrencyEffect: new Big(
'0.24112962014285697628'
),
netPerformanceWithCurrencyEffect: new Big('19.851974'),
marketPrice: 116.45,
marketPriceInBaseCurrency: 103.10483,
quantity: new Big('1'),
symbol: 'GOOGL',
tags: [],
timeWeightedInvestment: new Big('89.12'),
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'),
transactionCount: 1,
valueInBaseCurrency: new Big('103.10483')
}
],
totalFeesWithCurrencyEffect: new Big('0.9238'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('89.12'),
totalInvestmentWithCurrencyEffect: new Big('82.329056'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2023-01-03', investment: new Big('89.12') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2023-01-01', investment: 82.329056 },
{
date: '2023-02-01',
investment: 0
},
{
date: '2023-03-01',
investment: 0
},
{
date: '2023-04-01',
investment: 0
},
{
date: '2023-05-01',
investment: 0
},
{
date: '2023-06-01',
investment: 0
},
{
date: '2023-07-01',
investment: 0
}
]);
});
});
});

View File

@ -0,0 +1,158 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('compute portfolio snapshot', () => {
it.only('with item activity', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-01-31').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2022-01-01'),
fee: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: 'MANUAL',
name: 'Penthouse Apartment',
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde'
},
type: 'ITEM',
unitPrice: 500000
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'USD',
hasFilters: false,
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2022-01-01')
);
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('0'),
grossPerformancePercentage: new Big('0'),
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
grossPerformanceWithCurrencyEffect: new Big('0'),
hasErrors: true,
netPerformance: new Big('0'),
netPerformancePercentage: new Big('0'),
netPerformancePercentageWithCurrencyEffect: new Big('0'),
netPerformanceWithCurrencyEffect: new Big('0'),
positions: [
{
averagePrice: new Big('500000'),
currency: 'USD',
dataSource: 'MANUAL',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'),
feeInBaseCurrency: new Big('0'),
firstBuyDate: '2022-01-01',
grossPerformance: null,
grossPerformancePercentage: null,
grossPerformancePercentageWithCurrencyEffect: null,
grossPerformanceWithCurrencyEffect: null,
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
marketPrice: null,
marketPriceInBaseCurrency: 500000,
netPerformance: null,
netPerformancePercentage: null,
netPerformancePercentageWithCurrencyEffect: null,
netPerformanceWithCurrencyEffect: null,
quantity: new Big('0'),
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde',
tags: [],
timeWeightedInvestment: new Big('0'),
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
transactionCount: 1,
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('0'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
});
});
});

View File

@ -0,0 +1,108 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
redisCacheService
);
});
describe('compute portfolio snapshot', () => {
it.only('with liability activity', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-01-31').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2023-01-01'), // Date in future
fee: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: 'MANUAL',
name: 'Loan',
symbol: '55196015-1365-4560-aa60-8751ae6d18f8'
},
type: 'LIABILITY',
unitPrice: 3000
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'USD',
hasFilters: false,
userId: userDummyData.id
});
spy.mockRestore();
const liabilitiesInBaseCurrency =
await portfolioCalculator.getLiabilitiesInBaseCurrency();
expect(liabilitiesInBaseCurrency).toEqual(new Big(3000));
});
});
});

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