Compare commits

..

160 Commits

Author SHA1 Message Date
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
514 changed files with 35289 additions and 30850 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 # CACHE
REDIS_HOST=localhost REDIS_HOST=localhost
@ -10,6 +10,7 @@ POSTGRES_DB=ghostfolio-db
POSTGRES_USER=user POSTGRES_USER=user
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD> POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
# VARIOUS
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING> ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
JWT_SECRET_KEY=<INSERT_RANDOM_STRING> JWT_SECRET_KEY=<INSERT_RANDOM_STRING>

View File

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

View File

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

View File

@ -5,6 +5,316 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.69.0 - 2024-03-30
### Added
- Added the date range support in the activities table on the portfolio activities page (experimental)
- Extended the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental)
- Set up `Tini` to avoid zombie processes and perform signal forwarding in docker image
### Changed
- Improved the usability to delete an asset profile in the historical market data table and the asset profile details dialog of the admin control
### Fixed
- Added missing dates to edit historical market data in the asset profile details dialog of the admin control panel
## 2.68.0 - 2024-03-29
### Added
- Extended the export functionality by the user accounts currency
- Added support to override the name of an asset profile in the asset profile details dialog of the admin control
### Changed
- Optimized the portfolio calculations
### Fixed
- Fixed the chart tooltip of the benchmark comparator
- Fixed an issue with names in the activities table on the portfolio activities page while using symbol profile overrides
## 2.67.0 - 2024-03-26
### Added
- Added support for the cryptocurrency _Toncoin_ (`TON11419-USD`)
### Changed
- Replaced `Math.random()` with `crypto.randomBytes()` for generating cryptographically secure random strings
- Upgraded `ionicons` from version `7.1.0` to `7.3.0`
- Upgraded `yahoo-finance2` from version `2.10.0` to `2.11.0`
- Upgraded `zone.js` from version `0.14.3` to `0.14.4`
## 2.66.3 - 2024-03-23
### Added
- Extended the content of the _SaaS_ and _Self-Hosting_ sections by the backup strategy on the Frequently Asked Questions (FAQ) page
- Added an index for `dataSource` / `symbol` to the market data database table
### Changed
- Improved the chart tooltip of the benchmark comparator by adding the benchmark name
- Upgraded `angular` from version `17.1.3` to `17.2.4`
- Upgraded `Nx` from version `18.0.4` to `18.1.2`
### Fixed
- Fixed the missing portfolio performance chart in the _Presenter View_ / _Zen Mode_
## 2.65.0 - 2024-03-19
### Added
- Added the symbol and ISIN number to the position detail dialog
- Added support to delete an asset profile in the asset profile details dialog of the admin control
### Changed
- Moved the support to grant private access with permissions from experimental to general availability
- Set the meta theme color dynamically to respect the appearance (dark mode)
- Improved the usability to edit market data in the admin control panel
## 2.64.0 - 2024-03-16
### Added
- Added a toggle to switch between active and closed holdings on the portfolio holdings page
- Added support to update the cash balance of an account when adding a fee activity
- Added support to update the cash balance of an account when adding an interest activity
- Extended the content of the _General_ section by the product roadmap on the Frequently Asked Questions (FAQ) page
### Changed
- Improved the usability of the platform management in the admin control panel
- Improved the usability of the tag management in the admin control panel
- Improved the exception handling of various rules in the _X-ray_ section
- Increased the timeout to load benchmarks
- Upgraded `prisma` from version `5.10.2` to `5.11.0`
### Fixed
- Fixed an issue in the dividend calculation of the portfolio holdings
- Fixed the date conversion of the import of historical market data in the admin control panel
## 2.63.2 - 2024-03-12
### Added
- Extended the content of the _Self-Hosting_ section by available home server systems on the Frequently Asked Questions (FAQ) page
- Added support for the cryptocurrency _Real Smurf Cat_ (`SMURFCAT-USD`)
### Changed
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `8.3` to `9.0`
- Upgraded `countries-list` from version `2.6.1` to `3.1.0`
- Upgraded `yahoo-finance2` from version `2.9.1` to `2.10.0`
### Fixed
- Fixed an issue in the performance calculation caused by multiple `SELL` activities on the same day
- Fixed an issue in the calculation on the allocations page caused by liabilities
- Fixed an issue with the currency in the request to get quotes from _EOD Historical Data_
## 2.62.0 - 2024-03-09
### Changed
- Optimized the calculation of the accounts table
- Optimized the calculation of the portfolio holdings
- Integrated dividend into the transaction point concept in the portfolio service
- Removed the environment variable `WEB_AUTH_RP_ID`
### Fixed
- Fixed an issue in the calculation of the portfolio summary caused by future liabilities
- Fixed an issue with removing a linked account from a (wealth) item activity
## 2.61.1 - 2024-03-06
### Fixed
- Fixed an issue in the account value calculation caused by liabilities
## 2.61.0 - 2024-03-04
### Changed
- Optimized the calculation of the portfolio summary
### Fixed
- Fixed the activities import (query parameter handling)
## 2.60.0 - 2024-03-02
### Added
- Added support for the cryptocurrency _Uniswap_ (`UNI7083-USD`)
### Changed
- Improved the usability of the benchmarks in the markets overview
- Integrated (wealth) items into the transaction point concept in the portfolio service
- Refreshed the cryptocurrencies list
### Fixed
- Fixed a missing value in the activities table on mobile
- Fixed a missing value on the public page
- Displayed the button to fetch the current market price only if the activity is from today
## 2.59.0 - 2024-02-29
### Added
- Added an index for `isExcluded` to the account database table
- Extended the content of the _Self-Hosting_ section on the Frequently Asked Questions (FAQ) page
### Changed
- Improved the activities import by `isin` in the _Yahoo Finance_ service
### Fixed
- Fixed an issue with the exchange rate calculation of (wealth) items in accounts
## 2.58.0 - 2024-02-27
### Changed
- Improved the handling of activities without account
### Fixed
- Fixed the query to filter activities of excluded accounts
- Improved the asset profile validation in the activities import
## 2.57.0 - 2024-02-25
### Changed
- Moved the break down of the performance into asset and currency on the analysis page from experimental to general availability
- Restructured the `copy-assets` `Nx` target
### Fixed
- Changed the performances of the _Top 3_ and _Bottom 3_ performers on the analysis page to take the currency effects into account
## 2.56.0 - 2024-02-24
### Changed
- Switched the performance calculations to take the currency effects into account
- Removed the `isDefault` flag from the `Account` database schema
- Exposed the database index of _Redis_ as an environment variable (`REDIS_DB`)
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `5.9.1` to `5.10.2`
### Fixed
- Added the missing default currency to the prepare currencies function in the exchange rate data service
## 2.55.0 - 2024-02-22
### Added
- Added indexes for `alias`, `granteeUserId` and `userId` to the access database table
- Added indexes for `currency`, `name` and `userId` to the account database table
- Added indexes for `accountId`, `date` and `updatedAt` to the account balance database table
- Added an index for `userId` to the auth device database table
- Added indexes for `marketPrice` and `state` to the market data database table
- Added indexes for `date`, `isDraft` and `userId` to the order database table
- Added an index for `name` to the platform database table
- Added indexes for `assetClass`, `currency`, `dataSource`, `isin`, `name` and `symbol` to the symbol profile database table
- Added an index for `userId` to the subscription database table
- Added an index for `name` to the tag database table
- Added indexes for `accessToken`, `createdAt`, `provider`, `role` and `thirdPartyId` to the user database table
### Changed
- Improved the validation for `currency` in various endpoints
- Harmonized the setting of a default locale in various components
- Set the parser to `angular` in the `prettier` options
## 2.54.0 - 2024-02-19
### Added
- Added an index for `id` to the account database table
- Added indexes for `dataSource` and `date` to the market data database table
- Added an index for `accountId` to the order database table
## 2.53.1 - 2024-02-18
### Added
- Added an accounts tab to the position detail dialog
- Added `INACTIVE` as a new user role
### Changed
- Improved the usability of the holdings table
- Refactored the query to filter activities of excluded accounts
- Eliminated the search request to get quotes in the _EOD Historical Data_ service
- Improved the language localization for German (`de`)
- Upgraded `ng-extract-i18n-merge` from version `2.9.1` to `2.10.0`
## 2.52.0 - 2024-02-16
### Added
- Added a loading indicator to the dividend timeline on the analysis page
- Added a loading indicator to the investment timeline on the analysis page
- Added support for the cryptocurrency _Jupiter_ (`JUP29210-USD`)
### Changed
- Divided the content of the Frequently Asked Questions (FAQ) page into three sections: _General_, _Cloud (SaaS)_ and _Self-Hosting_
### Fixed
- Fixed an issue with the X-axis scale of the dividend timeline on the analysis page
- Fixed an issue with the X-axis scale of the investment timeline on the analysis page
## 2.51.0 - 2024-02-12
### Changed
- Improved the ordered list of the _Top 3_ and _Bottom 3_ performers on the analysis page in Safari
- Replaced `import-sort` with `prettier-plugin-sort-imports`
- Upgraded `eslint` dependencies
- Upgraded `Nx` from version `17.2.8` to `18.0.4`
### Fixed
- Fixed the date conversion of the import of historical market data in the admin control panel
## 2.50.0 - 2024-02-11
### Added
- Introduced a setting to disable the data gathering in the admin control
### Changed
- Harmonized the environment variables of various API keys
- Upgraded `prisma` from version `5.8.1` to `5.9.1`
### Todo
- Rename the environment variable from `ALPHA_VANTAGE_API_KEY` to `API_KEY_ALPHA_VANTAGE`
- Rename the environment variable from `BETTER_UPTIME_API_KEY` to `API_KEY_BETTER_UPTIME`
- Rename the environment variable from `EOD_HISTORICAL_DATA_API_KEY` to `API_KEY_EOD_HISTORICAL_DATA`
- Rename the environment variable from `FINANCIAL_MODELING_PREP_API_KEY` to `API_KEY_FINANCIAL_MODELING_PREP`
- Rename the environment variable from `OPEN_FIGI_API_KEY` to `API_KEY_OPEN_FIGI`
- Rename the environment variable from `RAPID_API_API_KEY` to `API_KEY_RAPID_API`
## 2.49.0 - 2024-02-09 ## 2.49.0 - 2024-02-09
### Added ### Added

View File

@ -56,7 +56,15 @@ RUN apt update && apt install -y \
openssl \ openssl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Add tini, which is an init process that handles signaling within the container
# and with the host. See https://github.com/krallin/tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
COPY ./docker/entrypoint.sh /ghostfolio/entrypoint.sh
WORKDIR /ghostfolio/apps/api WORKDIR /ghostfolio/apps/api
EXPOSE ${PORT:-3333} EXPOSE ${PORT:-3333}
CMD [ "yarn", "start:production" ] CMD [ "/ghostfolio/entrypoint.sh" ]

View File

@ -7,7 +7,7 @@
**Open Source Wealth Management Software** **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) | [**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://twitter.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: 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: Contributions Welcome](https://img.shields.io/badge/Contributions-Welcome-orange.svg)](#contributing)
@ -99,6 +99,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database | | `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database | | `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database | | `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
| `REDIS_DB` | `0` | The database index of _Redis_ |
| `REDIS_HOST` | | The host where _Redis_ is running | | `REDIS_HOST` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | | The password of _Redis_ | | `REDIS_PASSWORD` | | The password of _Redis_ |
| `REDIS_PORT` | | The port where _Redis_ is running | | `REDIS_PORT` | | The port where _Redis_ is running |
@ -143,7 +144,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
### Home Server Systems (Community) ### Home Server Systems (Community)
Ghostfolio is available for various home server systems, including [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio). Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
## Development ## Development
@ -153,7 +154,7 @@ Ghostfolio is available for various home server systems, including [Runtipi](htt
- [Node.js](https://nodejs.org/en/download) (version 18+) - [Node.js](https://nodejs.org/en/download) (version 18+)
- [Yarn](https://yarnpkg.com/en/docs/install) - [Yarn](https://yarnpkg.com/en/docs/install)
- Create a local copy of this Git repository (clone) - 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 ### Setup

View File

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

View File

@ -9,12 +9,13 @@
"build": { "build": {
"executor": "@nx/webpack:webpack", "executor": "@nx/webpack:webpack",
"options": { "options": {
"outputPath": "dist/apps/api",
"main": "apps/api/src/main.ts",
"tsConfig": "apps/api/tsconfig.app.json",
"assets": ["apps/api/src/assets"],
"target": "node",
"compiler": "tsc", "compiler": "tsc",
"deleteOutputPath": false,
"main": "apps/api/src/main.ts",
"outputPath": "dist/apps/api",
"sourceMap": true,
"target": "node",
"tsConfig": "apps/api/tsconfig.app.json",
"webpackConfig": "apps/api/webpack.config.js" "webpackConfig": "apps/api/webpack.config.js"
}, },
"configurations": { "configurations": {
@ -33,6 +34,26 @@
}, },
"outputs": ["{options.outputPath}"] "outputs": ["{options.outputPath}"]
}, },
"copy-assets": {
"executor": "nx:run-commands",
"options": {
"commands": [
{
"command": "shx rm -rf dist/apps/api"
},
{
"command": "shx mkdir -p dist/apps/api/assets/locales"
},
{
"command": "shx cp -r apps/api/src/assets/* dist/apps/api/assets"
},
{
"command": "shx cp -r apps/client/src/locales/* dist/apps/api/assets/locales"
}
],
"parallel": false
}
},
"serve": { "serve": {
"executor": "@nx/js:node", "executor": "@nx/js:node",
"options": { "options": {

View File

@ -4,6 +4,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { Access } from '@ghostfolio/common/interfaces'; import { Access } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -82,7 +83,7 @@ export class AccessController {
} }
try { try {
return await this.accessService.createAccess({ return this.accessService.createAccess({
alias: data.alias || undefined, alias: data.alias || undefined,
GranteeUser: data.granteeUserId GranteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }

View File

@ -1,5 +1,6 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AccessController } from './access.controller'; import { AccessController } from './access.controller';

View File

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

View File

@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
Delete, Delete,

View File

@ -1,5 +1,6 @@
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AccountBalanceController } from './account-balance.controller'; import { AccountBalanceController } from './account-balance.controller';

View File

@ -2,6 +2,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces'; import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types'; import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AccountBalance, Prisma } from '@prisma/client'; import { AccountBalance, Prisma } from '@prisma/client';

View File

@ -14,6 +14,7 @@ import type {
AccountWithValue, AccountWithValue,
RequestWithUser RequestWithUser
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -62,7 +63,7 @@ export class AccountController {
{ Order: true } { Order: true }
); );
if (account?.isDefault || account?.Order.length > 0) { if (!account || account?.Order.length > 0) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN

View File

@ -7,6 +7,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AccountController } from './account.controller'; import { AccountController } from './account.controller';

View File

@ -2,9 +2,10 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Account, Order, Platform, Prisma } from '@prisma/client'; import { Account, Order, Platform, Prisma } from '@prisma/client';
import Big from 'big.js'; import { Big } from 'big.js';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import { CashDetails } from './interfaces/cash-details.interface'; import { CashDetails } from './interfaces/cash-details.interface';
@ -20,10 +21,8 @@ export class AccountService {
public async account({ public async account({
id_userId id_userId
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> { }: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
const { id, userId } = id_userId;
const [account] = await this.accounts({ const [account] = await this.accounts({
where: { id, userId } where: id_userId
}); });
return account; return account;

View File

@ -1,6 +1,7 @@
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsISO4217CurrencyCode,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
@ -19,7 +20,7 @@ export class CreateAccountDto {
) )
comment?: string; comment?: string;
@IsString() @IsISO4217CurrencyCode()
currency: string; currency: string;
@IsOptional() @IsOptional()

View File

@ -1,6 +1,7 @@
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsISO4217CurrencyCode,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
@ -19,7 +20,7 @@ export class UpdateAccountDto {
) )
comment?: string; comment?: string;
@IsString() @IsISO4217CurrencyCode()
currency: string; currency: string;
@IsString() @IsString()

View File

@ -25,6 +25,7 @@ import type {
MarketDataPreset, MarketDataPreset,
RequestWithUser RequestWithUser
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -255,7 +256,7 @@ export class AdminController {
dataSource, dataSource,
marketPrice, marketPrice,
symbol, symbol,
date: resetHours(parseISO(date)), date: parseISO(date),
state: 'CLOSE' state: 'CLOSE'
}) })
); );
@ -338,6 +339,6 @@ export class AdminController {
@Param('key') key: string, @Param('key') key: string,
@Body() data: PropertyDto @Body() data: PropertyDto
) { ) {
return await this.adminService.putSetting(key, data.value); return this.adminService.putSetting(key, data.value);
} }
} }

View File

@ -8,6 +8,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller'; import { AdminController } from './admin.controller';

View File

@ -22,6 +22,7 @@ import {
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types'; import { MarketDataPreset } from '@ghostfolio/common/types';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { import {
AssetSubClass, AssetSubClass,
@ -70,7 +71,7 @@ export class AdminService {
); );
} }
return await this.symbolProfileService.add( return this.symbolProfileService.add(
assetProfiles[symbol] as Prisma.SymbolProfileCreateInput assetProfiles[symbol] as Prisma.SymbolProfileCreateInput
); );
} catch (error) { } catch (error) {
@ -211,6 +212,7 @@ export class AdminService {
countries: true, countries: true,
currency: true, currency: true,
dataSource: true, dataSource: true,
id: true,
name: true, name: true,
Order: { Order: {
orderBy: [{ date: 'asc' }], orderBy: [{ date: 'asc' }],
@ -225,7 +227,7 @@ export class AdminService {
this.prismaService.symbolProfile.count({ where }) this.prismaService.symbolProfile.count({ where })
]); ]);
let marketData = assetProfiles.map( let marketData: AdminMarketDataItem[] = assetProfiles.map(
({ ({
_count, _count,
assetClass, assetClass,
@ -234,6 +236,7 @@ export class AdminService {
countries, countries,
currency, currency,
dataSource, dataSource,
id,
name, name,
Order, Order,
sectors, sectors,
@ -256,6 +259,7 @@ export class AdminService {
currency, currency,
countriesCount, countriesCount,
dataSource, dataSource,
id,
name, name,
symbol, symbol,
marketDataItemCount, marketDataItemCount,
@ -330,19 +334,35 @@ export class AdminService {
symbol, symbol,
symbolMapping symbolMapping
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) { }: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
await this.symbolProfileService.updateSymbolProfile({ const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput & UniqueAsset =
{
assetClass, assetClass,
assetSubClass, assetSubClass,
comment, comment,
countries, countries,
currency, currency,
dataSource, dataSource,
name,
scraperConfiguration, scraperConfiguration,
sectors, sectors,
symbol, symbol,
symbolMapping symbolMapping,
}); ...(dataSource === 'MANUAL'
? { name }
: {
SymbolProfileOverrides: {
upsert: {
create: {
name: name as string
},
update: {
name: name as string
}
}
}
})
};
await this.symbolProfileService.updateSymbolProfile(updatedSymbolProfile);
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([ const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
{ {
@ -396,6 +416,7 @@ export class AdminService {
assetClass: 'CASH', assetClass: 'CASH',
countriesCount: 0, countriesCount: 0,
currency: symbol.replace(DEFAULT_CURRENCY, ''), currency: symbol.replace(DEFAULT_CURRENCY, ''),
id: undefined,
name: symbol, name: symbol,
sectorsCount: 0 sectorsCount: 0
}; };
@ -439,13 +460,14 @@ export class AdminService {
}, },
createdAt: true, createdAt: true,
id: true, id: true,
role: true,
Subscription: true Subscription: true
}, },
take: 30 take: 30
}); });
return usersWithAnalytics.map( return usersWithAnalytics.map(
({ _count, Analytics, createdAt, id, Subscription }) => { ({ _count, Analytics, createdAt, id, role, Subscription }) => {
const daysSinceRegistration = const daysSinceRegistration =
differenceInDays(new Date(), createdAt) + 1; differenceInDays(new Date(), createdAt) + 1;
const engagement = Analytics const engagement = Analytics
@ -455,13 +477,17 @@ export class AdminService {
const subscription = this.configurationService.get( const subscription = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION' 'ENABLE_FEATURE_SUBSCRIPTION'
) )
? this.subscriptionService.getSubscription(Subscription) ? this.subscriptionService.getSubscription({
createdAt,
subscriptions: Subscription
})
: undefined; : undefined;
return { return {
createdAt, createdAt,
engagement, engagement,
id, id,
role,
subscription, subscription,
accountCount: _count.Account || 0, accountCount: _count.Account || 0,
country: Analytics?.country, country: Analytics?.country,

View File

@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AdminJobs } from '@ghostfolio/common/interfaces'; import { AdminJobs } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { import {
Controller, Controller,
Delete, Delete,

View File

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

View File

@ -3,6 +3,7 @@ import {
QUEUE_JOB_STATUS_LIST QUEUE_JOB_STATUS_LIST
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { AdminJobs } from '@ghostfolio/common/interfaces'; import { AdminJobs } from '@ghostfolio/common/interfaces';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { JobStatus, Queue } from 'bull'; import { JobStatus, Queue } from 'bull';

View File

@ -2,6 +2,7 @@ import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
import { import {
IsArray, IsArray,
IsEnum, IsEnum,
IsISO4217CurrencyCode,
IsObject, IsObject,
IsOptional, IsOptional,
IsString IsString
@ -24,7 +25,7 @@ export class UpdateAssetProfileDto {
@IsOptional() @IsOptional()
countries?: Prisma.InputJsonArray; countries?: Prisma.InputJsonArray;
@IsString() @IsISO4217CurrencyCode()
@IsOptional() @IsOptional()
currency?: string; currency?: string;

View File

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

View File

@ -1,22 +1,23 @@
import { join } from 'path';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { CronService } from '@ghostfolio/api/services/cron.service'; import { CronService } from '@ghostfolio/api/services/cron.service';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module'; import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import { import {
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { join } from 'path';
import { AccessModule } from './access/access.module'; import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module'; import { AccountModule } from './account/account.module';
@ -52,6 +53,7 @@ import { UserModule } from './user/user.module';
BenchmarkModule, BenchmarkModule,
BullModule.forRoot({ BullModule.forRoot({
redis: { redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),
host: process.env.REDIS_HOST, host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT ?? '6379', 10), port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
password: process.env.REDIS_PASSWORD password: process.env.REDIS_PASSWORD
@ -73,6 +75,7 @@ import { UserModule } from './user/user.module';
PlatformModule, PlatformModule,
PortfolioModule, PortfolioModule,
PrismaModule, PrismaModule,
PropertyModule,
RedisCacheModule, RedisCacheModule,
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({

View File

@ -2,6 +2,7 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.s
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { Controller, Delete, Param, UseGuards } from '@nestjs/common'; import { Controller, Delete, Param, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';

View File

@ -2,6 +2,7 @@ import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-devic
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';

View File

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

View File

@ -3,6 +3,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { OAuthResponse } from '@ghostfolio/common/interfaces'; import { OAuthResponse } from '@ghostfolio/common/interfaces';
import { import {
Body, Body,
Controller, Controller,

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 { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';

View File

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

View File

@ -1,4 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client'; import { Provider } from '@prisma/client';

View File

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

View File

@ -2,6 +2,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config'; import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import * as countriesAndTimezones from 'countries-and-timezones'; import * as countriesAndTimezones from 'countries-and-timezones';

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 { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Inject, Inject,
Injectable, Injectable,
@ -40,7 +41,7 @@ export class WebAuthService {
) {} ) {}
get rpID() { get rpID() {
return this.configurationService.get('WEB_AUTH_RP_ID'); return new URL(this.configurationService.get('ROOT_URL')).hostname;
} }
get expectedOrigin() { get expectedOrigin() {

View File

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import type { import type {
@ -8,7 +9,8 @@ import type {
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -18,6 +20,7 @@ import {
Inject, Inject,
Param, Param,
Post, Post,
Query,
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
@ -105,13 +108,18 @@ export class BenchmarkController {
public async getBenchmarkMarketDataBySymbol( public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string, @Param('startDateString') startDateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string,
@Query('range') dateRange: DateRange = 'max'
): Promise<BenchmarkMarketDataDetails> { ): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString); const { endDate, startDate } = getInterval(
dateRange,
new Date(startDateString)
);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
return this.benchmarkService.getMarketDataBySymbol({ return this.benchmarkService.getMarketDataBySymbol({
dataSource, dataSource,
endDate,
startDate, startDate,
symbol, symbol,
userCurrency userCurrency

View File

@ -7,6 +7,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BenchmarkController } from './benchmark.controller'; import { BenchmarkController } from './benchmark.controller';

View File

@ -13,7 +13,8 @@ import {
import { import {
DATE_FORMAT, DATE_FORMAT,
calculateBenchmarkTrend, calculateBenchmarkTrend,
parseDate parseDate,
resetHours
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
Benchmark, Benchmark,
@ -23,10 +24,17 @@ import {
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { BenchmarkTrend } from '@ghostfolio/common/types'; import { BenchmarkTrend } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import Big from 'big.js'; import { Big } from 'big.js';
import { format, isSameDay, subDays } from 'date-fns'; import {
differenceInDays,
eachDayOfInterval,
format,
isSameDay,
subDays
} from 'date-fns';
import { isNumber, last, uniqBy } from 'lodash'; import { isNumber, last, uniqBy } from 'lodash';
import ms from 'ms'; import ms from 'ms';
@ -109,7 +117,9 @@ export class BenchmarkService {
const quotes = await this.dataProviderService.getQuotes({ const quotes = await this.dataProviderService.getQuotes({
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => { items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
return { dataSource, symbol }; return { dataSource, symbol };
}) }),
requestTimeout: ms('30 seconds'),
useCache: false
}); });
for (const { dataSource, symbol } of benchmarkAssetProfiles) { for (const { dataSource, symbol } of benchmarkAssetProfiles) {
@ -162,7 +172,7 @@ export class BenchmarkService {
await this.redisCacheService.set( await this.redisCacheService.set(
this.CACHE_KEY_BENCHMARKS, this.CACHE_KEY_BENCHMARKS,
JSON.stringify(benchmarks), JSON.stringify(benchmarks),
ms('4 hours') / 1000 ms('2 hours') / 1000
); );
} }
@ -205,15 +215,28 @@ export class BenchmarkService {
public async getMarketDataBySymbol({ public async getMarketDataBySymbol({
dataSource, dataSource,
endDate = new Date(),
startDate, startDate,
symbol, symbol,
userCurrency userCurrency
}: { }: {
endDate?: Date;
startDate: Date; startDate: Date;
userCurrency: string; userCurrency: string;
} & UniqueAsset): Promise<BenchmarkMarketDataDetails> { } & UniqueAsset): Promise<BenchmarkMarketDataDetails> {
const marketData: { date: string; value: number }[] = []; 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([ const [currentSymbolItem, marketDataItems] = await Promise.all([
this.symbolService.get({ this.symbolService.get({
dataGatheringItem: { dataGatheringItem: {
@ -229,7 +252,7 @@ export class BenchmarkService {
dataSource, dataSource,
symbol, symbol,
date: { date: {
gte: startDate in: dates
} }
} }
}) })
@ -263,17 +286,7 @@ export class BenchmarkService {
return { marketData }; return { marketData };
} }
const step = Math.round(
marketDataItems.length / Math.min(marketDataItems.length, MAX_CHART_ITEMS)
);
let i = 0;
for (let marketDataItem of marketDataItems) { for (let marketDataItem of marketDataItems) {
if (i % step !== 0) {
continue;
}
const exchangeRate = const exchangeRate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(marketDataItem.date, DATE_FORMAT) format(marketDataItem.date, DATE_FORMAT)
@ -296,15 +309,15 @@ export class BenchmarkService {
}); });
} }
const includesToday = isSameDay( const includesEndDate = isSameDay(
parseDate(last(marketData).date), parseDate(last(marketData).date),
new Date() endDate
); );
if (currentSymbolItem?.marketPrice && !includesToday) { if (currentSymbolItem?.marketPrice && !includesEndDate) {
const exchangeRate = const exchangeRate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(new Date(), DATE_FORMAT) format(endDate, DATE_FORMAT)
]; ];
const exchangeRateFactor = const exchangeRateFactor =
@ -313,7 +326,7 @@ export class BenchmarkService {
: 1; : 1;
marketData.push({ marketData.push({
date: format(new Date(), DATE_FORMAT), date: format(endDate, DATE_FORMAT),
value: value:
this.calculateChangeInPercentage( this.calculateChangeInPercentage(
marketPriceAtStartDate, marketPriceAtStartDate,

View File

@ -2,6 +2,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { Controller, Post, UseGuards } from '@nestjs/common'; import { Controller, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';

View File

@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CacheController } from './cache.controller'; import { CacheController } from './cache.controller';

View File

@ -1,5 +1,6 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
Controller, Controller,
Get, Get,

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { Export } from '@ghostfolio/common/interfaces'; import { Export } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';

View File

@ -5,6 +5,7 @@ import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ExportController } from './export.controller'; import { ExportController } from './export.controller';

View File

@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { Filter, Export } from '@ghostfolio/common/interfaces'; import { Filter, Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
@ -94,7 +95,10 @@ export class ExportService {
: SymbolProfile.symbol : 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.interceptor';
import { import {
Controller, Controller,
Get, Get,

View File

@ -1,6 +1,7 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module'; import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { HealthController } from './health.controller'; import { HealthController } from './health.controller';

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { ImportResponse } from '@ghostfolio/common/interfaces'; import { ImportResponse } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -42,8 +43,10 @@ export class ImportController {
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async import( public async import(
@Body() importData: ImportDataDto, @Body() importData: ImportDataDto,
@Query('dryRun') isDryRun?: boolean @Query('dryRun') isDryRunParam = 'false'
): Promise<ImportResponse> { ): Promise<ImportResponse> {
const isDryRun = isDryRunParam === 'true';
if ( if (
!hasPermission(this.request.user.permissions, permissions.createAccount) !hasPermission(this.request.user.permissions, permissions.createAccount)
) { ) {

View File

@ -10,6 +10,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ImportController } from './import.controller'; import { ImportController } from './import.controller';

View File

@ -24,9 +24,10 @@ import {
OrderWithAccount, OrderWithAccount,
UserWithSettings UserWithSettings
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import Big from 'big.js'; import { Big } from 'big.js';
import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns'; import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -116,7 +117,7 @@ export class ImportService {
feeInBaseCurrency: 0, feeInBaseCurrency: 0,
id: assetProfile.id, id: assetProfile.id,
isDraft: false, isDraft: false,
SymbolProfile: <SymbolProfile>(<unknown>assetProfile), SymbolProfile: assetProfile,
symbolProfileId: assetProfile.id, symbolProfileId: assetProfile.id,
type: 'DIVIDEND', type: 'DIVIDEND',
unitPrice: marketPrice, unitPrice: marketPrice,
@ -520,22 +521,14 @@ export class ImportService {
currency, currency,
dataSource, dataSource,
symbol, symbol,
assetClass: null, activitiesCount: undefined,
assetSubClass: null, assetClass: undefined,
comment: null, assetSubClass: undefined,
countries: null, countries: undefined,
createdAt: undefined, createdAt: undefined,
figi: null,
figiComposite: null,
figiShareClass: null,
id: undefined, id: undefined,
isin: null, sectors: undefined,
name: null, updatedAt: undefined
scraperConfiguration: null,
sectors: null,
symbolMapping: null,
updatedAt: undefined,
url: null
} }
}; };
} }
@ -569,17 +562,10 @@ export class ImportService {
[assetProfileIdentifier: string]: Partial<SymbolProfile>; [assetProfileIdentifier: string]: Partial<SymbolProfile>;
} = {}; } = {};
const uniqueActivitiesDto = uniqBy(
activitiesDto,
({ dataSource, symbol }) => {
return getAssetProfileIdentifier({ dataSource, symbol });
}
);
for (const [ for (const [
index, index,
{ currency, dataSource, symbol, type } { currency, dataSource, symbol, type }
] of uniqueActivitiesDto.entries()) { ] of activitiesDto.entries()) {
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) { if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
throw new Error( throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid` `activities.${index}.dataSource ("${dataSource}") is not valid`
@ -601,6 +587,7 @@ export class ImportService {
} }
} }
if (!assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })]) {
const assetProfile = { const assetProfile = {
currency, currency,
...( ...(
@ -617,15 +604,9 @@ export class ImportService {
); );
} }
if ( if (assetProfile.currency !== currency) {
assetProfile.currency !== currency &&
!this.exchangeRateDataService.hasCurrencyPair(
currency,
assetProfile.currency
)
) {
throw new Error( throw new Error(
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"` `activities.${index}.currency ("${currency}") does not match with currency of ${assetProfile.symbol} ("${assetProfile.currency}")`
); );
} }
} }
@ -633,6 +614,7 @@ export class ImportService {
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
assetProfile; assetProfile;
} }
}
return assetProfiles; return assetProfiles;
} }

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.interceptor';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { InfoService } from './info.service'; import { InfoService } from './info.service';

View File

@ -10,6 +10,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';

View File

@ -28,6 +28,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOffer } from '@ghostfolio/common/types'; import { SubscriptionOffer } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
@ -59,10 +60,6 @@ export class InfoService {
const globalPermissions: string[] = []; const globalPermissions: string[] = [];
if (this.configurationService.get('ENABLE_FEATURE_BLOG')) {
globalPermissions.push(permissions.enableBlog);
}
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
info.fearAndGreedDataSource = encodeDataSource( info.fearAndGreedDataSource = encodeDataSource(
@ -351,7 +348,7 @@ export class InfoService {
{ {
headers: { headers: {
Authorization: `Bearer ${this.configurationService.get( Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY' 'API_KEY_BETTER_UPTIME'
)}` )}`
}, },
// @ts-ignore // @ts-ignore

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.interceptor';
import { import {
Controller, Controller,
Get, Get,

View File

@ -1,5 +1,6 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { LogoController } from './logo.controller'; import { LogoController } from './logo.controller';

View File

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import got from 'got'; import got from 'got';

View File

@ -10,6 +10,7 @@ import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsEnum, IsEnum,
IsISO4217CurrencyCode,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
IsOptional, IsOptional,
@ -38,7 +39,7 @@ export class CreateOrderDto {
) )
comment?: string; comment?: string;
@IsString() @IsISO4217CurrencyCode()
currency: string; currency: string;
@IsOptional() @IsOptional()

View File

@ -1,13 +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 { export interface Activities {
activities: Activity[]; activities: Activity[];
count: number; count: number;
} }
export interface Activity extends OrderWithAccount { export interface Activity extends Order {
Account?: AccountWithPlatform;
error?: ActivityError; error?: ActivityError;
feeInBaseCurrency: number; feeInBaseCurrency: number;
SymbolProfile?: EnhancedSymbolProfile;
tags?: Tag[];
updateAccountBalance?: boolean; updateAccountBalance?: boolean;
value: number; value: number;
valueInBaseCurrency: number; valueInBaseCurrency: number;

View File

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
@ -8,7 +9,8 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -83,6 +85,7 @@ export class OrderController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('skip') skip?: number, @Query('skip') skip?: number,
@Query('sortColumn') sortColumn?: string, @Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('sortDirection') sortDirection?: Prisma.SortOrder,
@ -95,14 +98,18 @@ export class OrderController {
filterByTags filterByTags
}); });
const { endDate, startDate } = getInterval(dateRange);
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
const { activities, count } = await this.orderService.getOrders({ const { activities, count } = await this.orderService.getOrders({
endDate,
filters, filters,
sortColumn, sortColumn,
sortDirection, sortDirection,
startDate,
userCurrency, userCurrency,
includeDrafts: true, includeDrafts: true,
skip: isNaN(skip) ? undefined : skip, skip: isNaN(skip) ? undefined : skip,

View File

@ -11,6 +11,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { OrderController } from './order.controller'; import { OrderController } from './order.controller';

View File

@ -8,8 +8,9 @@ import {
GATHER_ASSET_PROFILE_PROCESS_OPTIONS GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
AssetClass, AssetClass,
@ -18,11 +19,11 @@ import {
Order, Order,
Prisma, Prisma,
Tag, Tag,
Type as TypeOfOrder Type as ActivityType
} from '@prisma/client'; } from '@prisma/client';
import Big from 'big.js'; import { Big } from 'big.js';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { groupBy } from 'lodash'; import { groupBy, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Activities } from './interfaces/activities.interface'; import { Activities } from './interfaces/activities.interface';
@ -69,12 +70,7 @@ export class OrderService {
const updateAccountBalance = data.updateAccountBalance ?? false; const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId; const userId = data.userId;
if ( if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) {
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
const assetClass = data.assetClass; const assetClass = data.assetClass;
const assetSubClass = data.assetSubClass; const assetSubClass = data.assetSubClass;
currency = data.SymbolProfile.connectOrCreate.create.currency; currency = data.SymbolProfile.connectOrCreate.create.currency;
@ -129,11 +125,7 @@ export class OrderService {
const orderData: Prisma.OrderCreateInput = data; const orderData: Prisma.OrderCreateInput = data;
const isDraft = const isDraft = ['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
? false ? false
: isAfter(data.date as Date, endOfToday()); : isAfter(data.date as Date, endOfToday());
@ -156,7 +148,7 @@ export class OrderService {
.plus(data.fee) .plus(data.fee)
.toNumber(); .toNumber();
if (data.type === 'BUY') { if (['BUY', 'FEE'].includes(data.type)) {
amount = new Big(amount).mul(-1).toNumber(); amount = new Big(amount).mul(-1).toNumber();
} }
@ -179,12 +171,7 @@ export class OrderService {
where where
}); });
if ( if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type)) {
order.type === 'FEE' ||
order.type === 'INTEREST' ||
order.type === 'ITEM' ||
order.type === 'LIABILITY'
) {
await this.symbolProfileService.deleteById(order.symbolProfileId); await this.symbolProfileService.deleteById(order.symbolProfileId);
} }
@ -199,25 +186,40 @@ export class OrderService {
return count; return count;
} }
public async getLatestOrder({ dataSource, symbol }: UniqueAsset) {
return this.prismaService.order.findFirst({
orderBy: {
date: 'desc'
},
where: {
SymbolProfile: { dataSource, symbol }
}
});
}
public async getOrders({ public async getOrders({
endDate,
filters, filters,
includeDrafts = false, includeDrafts = false,
skip, skip,
sortColumn, sortColumn,
sortDirection, sortDirection,
startDate,
take = Number.MAX_SAFE_INTEGER, take = Number.MAX_SAFE_INTEGER,
types, types,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts = false withExcludedAccounts = false
}: { }: {
endDate?: Date;
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
skip?: number; skip?: number;
sortColumn?: string; sortColumn?: string;
sortDirection?: Prisma.SortOrder; sortDirection?: Prisma.SortOrder;
startDate?: Date;
take?: number; take?: number;
types?: TypeOfOrder[]; types?: ActivityType[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
@ -227,6 +229,18 @@ export class OrderService {
]; ];
const where: Prisma.OrderWhereInput = { userId }; 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 { const {
ACCOUNT: filtersByAccount, ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass, ASSET_CLASS: filtersByAssetClass,
@ -291,13 +305,14 @@ export class OrderService {
} }
if (types) { if (types) {
where.OR = types.map((type) => { where.type = { in: types };
return {
type: {
equals: type
} }
};
}); if (withExcludedAccounts === false) {
where.OR = [
{ Account: null },
{ Account: { NOT: { isExcluded: true } } }
];
} }
const [orders, count] = await Promise.all([ const [orders, count] = await Promise.all([
@ -321,25 +336,45 @@ export class OrderService {
this.prismaService.order.count({ where }) this.prismaService.order.count({ where })
]); ]);
const activities = orders const uniqueAssets = uniqBy(
.filter((order) => { orders.map(({ SymbolProfile }) => {
return ( return {
withExcludedAccounts || dataSource: SymbolProfile.dataSource,
!order.Account || symbol: SymbolProfile.symbol
order.Account?.isExcluded === false };
}),
({ dataSource, symbol }) => {
return getAssetProfileIdentifier({
dataSource,
symbol
});
}
); );
})
.map((order) => { 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(); const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
return { return {
...order, ...order,
value, value,
// TODO: Use exchange rate of date
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.SymbolProfile.currency, order.SymbolProfile.currency,
userCurrency userCurrency
), ),
SymbolProfile: assetProfile,
// TODO: Use exchange rate of date
valueInBaseCurrency: this.exchangeRateDataService.toCurrency( valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value, value,
order.SymbolProfile.currency, order.SymbolProfile.currency,
@ -370,13 +405,10 @@ export class OrderService {
dataSource?: DataSource; dataSource?: DataSource;
symbol?: string; symbol?: string;
tags?: Tag[]; tags?: Tag[];
type?: ActivityType;
}; };
where: Prisma.OrderWhereUniqueInput; where: Prisma.OrderWhereUniqueInput;
}): Promise<Order> { }): Promise<Order> {
if (data.Account.connect.id_userId.id === null) {
delete data.Account;
}
if (!data.comment) { if (!data.comment) {
data.comment = null; data.comment = null;
} }
@ -385,13 +417,12 @@ export class OrderService {
let isDraft = false; let isDraft = false;
if ( if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) {
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
delete data.SymbolProfile.connect; delete data.SymbolProfile.connect;
if (data.Account?.connect?.id_userId?.id === null) {
data.Account = { disconnect: true };
}
} else { } else {
delete data.SymbolProfile.update; delete data.SymbolProfile.update;

View File

@ -9,6 +9,7 @@ import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsArray, IsArray,
IsEnum, IsEnum,
IsISO4217CurrencyCode,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
IsOptional, IsOptional,
@ -37,7 +38,7 @@ export class UpdateOrderDto {
) )
comment?: string; comment?: string;
@IsString() @IsISO4217CurrencyCode()
currency: string; currency: string;
@IsString() @IsString()

View File

@ -1,6 +1,7 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { import {
Body, Body,
Controller, Controller,

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock'; import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -20,7 +22,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -35,35 +37,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'CHF', activities: <Activity[]>[
orders: [
{ {
date: new Date('2021-11-22'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
currency: 'CHF', currency: 'CHF',
date: '2021-11-22',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(1.55),
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
quantity: new Big(2), symbol: 'BALN.SW'
symbol: 'BALN.SW', },
type: 'BUY', type: 'BUY',
unitPrice: new Big(142.9) unitPrice: 142.9
}, },
{ {
date: new Date('2021-11-30'),
fee: 1.65,
quantity: 2,
SymbolProfile: {
currency: 'CHF', currency: 'CHF',
date: '2021-11-30',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(1.65),
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
quantity: new Big(2), symbol: 'BALN.SW'
symbol: 'BALN.SW', },
type: 'SELL', type: 'SELL',
unitPrice: new Big(136.6) unitPrice: 136.6
} }
] ],
currency: 'CHF'
}); });
portfolioCalculator.computeTransactionPoints();
const spy = jest const spy = jest
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
@ -86,7 +90,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.0440867739678096571'), grossPerformancePercentage: new Big('-0.0440867739678096571'),
@ -106,6 +110,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'), fee: new Big('3.2'),
firstBuyDate: '2021-11-22', firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
@ -128,7 +134,8 @@ describe('PortfolioCalculator', () => {
symbol: 'BALN.SW', symbol: 'BALN.SW',
timeWeightedInvestment: new Big('285.8'), timeWeightedInvestment: new Big('285.8'),
timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'), timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'),
transactionCount: 2 transactionCount: 2,
valueInBaseCurrency: new Big('0')
} }
], ],
totalInvestment: new Big('0'), totalInvestment: new Big('0'),

View File

@ -1,9 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock'; import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -20,7 +22,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -35,24 +37,24 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'CHF', activities: <Activity[]>[
orders: [
{ {
date: new Date('2021-11-30'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
currency: 'CHF', currency: 'CHF',
date: '2021-11-30',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(1.55),
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
quantity: new Big(2), symbol: 'BALN.SW'
symbol: 'BALN.SW', },
type: 'BUY', type: 'BUY',
unitPrice: new Big(136.6) unitPrice: 136.6
} }
] ],
currency: 'CHF'
}); });
portfolioCalculator.computeTransactionPoints();
const spy = jest const spy = jest
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
@ -75,7 +77,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('297.8'), currentValueInBaseCurrency: new Big('297.8'),
errors: [], errors: [],
grossPerformance: new Big('24.6'), grossPerformance: new Big('24.6'),
grossPerformancePercentage: new Big('0.09004392386530014641'), grossPerformancePercentage: new Big('0.09004392386530014641'),
@ -95,6 +97,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('136.6'), averagePrice: new Big('136.6'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1.55'), fee: new Big('1.55'),
firstBuyDate: '2021-11-30', firstBuyDate: '2021-11-30',
grossPerformance: new Big('24.6'), grossPerformance: new Big('24.6'),
@ -117,7 +121,8 @@ describe('PortfolioCalculator', () => {
symbol: 'BALN.SW', symbol: 'BALN.SW',
timeWeightedInvestment: new Big('273.2'), timeWeightedInvestment: new Big('273.2'),
timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'), timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'),
transactionCount: 1 transactionCount: 1,
valueInBaseCurrency: new Big('297.8')
} }
], ],
totalInvestment: new Big('273.2'), totalInvestment: new Big('273.2'),

View File

@ -1,10 +1,12 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock'; import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -33,7 +35,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -48,35 +50,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'CHF', activities: <Activity[]>[
orders: [
{ {
date: new Date('2015-01-01'),
fee: 0,
quantity: 2,
SymbolProfile: {
currency: 'USD', currency: 'USD',
date: '2015-01-01',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(0),
name: 'Bitcoin USD', name: 'Bitcoin USD',
quantity: new Big(2), symbol: 'BTCUSD'
symbol: 'BTCUSD', },
type: 'BUY', type: 'BUY',
unitPrice: new Big(320.43) unitPrice: 320.43
}, },
{ {
date: new Date('2017-12-31'),
fee: 0,
quantity: 1,
SymbolProfile: {
currency: 'USD', currency: 'USD',
date: '2017-12-31',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(0),
name: 'Bitcoin USD', name: 'Bitcoin USD',
quantity: new Big(1), symbol: 'BTCUSD'
symbol: 'BTCUSD', },
type: 'SELL', type: 'SELL',
unitPrice: new Big(14156.4) unitPrice: 14156.4
} }
] ],
currency: 'CHF'
}); });
portfolioCalculator.computeTransactionPoints();
const spy = jest const spy = jest
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2018-01-01').getTime()); .mockImplementation(() => parseDate('2018-01-01').getTime());
@ -99,7 +103,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('13298.425356'), currentValueInBaseCurrency: new Big('13298.425356'),
errors: [], errors: [],
grossPerformance: new Big('27172.74'), grossPerformance: new Big('27172.74'),
grossPerformancePercentage: new Big('42.41978276196153750666'), grossPerformancePercentage: new Big('42.41978276196153750666'),
@ -119,6 +123,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('320.43'), averagePrice: new Big('320.43'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
firstBuyDate: '2015-01-01', firstBuyDate: '2015-01-01',
grossPerformance: new Big('27172.74'), grossPerformance: new Big('27172.74'),
@ -148,7 +154,8 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'636.79469348020066587024' '636.79469348020066587024'
), ),
transactionCount: 2 transactionCount: 2,
valueInBaseCurrency: new Big('13298.425356')
} }
], ],
totalInvestment: new Big('320.43'), totalInvestment: new Big('320.43'),

View File

@ -1,10 +1,12 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock'; import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -33,7 +35,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -48,24 +50,24 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'CHF', activities: <Activity[]>[
orders: [
{ {
date: new Date('2023-01-03'),
fee: 1,
quantity: 1,
SymbolProfile: {
currency: 'USD', currency: 'USD',
date: '2023-01-03',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(1),
name: 'Alphabet Inc.', name: 'Alphabet Inc.',
quantity: new Big(1), symbol: 'GOOGL'
symbol: 'GOOGL', },
type: 'BUY', type: 'BUY',
unitPrice: new Big(89.12) unitPrice: 89.12
} }
] ],
currency: 'CHF'
}); });
portfolioCalculator.computeTransactionPoints();
const spy = jest const spy = jest
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime()); .mockImplementation(() => parseDate('2023-07-10').getTime());
@ -88,7 +90,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('103.10483'), currentValueInBaseCurrency: new Big('103.10483'),
errors: [], errors: [],
grossPerformance: new Big('27.33'), grossPerformance: new Big('27.33'),
grossPerformancePercentage: new Big('0.3066651705565529623'), grossPerformancePercentage: new Big('0.3066651705565529623'),
@ -108,6 +110,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('89.12'), averagePrice: new Big('89.12'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1'), fee: new Big('1'),
firstBuyDate: '2023-01-03', firstBuyDate: '2023-01-03',
grossPerformance: new Big('27.33'), grossPerformance: new Big('27.33'),
@ -131,7 +135,8 @@ describe('PortfolioCalculator', () => {
tags: undefined, tags: undefined,
timeWeightedInvestment: new Big('89.12'), timeWeightedInvestment: new Big('89.12'),
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'), timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'),
transactionCount: 1 transactionCount: 1,
valueInBaseCurrency: new Big('103.10483')
} }
], ],
totalInvestment: new Big('89.12'), totalInvestment: new Big('89.12'),

View File

@ -0,0 +1,121 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
describe('PortfolioCalculator', () => {
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
});
describe('get current positions', () => {
it.only('with MSFT buy', async () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
activities: <Activity[]>[
{
date: new Date('2021-09-16'),
fee: 19,
quantity: 1,
SymbolProfile: {
currency: 'USD',
dataSource: 'YAHOO',
name: 'Microsoft Inc.',
symbol: 'MSFT'
},
type: 'BUY',
unitPrice: 298.58
},
{
date: new Date('2021-11-16'),
fee: 0,
quantity: 1,
SymbolProfile: {
currency: 'USD',
dataSource: 'YAHOO',
name: 'Microsoft Inc.',
symbol: 'MSFT'
},
type: 'DIVIDEND',
unitPrice: 0.62
}
],
currency: 'USD'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2023-07-10')
);
spy.mockRestore();
expect(currentPositions).toMatchObject({
errors: [],
hasErrors: false,
positions: [
{
averagePrice: new Big('298.58'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0.62'),
dividendInBaseCurrency: new Big('0.62'),
fee: new Big('19'),
firstBuyDate: '2021-09-16',
investment: new Big('298.58'),
investmentWithCurrencyEffect: new Big('298.58'),
marketPrice: 331.83,
marketPriceInBaseCurrency: 331.83,
quantity: new Big('1'),
symbol: 'MSFT',
tags: undefined,
transactionCount: 2
}
],
totalInvestment: new Big('298.58'),
totalInvestmentWithCurrencyEffect: new Big('298.58')
});
});
});
});

View File

@ -1,9 +1,11 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock'; import { Big } from 'big.js';
import { subDays } from 'date-fns';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -20,7 +22,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -35,23 +37,20 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'CHF', activities: [],
orders: [] currency: 'CHF'
}); });
portfolioCalculator.computeTransactionPoints();
const spy = jest const spy = jest
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({ const start = subDays(new Date(Date.now()), 10);
start: new Date()
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const chartData = await portfolioCalculator.getChartData({ start });
new Date()
); const currentPositions =
await portfolioCalculator.getCurrentPositions(start);
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
@ -63,7 +62,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big(0), currentValueInBaseCurrency: new Big(0),
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0),

View File

@ -1,9 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock'; import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -20,7 +22,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -35,35 +37,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'CHF', activities: <Activity[]>[
orders: [
{ {
date: new Date('2022-03-07'),
fee: 1.3,
quantity: 2,
SymbolProfile: {
currency: 'CHF', currency: 'CHF',
date: '2022-03-07',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(1.3),
name: 'Novartis AG', name: 'Novartis AG',
quantity: new Big(2), symbol: 'NOVN.SW'
symbol: 'NOVN.SW', },
type: 'BUY', type: 'BUY',
unitPrice: new Big(75.8) unitPrice: 75.8
}, },
{ {
date: new Date('2022-04-08'),
fee: 2.95,
quantity: 1,
SymbolProfile: {
currency: 'CHF', currency: 'CHF',
date: '2022-04-08',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(2.95),
name: 'Novartis AG', name: 'Novartis AG',
quantity: new Big(1), symbol: 'NOVN.SW'
symbol: 'NOVN.SW', },
type: 'SELL', type: 'SELL',
unitPrice: new Big(85.73) unitPrice: 85.73
} }
] ],
currency: 'CHF'
}); });
portfolioCalculator.computeTransactionPoints();
const spy = jest const spy = jest
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime()); .mockImplementation(() => parseDate('2022-04-11').getTime());
@ -86,7 +90,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('87.8'), currentValueInBaseCurrency: new Big('87.8'),
errors: [], errors: [],
grossPerformance: new Big('21.93'), grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.15113417083448194384'), grossPerformancePercentage: new Big('0.15113417083448194384'),
@ -106,6 +110,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('75.80'), averagePrice: new Big('75.80'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.25'), fee: new Big('4.25'),
firstBuyDate: '2022-03-07', firstBuyDate: '2022-03-07',
grossPerformance: new Big('21.93'), grossPerformance: new Big('21.93'),
@ -130,7 +136,8 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'145.10285714285714285714' '145.10285714285714285714'
), ),
transactionCount: 2 transactionCount: 2,
valueInBaseCurrency: new Big('87.8')
} }
], ],
totalInvestment: new Big('75.80'), totalInvestment: new Big('75.80'),

View File

@ -1,9 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock'; import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -20,7 +22,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -35,35 +37,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'CHF', activities: <Activity[]>[
orders: [
{ {
date: new Date('2022-03-07'),
fee: 0,
quantity: 2,
SymbolProfile: {
currency: 'CHF', currency: 'CHF',
date: '2022-03-07',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(0),
name: 'Novartis AG', name: 'Novartis AG',
quantity: new Big(2), symbol: 'NOVN.SW'
symbol: 'NOVN.SW', },
type: 'BUY', type: 'BUY',
unitPrice: new Big(75.8) unitPrice: 75.8
}, },
{ {
date: new Date('2022-04-08'),
fee: 0,
quantity: 2,
SymbolProfile: {
currency: 'CHF', currency: 'CHF',
date: '2022-04-08',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big(0),
name: 'Novartis AG', name: 'Novartis AG',
quantity: new Big(2), symbol: 'NOVN.SW'
symbol: 'NOVN.SW', },
type: 'SELL', type: 'SELL',
unitPrice: new Big(85.73) unitPrice: 85.73
} }
] ],
currency: 'CHF'
}); });
portfolioCalculator.computeTransactionPoints();
const spy = jest const spy = jest
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime()); .mockImplementation(() => parseDate('2022-04-11').getTime());
@ -112,7 +116,7 @@ describe('PortfolioCalculator', () => {
}); });
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
grossPerformance: new Big('19.86'), grossPerformance: new Big('19.86'),
grossPerformancePercentage: new Big('0.13100263852242744063'), grossPerformancePercentage: new Big('0.13100263852242744063'),
@ -132,6 +136,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
firstBuyDate: '2022-03-07', firstBuyDate: '2022-03-07',
grossPerformance: new Big('19.86'), grossPerformance: new Big('19.86'),
@ -154,7 +160,8 @@ describe('PortfolioCalculator', () => {
symbol: 'NOVN.SW', symbol: 'NOVN.SW',
timeWeightedInvestment: new Big('151.6'), timeWeightedInvestment: new Big('151.6'),
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'),
transactionCount: 2 transactionCount: 2,
valueInBaseCurrency: new Big('0')
} }
], ],
totalInvestment: new Big('0'), totalInvestment: new Big('0'),

View File

@ -1,7 +1,8 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import Big from 'big.js';
import { CurrentRateService } from './current-rate.service'; import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator';
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
@ -9,7 +10,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
@ -21,10 +22,10 @@ describe('PortfolioCalculator', () => {
describe('annualized performance percentage', () => { describe('annualized performance percentage', () => {
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
activities: [],
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
currency: 'USD', currency: 'USD'
orders: []
}); });
it('Get annualized performance', async () => { it('Get annualized performance', async () => {

View File

@ -1,3 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
@ -7,31 +15,27 @@ import {
InvestmentItem, InvestmentItem,
ResponseError, ResponseError,
SymbolMetrics, SymbolMetrics,
TimelinePosition TimelinePosition,
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { GroupBy } from '@ghostfolio/common/types'; import { GroupBy } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Type as TypeOfOrder } from '@prisma/client'; import { Big } from 'big.js';
import Big from 'big.js';
import { import {
addDays, addDays,
addMilliseconds, addMilliseconds,
differenceInDays, differenceInDays,
eachDayOfInterval,
endOfDay, endOfDay,
format, format,
isBefore, isBefore,
isSameDay, isSameDay,
max,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash'; import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface';
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculator { export class PortfolioCalculator {
private static readonly ENABLE_LOGGING = false; private static readonly ENABLE_LOGGING = false;
@ -43,108 +47,37 @@ export class PortfolioCalculator {
private transactionPoints: TransactionPoint[]; private transactionPoints: TransactionPoint[];
public constructor({ public constructor({
activities,
currency, currency,
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService
orders
}: { }: {
activities: Activity[];
currency: string; currency: string;
currentRateService: CurrentRateService; currentRateService: CurrentRateService;
exchangeRateDataService: ExchangeRateDataService; exchangeRateDataService: ExchangeRateDataService;
orders: PortfolioOrder[];
}) { }) {
this.currency = currency; this.currency = currency;
this.currentRateService = currentRateService; this.currentRateService = currentRateService;
this.exchangeRateDataService = exchangeRateDataService; this.exchangeRateDataService = exchangeRateDataService;
this.orders = orders; this.orders = activities.map(
({ date, fee, quantity, SymbolProfile, type, unitPrice }) => {
return {
SymbolProfile,
type,
date: format(date, DATE_FORMAT),
fee: new Big(fee),
quantity: new Big(quantity),
unitPrice: new Big(unitPrice)
};
}
);
this.orders.sort((a, b) => { this.orders.sort((a, b) => {
return a.date?.localeCompare(b.date); return a.date?.localeCompare(b.date);
}); });
}
public computeTransactionPoints() { this.computeTransactionPoints();
this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null;
for (const order of this.orders) {
const currentDate = order.date;
let currentTransactionPointItem: TransactionPointSymbol;
const oldAccumulatedSymbol = symbols[order.symbol];
const factor = this.getFactor(order.type);
const unitPrice = new Big(order.unitPrice);
if (oldAccumulatedSymbol) {
const newQuantity = order.quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
let investment = new Big(0);
if (newQuantity.gt(0)) {
if (order.type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice)
);
} else if (order.type === 'SELL') {
const averagePrice = oldAccumulatedSymbol.investment.div(
oldAccumulatedSymbol.quantity
);
investment = oldAccumulatedSymbol.investment.minus(
order.quantity.mul(averagePrice)
);
}
}
currentTransactionPointItem = {
investment,
currency: order.currency,
dataSource: order.dataSource,
fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
quantity: newQuantity,
symbol: order.symbol,
tags: order.tags,
transactionCount: oldAccumulatedSymbol.transactionCount + 1
};
} else {
currentTransactionPointItem = {
currency: order.currency,
dataSource: order.dataSource,
fee: order.fee,
firstBuyDate: order.date,
investment: unitPrice.mul(order.quantity).mul(factor),
quantity: order.quantity.mul(factor),
symbol: order.symbol,
tags: order.tags,
transactionCount: 1
};
}
symbols[order.symbol] = currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? [];
const newItems = items.filter(
(transactionPointItem) => transactionPointItem.symbol !== order.symbol
);
newItems.push(currentTransactionPointItem);
newItems.sort((a, b) => {
return a.symbol?.localeCompare(b.symbol);
});
if (lastDate !== currentDate || lastTransactionPoint === null) {
lastTransactionPoint = {
date: currentDate,
items: newItems
};
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.items = newItems;
}
lastDate = currentDate;
}
} }
public getAnnualizedPerformancePercent({ public getAnnualizedPerformancePercent({
@ -164,14 +97,6 @@ export class PortfolioCalculator {
return new Big(0); return new Big(0);
} }
public getTransactionPoints(): TransactionPoint[] {
return this.transactionPoints;
}
public setTransactionPoints(transactionPoints: TransactionPoint[]) {
this.transactionPoints = transactionPoints;
}
public async getChartData({ public async getChartData({
end = new Date(Date.now()), end = new Date(Date.now()),
start, start,
@ -189,29 +114,31 @@ export class PortfolioCalculator {
}) ?? []; }) ?? [];
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
const dates: Date[] = [];
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
const firstIndex = transactionPointsBeforeEndDate.length; const firstIndex = transactionPointsBeforeEndDate.length;
let day = start; let dates = eachDayOfInterval({ start, end }, { step }).map((date) => {
return resetHours(date);
});
while (isBefore(day, end)) { const includesEndDate = isSameDay(last(dates), end);
dates.push(resetHours(day));
day = addDays(day, step);
}
if (!isSameDay(last(dates), end)) { if (!includesEndDate) {
dates.push(resetHours(end)); dates.push(resetHours(end));
} }
if (transactionPointsBeforeEndDate.length > 0) { if (transactionPointsBeforeEndDate.length > 0) {
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { for (const {
currency,
dataSource,
symbol
} of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataGatheringItems.push({ dataGatheringItems.push({
dataSource: item.dataSource, dataSource,
symbol: item.symbol symbol
}); });
currencies[item.symbol] = item.currency; currencies[symbol] = currency;
symbols[item.symbol] = true; symbols[symbol] = true;
} }
} }
@ -233,7 +160,7 @@ export class PortfolioCalculator {
await this.exchangeRateDataService.getExchangeRatesByCurrency({ await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)), currencies: uniq(Object.values(currencies)),
endDate: endOfDay(end), endDate: endOfDay(end),
startDate: parseDate(this.transactionPoints?.[0]?.date), startDate: this.getStartDate(),
targetCurrency: this.currency targetCurrency: this.currency
}); });
@ -294,6 +221,7 @@ export class PortfolioCalculator {
start, start,
step, step,
symbol, symbol,
dataSource: null,
exchangeRates: exchangeRates:
exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`], exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`],
isChartMode: true isChartMode: true
@ -440,16 +368,27 @@ export class PortfolioCalculator {
public async getCurrentPositions( public async getCurrentPositions(
start: Date, start: Date,
end = new Date(Date.now()) end?: Date
): Promise<CurrentPositions> { ): Promise<CurrentPositions> {
const transactionPointsBeforeEndDate = const lastTransactionPoint = last(this.transactionPoints);
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
if (!transactionPointsBeforeEndDate.length) { let endDate = end;
if (!endDate) {
endDate = new Date(Date.now());
if (lastTransactionPoint) {
endDate = max([endDate, parseDate(lastTransactionPoint.date)]);
}
}
const transactionPoints = this.transactionPoints?.filter(({ date }) => {
return isBefore(parseDate(date), endDate);
});
if (!transactionPoints.length) {
return { return {
currentValue: new Big(0), currentValueInBaseCurrency: new Big(0),
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -464,41 +403,40 @@ export class PortfolioCalculator {
}; };
} }
const lastTransactionPoint =
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
let dates: Date[] = []; let dates: Date[] = [];
let firstIndex = transactionPointsBeforeEndDate.length; let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
dates.push(resetHours(start)); dates.push(resetHours(start));
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
for (const { currency, dataSource, symbol } of transactionPoints[
firstIndex - 1
].items) {
dataGatheringItems.push({ dataGatheringItems.push({
dataSource: item.dataSource, dataSource,
symbol: item.symbol symbol
}); });
currencies[item.symbol] = item.currency; currencies[symbol] = currency;
} }
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) { for (let i = 0; i < transactionPoints.length; i++) {
if ( if (
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) && !isBefore(parseDate(transactionPoints[i].date), start) &&
firstTransactionPoint === null firstTransactionPoint === null
) { ) {
firstTransactionPoint = transactionPointsBeforeEndDate[i]; firstTransactionPoint = transactionPoints[i];
firstIndex = i; firstIndex = i;
} }
if (firstTransactionPoint !== null) { if (firstTransactionPoint !== null) {
dates.push( dates.push(resetHours(parseDate(transactionPoints[i].date)));
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
);
} }
} }
dates.push(resetHours(end)); dates.push(resetHours(endDate));
// Add dates of last week for fallback // Add dates of last week for fallback
dates.push(subDays(resetHours(new Date()), 7)); dates.push(subDays(resetHours(new Date()), 7));
@ -525,8 +463,8 @@ export class PortfolioCalculator {
let exchangeRatesByCurrency = let exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({ await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)), currencies: uniq(Object.values(currencies)),
endDate: endOfDay(end), endDate: endOfDay(endDate),
startDate: parseDate(this.transactionPoints?.[0]?.date), startDate: this.getStartDate(),
targetCurrency: this.currency targetCurrency: this.currency
}); });
@ -561,7 +499,7 @@ export class PortfolioCalculator {
} }
} }
const endDateString = format(end, DATE_FORMAT); const endDateString = format(endDate, DATE_FORMAT);
if (firstIndex > 0) { if (firstIndex > 0) {
firstIndex--; firstIndex--;
@ -573,9 +511,9 @@ export class PortfolioCalculator {
const errors: ResponseError['errors'] = []; const errors: ResponseError['errors'] = [];
for (const item of lastTransactionPoint.items) { for (const item of lastTransactionPoint.items) {
const marketPriceInBaseCurrency = marketSymbolMap[endDateString]?.[ const marketPriceInBaseCurrency = (
item.symbol marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
]?.mul( ).mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
endDateString endDateString
] ]
@ -593,12 +531,15 @@ export class PortfolioCalculator {
netPerformanceWithCurrencyEffect, netPerformanceWithCurrencyEffect,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect,
totalDividend,
totalDividendInBaseCurrency,
totalInvestment, totalInvestment,
totalInvestmentWithCurrencyEffect totalInvestmentWithCurrencyEffect
} = this.getSymbolMetrics({ } = this.getSymbolMetrics({
end,
marketSymbolMap, marketSymbolMap,
start, start,
dataSource: item.dataSource,
end: endDate,
exchangeRates: exchangeRates:
exchangeRatesByCurrency[`${item.currency}${this.currency}`], exchangeRatesByCurrency[`${item.currency}${this.currency}`],
symbol: item.symbol symbol: item.symbol
@ -607,11 +548,11 @@ export class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
positions.push({ positions.push({
dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect,
averagePrice: item.quantity.eq(0) averagePrice: item.averagePrice,
? new Big(0)
: item.investment.div(item.quantity),
currency: item.currency, currency: item.currency,
dataSource: item.dataSource, dataSource: item.dataSource,
fee: item.fee, fee: item.fee,
@ -645,7 +586,10 @@ export class PortfolioCalculator {
quantity: item.quantity, quantity: item.quantity,
symbol: item.symbol, symbol: item.symbol,
tags: item.tags, tags: item.tags,
transactionCount: item.transactionCount transactionCount: item.transactionCount,
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
item.quantity
)
}); });
if ( if (
@ -714,7 +658,7 @@ export class PortfolioCalculator {
} }
private calculateOverallPerformance(positions: TimelinePosition[]) { private calculateOverallPerformance(positions: TimelinePosition[]) {
let currentValue = new Big(0); let currentValueInBaseCurrency = new Big(0);
let grossPerformance = new Big(0); let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0); let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false; let hasErrors = false;
@ -726,14 +670,9 @@ export class PortfolioCalculator {
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) { for (const currentPosition of positions) {
if ( if (currentPosition.valueInBaseCurrency) {
currentPosition.investment && currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
currentPosition.marketPriceInBaseCurrency currentPosition.valueInBaseCurrency
) {
currentValue = currentValue.plus(
new Big(currentPosition.marketPriceInBaseCurrency).mul(
currentPosition.quantity
)
); );
} else { } else {
hasErrors = true; hasErrors = true;
@ -790,7 +729,7 @@ export class PortfolioCalculator {
} }
return { return {
currentValue, currentValueInBaseCurrency,
grossPerformance, grossPerformance,
grossPerformanceWithCurrencyEffect, grossPerformanceWithCurrencyEffect,
hasErrors, hasErrors,
@ -819,25 +758,116 @@ export class PortfolioCalculator {
}; };
} }
private getFactor(type: TypeOfOrder) { public getStartDate() {
let factor: number; return this.transactionPoints.length > 0
? parseDate(this.transactionPoints[0].date)
switch (type) { : new Date();
case 'BUY':
factor = 1;
break;
case 'SELL':
factor = -1;
break;
default:
factor = 0;
break;
} }
return factor; public getTransactionPoints() {
return this.transactionPoints;
}
private computeTransactionPoints() {
this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null;
for (const {
fee,
date,
quantity,
SymbolProfile,
tags,
type,
unitPrice
} of this.orders) {
let currentTransactionPointItem: TransactionPointSymbol;
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
const factor = getFactor(type);
if (oldAccumulatedSymbol) {
let investment = oldAccumulatedSymbol.investment;
const newQuantity = quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
if (type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(unitPrice)
);
} else if (type === 'SELL') {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
}
currentTransactionPointItem = {
investment,
tags,
averagePrice: newQuantity.gt(0)
? investment.div(newQuantity)
: new Big(0),
currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource,
dividend: new Big(0),
fee: fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
quantity: newQuantity,
symbol: SymbolProfile.symbol,
transactionCount: oldAccumulatedSymbol.transactionCount + 1
};
} else {
currentTransactionPointItem = {
fee,
tags,
averagePrice: unitPrice,
currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource,
dividend: new Big(0),
firstBuyDate: date,
investment: unitPrice.mul(quantity).mul(factor),
quantity: quantity.mul(factor),
symbol: SymbolProfile.symbol,
transactionCount: 1
};
}
symbols[SymbolProfile.symbol] = currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? [];
const newItems = items.filter(({ symbol }) => {
return symbol !== SymbolProfile.symbol;
});
newItems.push(currentTransactionPointItem);
newItems.sort((a, b) => {
return a.symbol?.localeCompare(b.symbol);
});
if (lastDate !== date || lastTransactionPoint === null) {
lastTransactionPoint = {
date,
items: newItems
};
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.items = newItems;
}
lastDate = date;
}
} }
private getSymbolMetrics({ private getSymbolMetrics({
dataSource,
end, end,
exchangeRates, exchangeRates,
isChartMode = false, isChartMode = false,
@ -854,8 +884,7 @@ export class PortfolioCalculator {
}; };
start: Date; start: Date;
step?: number; step?: number;
symbol: string; } & UniqueAsset): SymbolMetrics {
}): SymbolMetrics {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {}; const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
@ -888,22 +917,21 @@ export class PortfolioCalculator {
[date: string]: Big; [date: string]: Big;
} = {}; } = {};
let totalDividend = new Big(0);
let totalDividendInBaseCurrency = new Big(0);
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
let totalInvestmentFromBuyTransactions = new Big(0);
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0);
let totalInvestmentWithGrossPerformanceFromSell = new Big(0); let totalQuantityFromBuyTransactions = new Big(0);
let totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect = new Big(
0
);
let totalUnits = new Big(0); let totalUnits = new Big(0);
let valueAtStartDate: Big; let valueAtStartDate: Big;
let valueAtStartDateWithCurrencyEffect: Big; let valueAtStartDateWithCurrencyEffect: Big;
// Clone orders to keep the original values in this.orders // Clone orders to keep the original values in this.orders
let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter( let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter(
(order) => { ({ SymbolProfile }) => {
return order.symbol === symbol; return SymbolProfile.symbol === symbol;
} }
); );
@ -931,6 +959,8 @@ export class PortfolioCalculator {
timeWeightedInvestmentValues: {}, timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {}, timeWeightedInvestmentValuesWithCurrencyEffect: {},
timeWeightedInvestmentWithCurrencyEffect: new Big(0), timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalDividend: new Big(0),
totalDividendInBaseCurrency: new Big(0),
totalInvestment: new Big(0), totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0) totalInvestmentWithCurrencyEffect: new Big(0)
}; };
@ -971,6 +1001,8 @@ export class PortfolioCalculator {
timeWeightedInvestmentValues: {}, timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {}, timeWeightedInvestmentValuesWithCurrencyEffect: {},
timeWeightedInvestmentWithCurrencyEffect: new Big(0), timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalDividend: new Big(0),
totalDividendInBaseCurrency: new Big(0),
totalInvestment: new Big(0), totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0) totalInvestmentWithCurrencyEffect: new Big(0)
}; };
@ -978,30 +1010,30 @@ export class PortfolioCalculator {
// Add a synthetic order at the start and the end date // Add a synthetic order at the start and the end date
orders.push({ orders.push({
symbol,
currency: null,
date: format(start, DATE_FORMAT), date: format(start, DATE_FORMAT),
dataSource: null,
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
itemType: 'start', itemType: 'start',
name: '',
quantity: new Big(0), quantity: new Big(0),
type: TypeOfOrder.BUY, SymbolProfile: {
dataSource,
symbol
},
type: 'BUY',
unitPrice: unitPriceAtStartDate unitPrice: unitPriceAtStartDate
}); });
orders.push({ orders.push({
symbol,
currency: null,
date: format(end, DATE_FORMAT), date: format(end, DATE_FORMAT),
dataSource: null,
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
itemType: 'end', itemType: 'end',
name: '', SymbolProfile: {
dataSource,
symbol
},
quantity: new Big(0), quantity: new Big(0),
type: TypeOfOrder.BUY, type: 'BUY',
unitPrice: unitPriceAtEndDate unitPrice: unitPriceAtEndDate
}); });
@ -1020,15 +1052,15 @@ export class PortfolioCalculator {
if (!hasDate) { if (!hasDate) {
orders.push({ orders.push({
symbol,
currency: null,
date: format(day, DATE_FORMAT), date: format(day, DATE_FORMAT),
dataSource: null,
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
name: '',
quantity: new Big(0), quantity: new Big(0),
type: TypeOfOrder.BUY, SymbolProfile: {
dataSource,
symbol
},
type: 'BUY',
unitPrice: unitPrice:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
lastUnitPrice lastUnitPrice
@ -1041,28 +1073,26 @@ export class PortfolioCalculator {
} }
} }
// Sort orders so that the start and end placeholder order are at the right // Sort orders so that the start and end placeholder order are at the correct
// position // position
orders = sortBy(orders, (order) => { orders = sortBy(orders, ({ date, itemType }) => {
let sortIndex = new Date(order.date); let sortIndex = new Date(date);
if (order.itemType === 'start') { if (itemType === 'end') {
sortIndex = addMilliseconds(sortIndex, -1);
}
if (order.itemType === 'end') {
sortIndex = addMilliseconds(sortIndex, 1); sortIndex = addMilliseconds(sortIndex, 1);
} else if (itemType === 'start') {
sortIndex = addMilliseconds(sortIndex, -1);
} }
return sortIndex.getTime(); return sortIndex.getTime();
}); });
const indexOfStartOrder = orders.findIndex((order) => { const indexOfStartOrder = orders.findIndex(({ itemType }) => {
return order.itemType === 'start'; return itemType === 'start';
}); });
const indexOfEndOrder = orders.findIndex((order) => { const indexOfEndOrder = orders.findIndex(({ itemType }) => {
return order.itemType === 'end'; return itemType === 'end';
}); });
let totalInvestmentDays = 0; let totalInvestmentDays = 0;
@ -1125,29 +1155,41 @@ export class PortfolioCalculator {
valueOfInvestmentBeforeTransactionWithCurrencyEffect; valueOfInvestmentBeforeTransactionWithCurrencyEffect;
} }
const transactionInvestment = let transactionInvestment = new Big(0);
order.type === 'BUY' let transactionInvestmentWithCurrencyEffect = new Big(0);
? order.quantity
.mul(order.unitPriceInBaseCurrency)
.mul(this.getFactor(order.type))
: totalUnits.gt(0)
? totalInvestment
.div(totalUnits)
.mul(order.quantity)
.mul(this.getFactor(order.type))
: new Big(0);
const transactionInvestmentWithCurrencyEffect = if (order.type === 'BUY') {
order.type === 'BUY' transactionInvestment = order.quantity
? order.quantity .mul(order.unitPriceInBaseCurrency)
.mul(getFactor(order.type));
transactionInvestmentWithCurrencyEffect = order.quantity
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(this.getFactor(order.type)) .mul(getFactor(order.type));
: totalUnits.gt(0)
? totalInvestmentWithCurrencyEffect totalQuantityFromBuyTransactions =
totalQuantityFromBuyTransactions.plus(order.quantity);
totalInvestmentFromBuyTransactions =
totalInvestmentFromBuyTransactions.plus(transactionInvestment);
totalInvestmentFromBuyTransactionsWithCurrencyEffect =
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
} else if (order.type === 'SELL') {
if (totalUnits.gt(0)) {
transactionInvestment = totalInvestment
.div(totalUnits) .div(totalUnits)
.mul(order.quantity) .mul(order.quantity)
.mul(this.getFactor(order.type)) .mul(getFactor(order.type));
: new Big(0); transactionInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type));
}
}
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('totalInvestment', totalInvestment.toNumber()); console.log('totalInvestment', totalInvestment.toNumber());
@ -1201,9 +1243,16 @@ export class PortfolioCalculator {
order.feeInBaseCurrencyWithCurrencyEffect ?? 0 order.feeInBaseCurrencyWithCurrencyEffect ?? 0
); );
totalUnits = totalUnits.plus( totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
order.quantity.mul(this.getFactor(order.type))
if (order.type === 'DIVIDEND') {
const dividend = order.quantity.mul(order.unitPrice);
totalDividend = totalDividend.plus(dividend);
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
dividend.mul(exchangeRateAtOrderDate ?? 1)
); );
}
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
@ -1212,14 +1261,14 @@ export class PortfolioCalculator {
); );
const grossPerformanceFromSell = const grossPerformanceFromSell =
order.type === TypeOfOrder.SELL order.type === 'SELL'
? order.unitPriceInBaseCurrency ? order.unitPriceInBaseCurrency
.minus(lastAveragePrice) .minus(lastAveragePrice)
.mul(order.quantity) .mul(order.quantity)
: new Big(0); : new Big(0);
const grossPerformanceFromSellWithCurrencyEffect = const grossPerformanceFromSellWithCurrencyEffect =
order.type === TypeOfOrder.SELL order.type === 'SELL'
? order.unitPriceInBaseCurrencyWithCurrencyEffect ? order.unitPriceInBaseCurrencyWithCurrencyEffect
.minus(lastAveragePriceWithCurrencyEffect) .minus(lastAveragePriceWithCurrencyEffect)
.mul(order.quantity) .mul(order.quantity)
@ -1234,35 +1283,21 @@ export class PortfolioCalculator {
grossPerformanceFromSellWithCurrencyEffect grossPerformanceFromSellWithCurrencyEffect
); );
totalInvestmentWithGrossPerformanceFromSell = lastAveragePrice = totalQuantityFromBuyTransactions.eq(0)
totalInvestmentWithGrossPerformanceFromSell
.plus(transactionInvestment)
.plus(grossPerformanceFromSell);
totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect =
totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect
.plus(transactionInvestmentWithCurrencyEffect)
.plus(grossPerformanceFromSellWithCurrencyEffect);
lastAveragePrice = totalUnits.eq(0)
? new Big(0) ? new Big(0)
: totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); : totalInvestmentFromBuyTransactions.div(
totalQuantityFromBuyTransactions
);
lastAveragePriceWithCurrencyEffect = totalUnits.eq(0) lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq(
0
)
? new Big(0) ? new Big(0)
: totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.div( : totalInvestmentFromBuyTransactionsWithCurrencyEffect.div(
totalUnits totalQuantityFromBuyTransactions
); );
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(
'totalInvestmentWithGrossPerformanceFromSell',
totalInvestmentWithGrossPerformanceFromSell.toNumber()
);
console.log(
'totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect',
totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.toNumber()
);
console.log( console.log(
'grossPerformanceFromSells', 'grossPerformanceFromSells',
grossPerformanceFromSells.toNumber() grossPerformanceFromSells.toNumber()
@ -1296,7 +1331,7 @@ export class PortfolioCalculator {
grossPerformanceWithCurrencyEffect; grossPerformanceWithCurrencyEffect;
} }
if (i > indexOfStartOrder) { if (i > indexOfStartOrder && ['BUY', 'SELL'].includes(order.type)) {
// Only consider periods with an investment for the calculation of // Only consider periods with an investment for the calculation of
// the time weighted investment // the time weighted investment
if (valueOfInvestmentBeforeTransaction.gt(0)) { if (valueOfInvestmentBeforeTransaction.gt(0)) {
@ -1308,11 +1343,10 @@ export class PortfolioCalculator {
orderDate, orderDate,
previousOrderDate previousOrderDate
); );
// Set to at least 1 day, otherwise the transactions on the same day
// would not be considered in the time weighted calculation
if (daysSinceLastOrder <= 0) { if (daysSinceLastOrder <= 0) {
daysSinceLastOrder = 1; // The time between two activities on the same day is unknown
// -> Set it to the smallest floating point number greater than 0
daysSinceLastOrder = Number.EPSILON;
} }
// Sum up the total investment days since the start date to calculate // Sum up the total investment days since the start date to calculate
@ -1490,6 +1524,7 @@ export class PortfolioCalculator {
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
2 2
)} )}
Total dividend: ${totalDividend.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed( Gross performance: ${totalGrossPerformance.toFixed(
2 2
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% )} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
@ -1527,6 +1562,8 @@ export class PortfolioCalculator {
netPerformanceValuesWithCurrencyEffect, netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues, timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect, timeWeightedInvestmentValuesWithCurrencyEffect,
totalDividend,
totalDividendInBaseCurrency,
totalInvestment, totalInvestment,
totalInvestmentWithCurrencyEffect, totalInvestmentWithCurrencyEffect,
grossPerformance: totalGrossPerformance, grossPerformance: totalGrossPerformance,

View File

@ -1,4 +1,5 @@
import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns'; import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns';
import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValueObject } from './interfaces/get-value-object.interface';
@ -42,6 +43,17 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 0 }; return { marketPrice: 0 };
case 'MSFT':
if (isSameDay(parseDate('2021-09-16'), date)) {
return { marketPrice: 89.12 };
} else if (isSameDay(parseDate('2021-11-16'), date)) {
return { marketPrice: 339.51 };
} else if (isSameDay(parseDate('2023-07-10'), date)) {
return { marketPrice: 331.83 };
}
return { marketPrice: 0 };
case 'NOVN.SW': case 'NOVN.SW':
if (isSameDay(parseDate('2022-04-11'), date)) { if (isSameDay(parseDate('2022-04-11'), date)) {
return { marketPrice: 87.8 }; return { marketPrice: 87.8 };

View File

@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
@ -106,7 +107,9 @@ describe('CurrentRateService', () => {
currentRateService = new CurrentRateService( currentRateService = new CurrentRateService(
dataProviderService, dataProviderService,
marketDataService marketDataService,
null,
null
); );
}); });

View File

@ -1,3 +1,4 @@
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
@ -6,7 +7,10 @@ import {
ResponseError, ResponseError,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isToday } from 'date-fns';
import { flatten, isEmpty, uniqBy } from 'lodash'; import { flatten, isEmpty, uniqBy } from 'lodash';
@ -18,16 +22,19 @@ import { GetValuesParams } from './interfaces/get-values-params.interface';
export class CurrentRateService { export class CurrentRateService {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService private readonly marketDataService: MarketDataService,
private readonly orderService: OrderService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
// TODO: Pass user instead of using this.request.user
public async getValues({ public async getValues({
dataGatheringItems, dataGatheringItems,
dateQuery dateQuery
}: GetValuesParams): Promise<GetValuesObject> { }: GetValuesParams): Promise<GetValuesObject> {
const dataProviderInfos: DataProviderInfo[] = []; const dataProviderInfos: DataProviderInfo[] = [];
const includeToday = const includesToday =
(!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) &&
(!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) &&
(!dateQuery.in || this.containsToday(dateQuery.in)); (!dateQuery.in || this.containsToday(dateQuery.in));
@ -36,10 +43,10 @@ export class CurrentRateService {
const quoteErrors: ResponseError['errors'] = []; const quoteErrors: ResponseError['errors'] = [];
const today = resetHours(new Date()); const today = resetHours(new Date());
if (includeToday) { if (includesToday) {
promises.push( promises.push(
this.dataProviderService this.dataProviderService
.getQuotes({ items: dataGatheringItems }) .getQuotes({ items: dataGatheringItems, user: this.request?.user })
.then((dataResultProvider) => { .then((dataResultProvider) => {
const result: GetValueObject[] = []; const result: GetValueObject[] = [];
@ -116,11 +123,17 @@ export class CurrentRateService {
}); });
if (!value) { if (!value) {
// Fallback to unit price of latest activity
const latestActivity = await this.orderService.getLatestOrder({
dataSource,
symbol
});
value = { value = {
dataSource, dataSource,
symbol, symbol,
date: today, date: today,
marketPrice: 0 marketPrice: latestActivity?.unitPrice ?? 0
}; };
response.values.push(value); response.values.push(value);

View File

@ -1,8 +1,9 @@
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
import Big from 'big.js';
import { Big } from 'big.js';
export interface CurrentPositions extends ResponseError { export interface CurrentPositions extends ResponseError {
positions: TimelinePosition[]; currentValueInBaseCurrency: Big;
grossPerformance: Big; grossPerformance: Big;
grossPerformanceWithCurrencyEffect: Big; grossPerformanceWithCurrencyEffect: Big;
grossPerformancePercentage: Big; grossPerformancePercentage: Big;
@ -13,6 +14,6 @@ export interface CurrentPositions extends ResponseError {
netPerformanceWithCurrencyEffect: Big; netPerformanceWithCurrencyEffect: Big;
netPerformancePercentage: Big; netPerformancePercentage: Big;
netPerformancePercentageWithCurrencyEffect: Big; netPerformancePercentageWithCurrencyEffect: Big;
currentValue: Big; positions: TimelinePosition[];
totalInvestment: Big; totalInvestment: Big;
} }

View File

@ -1,11 +1,11 @@
import Big from 'big.js'; import { Big } from 'big.js';
import { PortfolioOrder } from './portfolio-order.interface'; import { PortfolioOrder } from './portfolio-order.interface';
export interface PortfolioOrderItem extends PortfolioOrder { export interface PortfolioOrderItem extends PortfolioOrder {
feeInBaseCurrency?: Big; feeInBaseCurrency?: Big;
feeInBaseCurrencyWithCurrencyEffect?: Big; feeInBaseCurrencyWithCurrencyEffect?: Big;
itemType?: '' | 'start' | 'end'; itemType?: 'end' | 'start';
unitPriceInBaseCurrency?: Big; unitPriceInBaseCurrency?: Big;
unitPriceInBaseCurrencyWithCurrencyEffect?: Big; unitPriceInBaseCurrencyWithCurrencyEffect?: Big;
} }

View File

@ -1,15 +1,12 @@
import { DataSource, Tag, Type as TypeOfOrder } from '@prisma/client'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import Big from 'big.js';
export interface PortfolioOrder { export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
currency: string;
date: string; date: string;
dataSource: DataSource;
fee: Big; fee: Big;
name: string;
quantity: Big; quantity: Big;
symbol: string; SymbolProfile: Pick<
tags?: Tag[]; Activity['SymbolProfile'],
type: TypeOfOrder; 'currency' | 'dataSource' | 'name' | 'symbol'
>;
unitPrice: Big; unitPrice: Big;
} }

View File

@ -1,12 +1,14 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
DataProviderInfo, DataProviderInfo,
EnhancedSymbolProfile, EnhancedSymbolProfile,
HistoricalDataItem HistoricalDataItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { Tag } from '@prisma/client'; import { Account, Tag } from '@prisma/client';
export interface PortfolioPositionDetail { export interface PortfolioPositionDetail {
accounts: Account[];
averagePrice: number; averagePrice: number;
dataProviderInfo: DataProviderInfo; dataProviderInfo: DataProviderInfo;
dividendInBaseCurrency: number; dividendInBaseCurrency: number;
@ -25,7 +27,7 @@ export interface PortfolioPositionDetail {
netPerformancePercent: number; netPerformancePercent: number;
netPerformancePercentWithCurrencyEffect: number; netPerformancePercentWithCurrencyEffect: number;
netPerformanceWithCurrencyEffect: number; netPerformanceWithCurrencyEffect: number;
orders: OrderWithAccount[]; orders: Activity[];
quantity: number; quantity: number;
SymbolProfile: EnhancedSymbolProfile; SymbolProfile: EnhancedSymbolProfile;
tags: Tag[]; tags: Tag[];

View File

@ -1,9 +1,11 @@
import { DataSource, Tag } from '@prisma/client'; import { DataSource, Tag } from '@prisma/client';
import Big from 'big.js'; import { Big } from 'big.js';
export interface TransactionPointSymbol { export interface TransactionPointSymbol {
averagePrice: Big;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
dividend: Big;
fee: Big; fee: Big;
firstBuyDate: string; firstBuyDate: string;
investment: Big; investment: Big;

View File

@ -1,16 +1,19 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { import {
hasNotDefinedValuesInObject, hasNotDefinedValuesInObject,
nullifyValuesInObject nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper'; } from '@ghostfolio/api/helper/object.helper';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
HEADER_KEY_IMPERSONATION HEADER_KEY_IMPERSONATION
@ -18,6 +21,7 @@ import {
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioDividends, PortfolioDividends,
PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
@ -28,6 +32,7 @@ import type {
GroupBy, GroupBy,
RequestWithUser RequestWithUser
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
Get, Get,
@ -42,7 +47,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import Big from 'big.js'; import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
@ -56,6 +61,8 @@ export class PortfolioController {
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService private readonly userService: UserService
@ -70,8 +77,11 @@ export class PortfolioController {
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string,
@Query('withLiabilities') withLiabilitiesParam = 'false'
): Promise<PortfolioDetails & { hasError: boolean }> { ): Promise<PortfolioDetails & { hasError: boolean }> {
const withLiabilities = withLiabilitiesParam === 'true';
let hasDetails = true; let hasDetails = true;
let hasError = false; let hasError = false;
const hasReadRestrictedAccessPermission = const hasReadRestrictedAccessPermission =
@ -90,20 +100,14 @@ export class PortfolioController {
filterByTags filterByTags
}); });
const { const { accounts, hasErrors, holdings, platforms, summary } =
accounts, await this.portfolioService.getDetails({
filteredValueInBaseCurrency,
filteredValueInPercentage,
hasErrors,
holdings,
platforms,
summary,
totalValueInBaseCurrency
} = await this.portfolioService.getDetails({
dateRange, dateRange,
filters, filters,
impersonationId, impersonationId,
userId: this.request.user.id withLiabilities,
userId: this.request.user.id,
withSummary: true
}); });
if (hasErrors || hasNotDefinedValuesInObject(holdings)) { if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
@ -117,27 +121,23 @@ export class PortfolioController {
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) const totalInvestment = Object.values(holdings)
.map((portfolioPosition) => { .map(({ investment }) => {
return portfolioPosition.investment; return investment;
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
const totalValue = Object.values(holdings) const totalValue = Object.values(holdings)
.map((portfolioPosition) => { .filter(({ assetClass, assetSubClass }) => {
return this.exchangeRateDataService.toCurrency( return assetClass !== 'CASH' && assetSubClass !== 'CASH';
portfolioPosition.quantity * portfolioPosition.marketPrice, })
portfolioPosition.currency, .map(({ valueInBaseCurrency }) => {
this.request.user.Settings.settings.baseCurrency return valueInBaseCurrency;
);
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPosition.grossPerformance = null;
portfolioPosition.investment = portfolioPosition.investment =
portfolioPosition.investment / totalInvestment; portfolioPosition.investment / totalInvestment;
portfolioPosition.netPerformance = null;
portfolioPosition.quantity = null;
portfolioPosition.valueInPercentage = portfolioPosition.valueInPercentage =
portfolioPosition.valueInBaseCurrency / totalValue; portfolioPosition.valueInBaseCurrency / totalValue;
} }
@ -163,19 +163,21 @@ export class PortfolioController {
'currentGrossPerformanceWithCurrencyEffect', 'currentGrossPerformanceWithCurrencyEffect',
'currentNetPerformance', 'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect', 'currentNetPerformanceWithCurrencyEffect',
'currentNetWorth',
'currentValue', 'currentValue',
'dividend', 'dividendInBaseCurrency',
'emergencyFund', 'emergencyFund',
'excludedAccountsAndActivities', 'excludedAccountsAndActivities',
'fees', 'fees',
'filteredValueInBaseCurrency',
'fireWealth', 'fireWealth',
'interest', 'interest',
'items', 'items',
'liabilities', 'liabilities',
'netWorth',
'totalBuy', 'totalBuy',
'totalInvestment', 'totalInvestment',
'totalSell' 'totalSell',
'totalValueInBaseCurrency'
]); ]);
} }
@ -202,12 +204,9 @@ export class PortfolioController {
return { return {
accounts, accounts,
filteredValueInBaseCurrency,
filteredValueInPercentage,
hasError, hasError,
holdings, holdings,
platforms, platforms,
totalValueInBaseCurrency,
summary: portfolioSummary summary: portfolioSummary
}; };
} }
@ -234,11 +233,24 @@ export class PortfolioController {
filterByTags filterByTags
}); });
let dividends = await this.portfolioService.getDividends({ const impersonationUserId =
dateRange, await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const { endDate, startDate } = getInterval(dateRange);
const { activities } = await this.orderService.getOrders({
endDate,
filters, filters,
groupBy, startDate,
impersonationId userCurrency,
userId: impersonationUserId || this.request.user.id,
types: ['DIVIDEND']
});
let dividends = await this.portfolioService.getDividends({
activities,
groupBy
}); });
if ( if (
@ -268,6 +280,35 @@ export class PortfolioController {
return { dividends }; return { dividends };
} }
@Get('holdings')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getHoldings(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('holdingType') filterByHoldingType?: string,
@Query('query') filterBySearchQuery?: string,
@Query('tags') filterByTags?: string
): Promise<PortfolioHoldingsResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByHoldingType,
filterBySearchQuery,
filterByTags
});
const { holdings } = await this.portfolioService.getDetails({
filters,
impersonationId,
userId: this.request.user.id
});
return { holdings: Object.values(holdings) };
}
@Get('investments') @Get('investments')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getInvestments( public async getInvestments(
@ -345,8 +386,12 @@ export class PortfolioController {
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string, @Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false @Query('withExcludedAccounts') withExcludedAccountsParam = 'false',
@Query('withItems') withItemsParam = 'false'
): Promise<PortfolioPerformanceResponse> { ): Promise<PortfolioPerformanceResponse> {
const withExcludedAccounts = withExcludedAccountsParam === 'true';
const withItems = withItemsParam === 'true';
const hasReadRestrictedAccessPermission = const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({ this.userService.hasReadRestrictedAccessPermission({
impersonationId, impersonationId,
@ -364,6 +409,7 @@ export class PortfolioController {
filters, filters,
impersonationId, impersonationId,
withExcludedAccounts, withExcludedAccounts,
withItems,
userId: this.request.user.id userId: this.request.user.id
}); });
@ -376,6 +422,7 @@ export class PortfolioController {
({ ({
date, date,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformanceInPercentageWithCurrencyEffect,
netWorth, netWorth,
totalInvestment, totalInvestment,
value value
@ -383,6 +430,7 @@ export class PortfolioController {
return { return {
date, date,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformanceInPercentageWithCurrencyEffect,
netWorthInPercentage: netWorthInPercentage:
performanceInformation.performance.currentNetWorth === 0 performanceInformation.performance.currentNetWorth === 0
? 0 ? 0
@ -428,6 +476,10 @@ export class PortfolioController {
return nullifyValuesInObject(item, ['totalInvestment', 'value']); return nullifyValuesInObject(item, ['totalInvestment', 'value']);
} }
); );
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
['currentNetPerformance', 'currentNetPerformancePercent']
);
} }
return performanceInformation; return performanceInformation;
@ -482,7 +534,6 @@ export class PortfolioController {
} }
const { holdings } = await this.portfolioService.getDetails({ const { holdings } = await this.portfolioService.getDetails({
dateRange: 'max',
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }], filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
impersonationId: access.userId, impersonationId: access.userId,
userId: user.id userId: user.id
@ -514,7 +565,8 @@ export class PortfolioController {
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
markets: hasDetails ? portfolioPosition.markets : undefined, markets: hasDetails ? portfolioPosition.markets : undefined,
name: portfolioPosition.name, name: portfolioPosition.name,
netPerformancePercent: portfolioPosition.netPerformancePercent, netPerformancePercentWithCurrencyEffect:
portfolioPosition.netPerformancePercentWithCurrencyEffect,
sectors: hasDetails ? portfolioPosition.sectors : [], sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol, symbol: portfolioPosition.symbol,
url: portfolioPosition.url, url: portfolioPosition.url,

View File

@ -12,6 +12,7 @@ import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impe
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()

View File

@ -1,5 +1,6 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CacheModule } from '@nestjs/cache-manager'; import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store'; import * as redisStore from 'cache-manager-redis-store';
@ -14,6 +15,7 @@ import { RedisCacheService } from './redis-cache.service';
inject: [ConfigurationService], inject: [ConfigurationService],
useFactory: async (configurationService: ConfigurationService) => { useFactory: async (configurationService: ConfigurationService) => {
return <RedisClientOptions>{ return <RedisClientOptions>{
db: configurationService.get('REDIS_DB'),
host: configurationService.get('REDIS_HOST'), host: configurationService.get('REDIS_HOST'),
max: configurationService.get('MAX_ITEM_IN_CACHE'), max: configurationService.get('MAX_ITEM_IN_CACHE'),
password: configurationService.get('REDIS_PASSWORD'), password: configurationService.get('REDIS_PASSWORD'),

View File

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
@ -20,7 +21,7 @@ export class RedisCacheService {
} }
public async get(key: string): Promise<string> { public async get(key: string): Promise<string> {
return await this.cache.get(key); return this.cache.get(key);
} }
public getQuoteKey({ dataSource, symbol }: UniqueAsset) { public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
@ -28,15 +29,15 @@ export class RedisCacheService {
} }
public async remove(key: string) { public async remove(key: string) {
await this.cache.del(key); return this.cache.del(key);
} }
public async reset() { public async reset() {
await this.cache.reset(); return this.cache.reset();
} }
public async set(key: string, value: string, ttlInSeconds?: number) { public async set(key: string, value: string, ttlInSeconds?: number) {
await this.cache.set( return this.cache.set(
key, key,
value, value,
ttlInSeconds ?? this.configurationService.get('CACHE_TTL') ttlInSeconds ?? this.configurationService.get('CACHE_TTL')

View File

@ -1,14 +1,14 @@
import * as fs from 'fs';
import * as path from 'path';
import { import {
DATE_FORMAT, DATE_FORMAT,
getYesterday, getYesterday,
interpolate interpolate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common'; import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Response } from 'express'; import { Response } from 'express';
import * as fs from 'fs';
import * as path from 'path';
@Controller('sitemap.xml') @Controller('sitemap.xml')
export class SitemapController { export class SitemapController {

View File

@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SitemapController } from './sitemap.controller'; import { SitemapController } from './sitemap.controller';

View File

@ -7,6 +7,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { Coupon } from '@ghostfolio/common/interfaces'; import { Coupon } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -115,7 +116,7 @@ export class SubscriptionController {
@Body() { couponId, priceId }: { couponId: string; priceId: string } @Body() { couponId, priceId }: { couponId: string; priceId: string }
) { ) {
try { try {
return await this.subscriptionService.createCheckoutSession({ return this.subscriptionService.createCheckoutSession({
couponId, couponId,
priceId, priceId,
user: this.request.user user: this.request.user

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