Compare commits

...

405 Commits

Author SHA1 Message Date
2c7ece50fe Release 2.29.0 (#2733) 2023-12-09 17:14:13 +01:00
51a0ede3e4 Feature/introduce lazy loaded activities table (#2729)
* Introduce lazy-loaded activities table

* Add icon column

* Emit paginator event

* Add pagination logic

* Integrate total items

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

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

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

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

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

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

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

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

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

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

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

* Update changelog

---------

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

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

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

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

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

* Run migrations

* Extend instructions

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

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

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

* Add Compound Planning

* Add Whal

* Add De.Fi

* Add Tiller

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

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

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

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

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

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

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

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

* Update changelog

---------

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

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

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

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

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

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

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

* Exchange rates management
* Coupons management

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

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

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

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

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

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

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

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

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

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

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

* Add YNAB

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

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

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

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

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

* Update changelog

---------

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

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

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

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

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

* Upgrade Angular dependencies to version 16.2.12

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

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

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

* Add Rocket Money

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

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

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

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

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

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

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

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

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

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

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

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-28 15:09:01 +02:00
379c651ce0 Add translation for product slogan (#2554) 2023-10-28 15:07:15 +02:00
7804c6879d Clean up (#2543) 2023-10-28 11:24:50 +02:00
de2255f9ba Release 2.15.0 (#2548) 2023-10-26 19:38:44 +02:00
e4ec5f213e Feature/extend personal finance tools pages 20231026 (#2547)
* Improve wording (vs)

* Improve breadcrumb (vs)

* Add Beanvest

* Add Wealthica

* Update locales
2023-10-26 19:36:34 +02:00
f3c2fb853d Upgrade to Nx 17 (#2545)
* Upgrade to Nx 17

* Update changelog
2023-10-26 19:35:56 +02:00
f5ad1d2d24 Feature/set validation rule to positive number in cash balance transfer (#2544)
* Add validation rule (positive number)

* Update changelog
2023-10-26 19:19:43 +02:00
0af37ca1d7 Extend asset profile dialog form (#2535)
* Extend asset profile dialog form

* Update changelog
2023-10-25 20:28:51 +02:00
2992a0da4c Verify current benchmark before loading it (#2541)
* Verify current benchmark before loading it

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-25 20:27:41 +02:00
2dcc7e161c Improve validation of activities import (#2496)
* Improve validation of activities import: expects positive values for fee, quantity and unitPrice

* Update changelog

---------

Co-authored-by: Rafael Claudio <rafacla@github.com>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-24 20:51:48 +02:00
fa627f686f Bugfix/fix chart for account excluded from analysis (#2534)
* Fix chart for account excluded from analysis

* Update changelog
2023-10-24 20:13:57 +02:00
0567083fc1 Feature/upgrade UUID to version 9.0.1 (#2537)
* Upgrade uuid to version 9.0.1

* Update changelog
2023-10-24 20:12:00 +02:00
3212efef17 Feature/upgrade yahoo finance2 to version 2.8.1 (#2536)
* Upgrade yahoo-finance2 to version 2.8.1

* Update changelog
2023-10-24 19:40:53 +02:00
6077e7c2f9 Feature/improve position detail dialog (#2532)
* Improve style and wording

* Update locales

* Update changelog
2023-10-23 20:50:40 +02:00
96b5dcfaf8 Create reusable currency selector component using mat-autocomplete (#2487)
* Create reusable currency selector component using mat-autocomplete

* Update changelog
2023-10-23 20:30:05 +02:00
c4e8e37884 Release 2.14.0 (#2530) 2023-10-21 20:07:40 +02:00
281d33f825 Feature/update oss friends 20231021 (#2529)
* Update OSS friends

* Improve style of sub title
2023-10-21 20:05:20 +02:00
5822e4d186 Bugfix/fix style of active page in header navigation (#2528)
* Fix style of active page

* Update changelog
2023-10-21 19:34:44 +02:00
cb166dcc78 Redirect to membership page (#2527) 2023-10-21 18:43:33 +02:00
4e7b7375a9 Feature/setup open figi (#2526)
* Setup OpenFIGI

* Update changelog
2023-10-21 18:12:50 +02:00
b8626c2086 Feature/change fees interest and search to general availability (#2525)
* Change features to general availability

* Fees on account level
* Interest on account level
* Search for a holding

* Update changelog

* Add documentation for experimental features
2023-10-21 10:25:05 +02:00
a59f9fa037 Feature/remove version from client (#2522)
* Remove version

* Update changelog
2023-10-21 09:41:07 +02:00
1666486940 Bugfix/trim text in i18n service (#2520)
* Trim text

* Update changelog
2023-10-20 22:36:03 +02:00
ac0ad48a65 Display invalid activity in csv import (#2460)
* Display invalid activity in csv import

* Update changelog
2023-10-20 22:07:57 +02:00
6a19eab425 Feature/improve membership card (#2517)
* Improve style

* Add animated border
2023-10-20 22:05:27 +02:00
750c627613 Feature/allow to edit market data of today (#2515)
* Allow to edit today's market data

* Update changelog
2023-10-20 17:37:55 +02:00
60b2115e3b Release 2.13.0 (#2514) 2023-10-20 08:24:49 +02:00
e7956943ba Make holdings request only once (#2453)
* Make holdings request only once

* Update changelog
2023-10-20 08:21:23 +02:00
f66edf8de0 Add membership card component (#2507)
* Add membership card component

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-20 08:18:34 +02:00
29028a81f5 Add i18n service to query XML files (#2503)
* Add i18n service to query XML files

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-19 17:13:40 +02:00
c9878c9050 Migrated users table in Admin Control to mat-table (#2469)
* Migrated users table in Admin Control to mat-table

* Update changelog
2023-10-19 16:58:01 +02:00
73ac4b4197 Add chart to account detail dialog (#2502)
* Add chart to account detail dialog

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-19 15:51:31 +02:00
016634a77f Feature/setup i18n page (#2508)
* Setup i18n page

* Add meta description
2023-10-18 17:35:07 +02:00
ea65dc5034 Release 2.12.0 (#2505) 2023-10-17 20:52:02 +02:00
84db54babd Change checkboxes to slide toggles on user settings page (#2497)
* Change checkboxes to slide toggles on user settings page

* Update changelog
2023-10-17 20:49:54 +02:00
653c9c62a8 Sort imports (#2490) 2023-10-17 20:42:32 +02:00
74278073b3 Bugfix/fix query to get asset profiles matching data source and symbol (#2504)
* Match dataSource and symbol

* Update changelog
2023-10-17 20:36:01 +02:00
0375b938a2 Add confirmation dialog (#2501) 2023-10-17 20:14:46 +02:00
32df7620d9 Add support for creating asset profiles with MANUAL data source (#2479)
* Add support for creating asset profiles with MANUAL data source

* Refactoring

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-17 18:33:22 +02:00
8492a8fed0 Upgrade simplewebauthn (#2498)
* Upgrade simplewebauthn to new major version

* Update changelog
2023-10-17 09:17:44 +02:00
30e561c06f Feature/extend assistant with search for asset profile (#2499)
* Extend assistant with search for asset profile

* Extend search results by currency, symbol and asset sub class

* Update changelog
2023-10-16 21:54:36 +02:00
7243090c0e Feature/copy client locales to assets of server (#2493)
* Copy client locales to server’s assets

* Update changelog
2023-10-15 18:08:44 +02:00
7ae49eb839 Add endpoint for account balances (#2484)
* Add endpoint for account balances

* Update changelog

---------

Co-authored-by: Pavol Kolcun <pavol.kolcun@student.tuke.sk>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-15 17:09:47 +02:00
bf816c3b89 Bugfix/show transfer balance button based on permission (#2489)
* Show button depending on permission

* Update changelog
2023-10-15 10:02:55 +02:00
20f9225daa Release 2.11.0 (#2488) 2023-10-14 19:12:37 +02:00
b6101c6375 Feature/import historical data (#2448)
* Import historical data for an asset

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-14 19:06:27 +02:00
e1022846b9 Bugfix/fix displayed currency of cash balance in account dialog (#2480)
* Fix currency

* Update changelog
2023-10-14 15:01:29 +02:00
9ba79f6721 Improve style (#2478) 2023-10-13 22:11:05 +02:00
0ac97bd112 Transfer cash balance between accounts (#2455)
* Transfer cash balance between accounts

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-13 21:46:49 +02:00
827270704a Bugfix/fix import for activities of type fee and interest (#2474)
* Fix import for activities of type fee and interest

* Update changelog
2023-10-13 19:51:02 +02:00
8634463597 Feature/upgrade prisma to version 5.4.2 (#2477)
* Upgrade prisma to version 5.4.2

* Update changelog
2023-10-13 19:50:06 +02:00
3905782ad6 Fix fab-container (#2476) 2023-10-13 19:49:30 +02:00
5db984ffef Add date column to benchmark component (#2466)
* Add date column to benchmark component
2023-10-12 10:21:00 +02:00
fb3cd4b689 Remove icon (#2467) 2023-10-11 09:58:18 +02:00
3b5a34f6f3 Use fab-button in access management tab (#2456)
* Use fab-button in access management tab

* Refactor fab container

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-11 09:57:35 +02:00
22b43b5bfc Feature/extract locales 20231010 (#2462)
* Update locales

* Update changelog
2023-10-10 20:17:45 +02:00
6c66033eb4 Add date to markets overview by benchmarks (#2436)
* Add date

* Update changelog
2023-10-10 17:31:53 +02:00
162fc25e23 Release 2.10.0 (#2459) 2023-10-09 20:31:34 +02:00
45f385a483 Feature/improve symbol conversion in eod historical data service (#2457)
* Improve conversion of currency symbols

* Update changelog
2023-10-09 20:29:56 +02:00
e9ef911548 Feature/improve search results display in assistant (#2458)
* Only show search results if search is active

* Update changelog
2023-10-09 20:28:39 +02:00
d8d4d8f001 Change jobs table in admin control to mat-table (#2444)
* Change jobs table in admin control to mat-table

* Update changelog
2023-10-09 19:38:33 +02:00
f47c7313af Support enter key press to submit access dialog form (#2437)
* Support enter key press to submit access dialog form

* Update changelog
2023-10-09 19:11:09 +02:00
31f0056a2d Release 2.9.0 (#2454) 2023-10-08 20:34:39 +02:00
550e646079 Feature/introduce assistant (#2451)
* Introduce assistant

* Update changelog
2023-10-08 20:32:00 +02:00
37ff7acf04 Change platform control from select to autocomplete in account dialog (#2429)
* Change platform control from select to autocomplete in account dialog

* Update changelog
2023-10-08 19:17:55 +02:00
8236091477 Feature/add support for search query in portfolio position endpoint (#2443)
* Introduce search query filter

* Update changelog
2023-10-07 19:30:28 +02:00
2a71cb66de Use port numbers from environment variables in docker-compose.dev.yml (#2406) 2023-10-07 17:18:13 +02:00
e60fe48fdd Add dialog for cash transfer between accounts (#2433)
* Add dialog for cash transfer between accounts

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-07 14:46:13 +02:00
d40bc5070a Feature/remove permission to markets overview on home page (#2441)
* Remove show condition for markets overview

* Update changelog
2023-10-07 09:15:54 +02:00
fda4e0ea7d Use application version from API endpoint in Admin Control panel (#2427)
* Use application version from API endpoint

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-07 09:11:32 +02:00
08d696ce33 Align ok.json with ok.csv (#2439) 2023-10-07 08:44:51 +02:00
46614a7c24 Create carousel component for testimonials (#2394)
* Create carousel component for testimonials

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-06 22:12:09 +02:00
02b433eb1e Prevent empty form submission in account dialog (#2428)
* Prevent empty form submission in account dialog
2023-10-05 21:02:35 +02:00
25112a450b Add support for comment in csv import (#2416)
* Add support for comment in csv import (activities)

* Update changelog
2023-10-05 20:42:35 +02:00
727340748b Clean up imports (#2411) 2023-10-05 20:40:31 +02:00
8ad6492477 Feature/various improvements in client (#2434)
* Various improvements

* Update changelog
2023-10-05 20:31:00 +02:00
4af76f6f6d Fix hasNotDefinedValuesInObject() in object.helper.ts (#2421) 2023-10-05 20:29:34 +02:00
10940214a5 Update OSS Friends (#2431) 2023-10-05 20:27:39 +02:00
d9a6c22e1e Add application version to admin endpoint (#2423)
* Add application version to admin endpoint

* Update changelog
2023-10-04 16:15:08 +02:00
692309988c Add Prisma Studio (#2415) 2023-10-04 08:48:50 +02:00
42a54263f9 Release 2.8.0 (#2420) 2023-10-03 20:10:29 +02:00
4fb88859b2 Improve form in account dialog (#2408)
* Improve form in account dialog

* Update changelog
2023-10-03 19:34:04 +02:00
aa24b5e8c6 Feature/reload platform and tag on change (#2417)
* Reload platforms via info

* Reload tags via info

* Update changelog
2023-10-03 18:58:23 +02:00
90e18338f6 Change UX to set an asset profile as a benchmark (#2409)
* Add checkbox functionality to set / unset benchmark

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-02 20:50:12 +02:00
ad5ae938ef Feature/harmonize settings icon of user account page (#2412)
* Harmonize icon

* Update changelog
2023-10-02 11:29:33 +02:00
c9a8dd4958 Support copy-assets Nx target on Windows (#2410)
* Introduce shx plugin
2023-10-02 10:48:36 +02:00
f1ec5e704e Add version to Overview of Admin Control panel (#2414)
* Add version to Overview of Admin Control panel

* Update changelog
2023-10-02 10:05:01 +02:00
f40f0653c2 Add request params (ship, take) for pagination to GET order endpoint (#2382)
* Add request params (ship, take) for pagination to GET order endpoint

* Update changelog
2023-10-02 08:54:21 +02:00
5f7a230fd3 Bugfix/fix sidebar on user account page (#2413)
* Fix sidebar on user account page

* Update changelog
2023-10-01 19:11:48 +02:00
71feb531e8 Release 2.7.0 (#2404) 2023-09-30 08:26:04 +02:00
ec3552d7f6 Feature/add tabs to user account page (#2396)
* Create components for access, membership and settings

* Add tabs

* Update changelog
2023-09-30 08:24:26 +02:00
41875e70d6 Extend personal finance tools (#2403) 2023-09-30 08:24:00 +02:00
5fa0540936 Feature/add emergency fund setup to static portfolio analysis rules (#2400)
* Add new static portfolio analysis rule: Emergency fund setup

* Update changelog
2023-09-30 08:08:20 +02:00
5b69dee246 Feature/setup inter font family (#2402)
* Setup Inter font family

* Update changelog
2023-09-30 08:06:01 +02:00
19b0fe04a6 Feature/upgrade yahoo finance2 to version 2.8.0 (#2401)
* Upgrade yahoo-finance2 to version 2.8.0

* Update changelog
2023-09-30 07:31:02 +02:00
19ea4479ff Bugfix/fix link on features page (#2398)
* Fix link

* Update changelog
2023-09-30 07:11:44 +02:00
0b2f6a312c Sort imports (#2399) 2023-09-30 07:11:10 +02:00
f79d60014b Release 2.6.0 (#2395) 2023-09-26 18:58:22 +02:00
5b7409d08e Feature/add tag management in admin control panel (#2389)
* Add tag management

* Update locales

* Update changelog
2023-09-26 18:56:09 +02:00
6230aa87e2 Feature/add hacktoberfest 2023 blog post (#2359)
* Add blog post: Hacktoberfest 2023

* Update changelog
2023-09-26 16:08:17 +02:00
8b615d2f56 Feature/upgrade prettier to version 3.0.3 (#2393)
* Upgrade prettier to version 3.0.3

* Update changelog
2023-09-26 15:07:20 +02:00
4100446cac Update OSS Friends (#2385) 2023-09-26 15:05:14 +02:00
ad3e6d637c Improve file name (#2391) 2023-09-26 15:04:32 +02:00
aa87262954 Feature/upgrade yahoo finance2 to version 2.7.0 (#2392)
* Upgrade yahoo-finance2 to version 2.7.0

* Update changelog
2023-09-26 07:51:27 +02:00
01b6bb5b99 Clean up (#2390) 2023-09-25 23:37:52 +02:00
884b7f4de7 Clean up (#2342) 2023-09-24 08:25:25 +02:00
3f8a2b47f9 Release 2.5.0 (#2380) 2023-09-23 20:11:46 +02:00
e2e4c9be3c Feature/skip data gathering for manual data source (#2379)
* Skip data gathering

* Update changelog
2023-09-23 20:10:08 +02:00
0f7c6ff0fe Bugfix/fix asset class of cash position for empty account (#2378)
* Fix assetClass and assetSubClass

* Update changelog
2023-09-23 19:52:28 +02:00
703a96f4db Add guard (#2377) 2023-09-23 19:45:15 +02:00
42c0560422 Feature/translate activity type (#2376)
* Introduce ActivityTypeComponent with localized label

* Update changelog
2023-09-23 16:44:03 +02:00
eb63802d01 Feature/extend supported date formats in activities import (#2362)
* Extend supported date formats in activities import

* Update changelog
2023-09-23 16:14:54 +02:00
6d9191a46f Feature/setup turkish (#2300)
* Setup Turkish

* Add Turkish translations

* Update changelog

---------

Co-authored-by: sadmimye <134071831+sadmimye@users.noreply.github.com>
2023-09-22 20:26:45 +02:00
6744245d8b Feature/extend personal finance tools pages 20230922 (#2369)
* Extend pages

* Refactoring
2023-09-22 20:04:40 +02:00
8f64a77a9d Clean up (#2329) 2023-09-21 19:56:31 +02:00
0d5fc7655b Improve wording (#2358) 2023-09-21 19:55:36 +02:00
c511ec7e33 Release 2.4.0 (#2356) 2023-09-19 20:38:50 +02:00
b12349a148 Feature/add support for interest on account level (#2354)
* Add support for interest

* Update changelog
2023-09-19 20:37:04 +02:00
f7e3a4c727 Update OSS Friends (#2352) 2023-09-19 20:27:14 +02:00
5f276469b7 Feature/upgrade prisma to version 5.3.1 (#2355)
* Upgrade prisma to version 5.3.1

* Update changelog
2023-09-19 19:37:33 +02:00
69e1d92ed3 Feature/unlock experimental features setting for all users (#2351)
* Unlock experimental features setting for all users

* Update changelog
2023-09-19 18:41:12 +02:00
ef2849aa6c Remove this (#2341) 2023-09-19 10:28:07 +02:00
c668d7b456 Feature/improve preselected currency in create or update activity dialog (#2349)
* Preselect currency based on account's currency

* Update changelog
2023-09-18 19:45:02 +02:00
e23bf62859 Fix Memory Leak on Data Gathering when server TZ is behind UTC (#2332)
* Fix for timezones behind UTC (the previous code converted the date to one day before (in local time) then added a day, which resulted in the same day after converting back to UTC and thus generating an infinite loop)

* Update changelog

---------

Co-authored-by: Rafael Claudio <rafacla@github.com>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-09-17 22:19:06 +02:00
54c5746d21 Release 2.3.0 (#2348) 2023-09-17 18:23:28 +02:00
7130ac7565 Feature/add support for fees on account level (#1954)
* Add migration

* Add business logic for fees

* Fix export for liabilities

* Update changelog
2023-09-17 18:20:54 +02:00
1851ae137f Release 2.2.0 (#2346) 2023-09-17 07:17:20 +02:00
6f6ff94979 Improve sidebar (#2343)
* Improve sidebar

* Improve style of system message

* Update changelog
2023-09-17 07:08:26 +02:00
7f25066f0f Remove ALPHA_VANTAGE_API_KEY (#2345) 2023-09-17 06:54:38 +02:00
fc795aaa8c Update postgres to version 15 in docker-compose files (#1596)
* Update postgres to version 15 in docker-compose files

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-09-17 06:52:41 +02:00
d0112968e8 Feature/introduce sidebar navigation on desktop (#2340)
* Introduce sidebar navigation on desktop

* Update changelog
2023-09-16 14:40:05 +02:00
522025ffa0 Fix entry (#2339) 2023-09-16 14:37:08 +02:00
27bf662281 Release 2.1.0 (#2338) 2023-09-15 19:48:09 +02:00
93c27277c6 Extend sitemap with Italian pages (#2337) 2023-09-15 19:46:32 +02:00
5e6adfcef5 Feature/improve language localization for german 20230915 (#2336)
* Improve language localization

* Update changelog
2023-09-15 19:38:15 +02:00
ab691bb27a Feature/remove account type from user interface (#2335)
* Remove account type from user interface and set it optional

* Update changelog
2023-09-15 19:11:20 +02:00
8fc5676443 Feature/improve timeout of data source requests (#2330)
* Improve timeout

* Update changelog
2023-09-15 16:25:01 +02:00
1fe1e2fe0c Feature/improve read only mode (#2322)
* Improve read-only mode

* Update changelog
2023-09-15 16:22:39 +02:00
921d38a706 Feature/harmonize style of granted access user interface (#2326)
* Harmonize style

* Update changelog
2023-09-15 16:21:14 +02:00
6161d5e77c Feature/improve logger output of info service (#2331)
* Improve context of logger output

* Update changelog
2023-09-15 08:26:08 +02:00
369386f976 Add drop file functionality on import (#2323)
* Add drop file functionality on import

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-09-14 21:12:48 +02:00
41437636b1 Update messages.it.xlf (#2325)
* Update messages.it.xlf

* Update changelog
2023-09-14 19:48:47 +02:00
b21884eb66 Feature/harmonize logger output (#2321)
* Harmonize logger output

* Update changelog
2023-09-13 08:39:37 +02:00
1c5437e1fd Extend sitemap.xml with dutch pages (#2318) 2023-09-13 08:39:09 +02:00
58278ba5e6 Bugfix/fix dutch localization of portfolio summary (#2315)
* Revert reserved keyword (plural)

* Update changelog
2023-09-11 12:04:09 +02:00
921f3e9807 Update dutch translation (#2314)
* Update dutch translation

* Update changelog
2023-09-11 11:42:36 +02:00
75ca125a70 Add home server systems (#2311) 2023-09-10 08:01:01 +02:00
a1fd4e7a38 Improve wording (#2312) 2023-09-10 08:00:07 +02:00
0d5a8eb33e Add Ghostfolio 2.0 (#2309) 2023-09-09 09:10:36 +02:00
b088df2fa3 Release 2.0.0 (#2310) 2023-09-09 08:29:18 +02:00
f45d8f616a Bugfix/fix blog post ghostfolio 2 (#2307)
* Fix month

* Update sitemap.xml

* Update locales
2023-09-08 21:36:01 +02:00
d8300502ce Feature/upgrade yahoo finance2 to version 2.5.0 (#2306)
* Upgrade yahoo-finance2 to version 2.5.0

* Update changelog
2023-09-08 21:22:57 +02:00
502d51ad29 Feature/add blog post ghostfolio 2 (#2269)
* Add blog post: Ghostfolio 2.0

* Update changelog
2023-09-08 20:45:31 +02:00
bc33e5f147 Feature/remove deprecated environment variable base currency (#2255)
* Remove the deprecated environment variable BASE_CURRENCY

* Update changelog
2023-09-08 20:43:23 +02:00
48ba8f936b Feature/deactivate internet identity for account registration (#2293)
* Deactivate Internet Identity

* Update changelog
2023-09-08 20:23:22 +02:00
05ec4cce05 Add Portuguese landing page (#2301) 2023-09-08 17:06:58 +02:00
d74f283707 Eliminate prisma service (#2286)
* Eliminate prisma service
2023-09-08 17:05:42 +02:00
0f8bc7db32 Bugfix/do not remove countries and sectors in yahoo finance data enhancer (#2297)
* Do not remove countries and sectors

* Update changelog
2023-09-08 15:07:17 +02:00
431500f28a Update docker compose files to version 3.9 (#2299)
* Update docker compose files to version 3.9

* Format yml files

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-09-07 20:44:52 +02:00
9672de174e Feature/improve language localization for german 20230903 (#2294)
* Update locales

* Update changelog
2023-09-07 19:20:30 +02:00
c6aa06b933 Feature/improve import validation (#2305)
* Improve import validation

* Update changelog

Co-authored-by: httpiga <36515569+httpiga@users.noreply.github.com>
2023-09-07 18:28:47 +02:00
1f46a6b6f3 Clean up (#2291) 2023-09-05 19:44:45 +02:00
1bed940bc0 Feature/refresh cryptocurrencies list 20230903 (#2290)
* Refresh cryptocurrencies list

* Add CyberConnect

* Update changelog
2023-09-04 09:07:06 +02:00
f9eb3cc3c5 Release 1.305.0 (#2289) 2023-09-03 08:16:02 +02:00
2519c3ffb0 Bugfix/fix alignment in menu of impersonation mode (#2284)
* Fix alignment

* Update changelog
2023-09-02 17:57:50 +02:00
91013d1d10 Bugfix/fix alignment in header navigation (#2285)
* Fix alignment

* Update changelog
2023-09-02 17:13:13 +02:00
6deefb9c43 Update OSS Friends (#2282) 2023-09-02 11:52:29 +02:00
d0744e07df Feature/upgrade replace in file to version 7.0.1 (#2277)
* Upgrade replace-in-file to version 7.0.1

* Update changelog
2023-09-02 08:58:25 +02:00
93e1ee3ba7 Feature/improve localization of personal finance tools (#2274)
* Improve localization

* Update changelog
2023-09-02 08:58:10 +02:00
dceaa55a6c Feature/add hacker news logo to landing page (#2281)
* Add Hacker News

* Update changelog
2023-09-02 08:39:32 +02:00
8b4d55925d Feature/upgrade yahoo finance2 to version 2.4.4 (#2276)
* Upgrade yahoo-finance2 to version 2.4.4

* Update changelog
2023-09-02 08:36:46 +02:00
754b49e50f Feature/shorten page titles (#2273)
* Shorten page titles

* Update changelog
2023-08-31 18:22:22 +02:00
6ccbda8169 Feature/upgrade prisma to version 5.2.0 (#2217)
* Upgrade prisma to version 5.2.0

* Update changelog
2023-08-29 13:44:53 +02:00
b0fb986208 Release 1.304.0 (#2272) 2023-08-27 11:19:14 +02:00
0b59fc639d Feature/upgrade prettier to version 3 (#2163)
* Upgrade prettier to version 3.0.2

* Prettify code

* Update changelog
2023-08-27 11:13:11 +02:00
7ddd6f27b5 Feature/upgrade nx to version 16.7.4 (#2271)
* Upgrade Nx to version 16.7.4

* Update changelog
2023-08-27 10:44:06 +02:00
c5d56f4b47 Fix border (#2268) 2023-08-27 10:20:36 +02:00
2f2b712999 Fix breadcrumb (#2267) 2023-08-27 10:20:21 +02:00
c2fd31f5e5 Feature/add health check endpoints for data enhancers (#2265)
* Add health check for data enhancers

* Update changelog
2023-08-27 10:19:53 +02:00
f2d70f9070 Sort imports (#2266) 2023-08-26 11:22:19 +02:00
f41dd9cd8e Fix lint script (#2264) 2023-08-25 15:13:04 +02:00
7d238b4935 Release 1.303.0 (#2261) 2023-08-23 18:52:59 +02:00
da6591fca0 Bugfix/fix base url in trackinsight data enhancer (#2258)
* Fix base url

* Update changelog
2023-08-23 18:51:02 +02:00
1f9b9e9998 Feature/blog post ghostfolio joins oss friends (#2260)
* Add blog post: Ghostfolio joins OSS Friends

* Update changelog
2023-08-23 18:49:53 +02:00
49c4ea306d Feature/improve oss friends page (#2257)
* Improve OSS Friends page

* Update changelog
2023-08-22 09:02:39 +02:00
ccb5c664ef Feature/refresh cryptocurrencies list 20230821 (#2256)
* Update cryptocurrencies.json

* Update changelog
2023-08-21 20:20:59 +02:00
97e165ff69 Improve localization (#2254) 2023-08-21 18:05:08 +02:00
45aefb6a45 Reorder charts (#2253) 2023-08-21 18:04:49 +02:00
2435535975 Release 1.302.0 (#2252) 2023-08-20 10:35:13 +02:00
bd3d43bf05 Feature/upgrade nx to version 16.7.2 (#2251)
* Upgrade Nx and Angular dependencies

* Update changelog
2023-08-20 10:32:52 +02:00
02dc7c52b1 Localize routes (#2250)
* Localize about path

* Localize faq path

* Localize features path

* Localize markets path

* Localize pricing path

* Localize register path

* Localize resources path

* Extend sitemap
2023-08-20 10:01:40 +02:00
ff59fd4196 Feature/improve language localization for german 20230819 (#2249)
* Improve language localization

* Update changelog
2023-08-19 19:49:40 +02:00
4955555ddd Release 1.301.1 (#2247) 2023-08-19 08:49:31 +02:00
a98c788a26 Release 1.301.0 (#2246) 2023-08-18 20:46:58 +02:00
9c16af81c7 Feature/setup oss friends page (#2245)
* Setup OSS Friends page

* Update changelog
2023-08-18 20:45:10 +02:00
2df27100f0 Add middleware (#2239)
* Add middleware

* Update changelog
2023-08-18 20:27:19 +02:00
6cf6538719 Feature/add currencies preset to historical market data table (#2243)
* Add currencies preset

* Update locales

* Update changelog
2023-08-18 19:33:00 +02:00
0fd3db3228 Bugfix/fix cash position rows in holdings table (#2237)
* Fix cash position rows

* Update changelog
2023-08-17 20:23:23 +02:00
18835149e2 Add repository (#2189) 2023-08-16 20:32:54 +02:00
6c9779fb0d Bugfix/change date creation from string using parse iso (#2236)
* Change date creation using parseISO

parseISO provides consistent date parsing across different time zones

* Update changelog
2023-08-15 19:24:31 +02:00
3e98f097ef Refactor account page to user account page (#2235)
* Refactor account page to user account page
2023-08-13 09:24:54 +02:00
183ac8fa2b Feature/add data export to user account page (#2234)
* Add data export

* Update changelog
2023-08-12 21:51:35 +02:00
9036f53e7d Reset benchmark in user settings (#2233) 2023-08-12 21:50:01 +02:00
f7c04e469a Release 1.300.0 (#2232) 2023-08-11 20:24:23 +02:00
b5f01c0d15 Feature/migrate requests from bent to got (#2231)
* Migrate requests from bent to got

* Update changelog
2023-08-11 20:20:35 +02:00
5a23cd34ad Replace variables (#2229) 2023-08-11 18:29:39 +02:00
6e87f34c6f Feature/add more durations in coupon system (#2228)
* Add 90 and 180 days

* Update changelog
2023-08-10 20:49:06 +02:00
6618aa2e9b Release 1.299.1 (#2227) 2023-08-10 07:58:34 +02:00
0d25a96f7e Release 1.299.0 (#2225) 2023-08-09 20:59:52 +02:00
4f6d9d3a76 Feature/add timeout to eod historical data requests (#2222)
* Add timeout to requests using got

* Update changelog
2023-08-09 20:58:00 +02:00
928f6f0c45 Bugfix/fix historical data gathering interval for benchmarks with activity (#2221)
* Fix historical data gathering interval for asset profiles used as benchmarks having activities

* Update changelog
2023-08-09 20:43:03 +02:00
09e95ddcee Bugfix/fix editing of emergency fund (#2220)
* Fix editing of emergency fund

* Update changelog
2023-08-09 19:41:42 +02:00
2d003225bc Allow custom currency in activity import (#2215)
* Allow custom currency in activity import

* Extend import test files

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-08-08 20:00:55 +02:00
de93cabd69 Release 1.298.0 (#2214) 2023-08-06 09:13:04 +02:00
51489cca81 Feature/upgrade ng extract i18n merge to version 2.7.0 (#2155)
* Upgrade ng-extract-i18n-merge to version 2.7.0

* Update changelog
2023-08-06 09:10:14 +02:00
f7f4c3afb1 Feature/localize open startup page (#2213)
* Localize Open Startup page

* Update changelog
2023-08-06 09:09:21 +02:00
0821086e41 Bugfix/fix various styles after angular material 16 upgrade (#2212)
* Fix styles

* Update changelog
2023-08-06 08:52:45 +02:00
7a905fde63 Clean up (#2210) 2023-08-06 08:32:06 +02:00
d2882b1119 Clean up (#2206) 2023-08-06 08:31:48 +02:00
3a500598c5 Feature/upgrade nx to version 16.6.0 (#2211)
* Upgrade Nx to version 16.6.0

* Update changelog
2023-08-06 08:30:28 +02:00
42274917e0 Release 1.297.4 (#2209) 2023-08-05 19:57:39 +02:00
8ba50f2729 Release 1.297.3 (#2208) 2023-08-05 16:08:06 +02:00
f22071f061 Release 1.297.2 (#2207) 2023-08-05 14:51:10 +02:00
d2312371a6 Release 1.297.1 (#2205) 2023-08-05 14:35:13 +02:00
ba837c3c30 Release 1.297.0 (#2204) 2023-08-05 13:10:18 +02:00
d85d83a0f5 Feature/improve alignment of region percentages (#2203)
* Improve alignment

* Update changelog
2023-08-05 11:11:06 +02:00
62e8594c57 Feature/improve language localization for german 20230802 (#2200)
* Improve localization

* Update changelog
2023-08-05 11:10:15 +02:00
509f95ea30 Feature/add footer to public page (#2202)
* Add footer to public page

* Update changelog
2023-08-05 11:09:27 +02:00
43d0b55004 Feature/upgrade to angular 16 (#2156)
* Upgrade Angular, NestJS and Nx

* Replace executor to @nx/angular:webpack-browser and @nx/angular:webpack-dev-server

* Add target for copying assets

* Improve redirection of home page

* Update changelog
2023-08-05 11:08:10 +02:00
c0f130a077 Remove sitemap.xml (#2201) 2023-08-04 08:04:21 +02:00
90dc34380e Release 1.296.0 (#2199) 2023-08-01 09:12:26 +02:00
286e41eb21 Feature/optimize import validation by reducing to unique asset profiles (#2198)
* Optimize activities validation

* Optimize data gathering in import

* Update changelog
2023-08-01 09:10:13 +02:00
4973d0261d Release 1.295.0 (#2197) 2023-07-30 19:41:22 +02:00
c4a62dfd68 Bugfix/remove stay signed in setting from local storage on sign in with fingerprint activation (#2196)
* Remove staySignedIn from local storage

* Update changelog
2023-07-30 19:36:06 +02:00
4d6be0a507 Exclude open-source-alternative-to-markets.sh (#2195) 2023-07-30 19:35:49 +02:00
b259ab7b0c Feature/add step by step introduction for new users (#2191)
* Add introduction for new users

* Update changelog
2023-07-30 18:49:38 +02:00
e1ac5245c7 Release 1.294.0 (#2192) 2023-07-29 20:33:31 +02:00
d4fea075af Feature/include unavailable data in allocations by market chart (#2190)
* Include unavailable data in allocations by market chart

* Update changelog
2023-07-28 20:20:08 +02:00
cef7fa79de Fix total account value calculation for liabilities (#2184)
* Fix calculation

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-07-28 19:42:57 +02:00
ca05397dcd Extend Community Projects section (#2188) 2023-07-27 17:39:29 +02:00
2a11977001 Release 1.293.0 (#2186) 2023-07-26 21:32:35 +02:00
fb1a5c93ef Bugfix/fix no such file or directory error caused by missing favicon.ico (#2185)
* Add instructions to copy favicon.ico

* Update changelog
2023-07-26 21:26:05 +02:00
77e9791e03 Feature/set lastmod dates of sitemap.xml dynamically (#2170)
* Setup template with interpolation for sitemap.xml

* Update changelog
2023-07-26 21:08:38 +02:00
efd9e7a5c7 Fix RedisClient import (#2183) 2023-07-26 20:36:34 +02:00
d9ced885e1 Feature/add error handling for redis connections (#2179)
* Add error handling

* Update changelog
2023-07-26 20:30:32 +02:00
5fe07cb85f Bugfix/fix value in holdings table (#2182)
* Fix missing value

* Update changelog
2023-07-26 20:05:26 +02:00
af008aa74f Release 1.292.0 (#2175) 2023-07-24 20:17:46 +02:00
ca7bf27c20 Feature/upgrade yahoo finance2 to version 2.4.3 (#2174)
* Upgrade yahoo-finance2 to version 2.4.3

* Update changelog
2023-07-24 20:16:14 +02:00
0866587cab Increase frequency (#2169) 2023-07-24 20:12:07 +02:00
622bb8b0cf Feature/add allocations by market chart (#2171)
* Add allocations by (advanced) market

* Fix public page

* Update changelog
2023-07-24 20:04:34 +02:00
16b9fbe00e Release 1.291.0 (#2168) 2023-07-23 16:06:45 +02:00
c9353d0a39 Support account balance time series (#2166)
* Initial setup

* Support account balance in export

* Handle account balance update

* Add schema migration

* Update changelog
2023-07-23 15:55:58 +02:00
ea101dd3bd Refactor value to valueInBaseCurrency (#2167)
* Revert value to valueInBaseCurrency refactoring
2023-07-23 14:13:02 +02:00
cd67ce82fa Feature/rename queries to presets in market data table of admin control (#2165)
* Rename queries to presets

* Update changelog
2023-07-21 11:40:49 +02:00
d5b3c52602 Refactor value to valueInBaseCurrency (#2164) 2023-07-20 20:28:56 +02:00
bdf72164b1 Feature/break down emergency fund by cash and assets (#2159)
* Break down emergency fund in cash and assets

* Update changelog
2023-07-19 11:30:48 +02:00
455a2d2e92 Refactor value to valueInBaseCurrency (#2160) 2023-07-18 21:29:08 +02:00
9c0f46b587 Add markets.sh (#2161) 2023-07-18 21:28:44 +02:00
8533606177 Release 1.290.0 (#2158) 2023-07-16 08:01:31 +02:00
6728e04ff7 Improve http response interceptor (#2157)
Do not show snack bar for login endpoint
2023-07-15 22:17:07 +02:00
2bf4f1237a Feature/Improve login dialog (#2124)
* Improve login dialog

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-07-15 22:09:12 +02:00
4857b2e620 Update locales (#2154) 2023-07-15 19:50:11 +02:00
68a9a7f6f9 Feature/add queries to market data table in admin control (#2153)
* Add queries

* ETF_WITHOUT_COUNTRIES
* ETF_WITHOUT_SECTORS

* Update changelog
2023-07-15 17:54:16 +02:00
81ef95e13e Setup permissions (#2151) 2023-07-15 12:32:59 +02:00
b633132757 Feature/upgrade prisma to version 4.16.2 (#2109)
* Upgrade prisma to version 4.16.2

* Update changelog
2023-07-15 12:32:43 +02:00
2b0f961370 Feature/improve faq page (#2152)
* Extend content

* Update changelog
2023-07-15 12:16:19 +02:00
30f1a3514a Feature/add hints to activity types in create or edit activity dialog (#2150)
* Add hints

* Update changelog
2023-07-15 11:31:05 +02:00
ed735e0b29 Feature/disable caching in health check endpoints for data providers (#2147)
* Disable caching in health check endpoint

* Update changelog
2023-07-15 10:54:19 +02:00
b89ccd2dde Release 1.289.0 (#2149) 2023-07-14 10:26:27 +02:00
df6d39377f Upgrade @types/lodash to version 4.14.195 (#2125) 2023-07-14 07:53:39 +02:00
d5d14497d6 Release 1.288.0 (#2146) 2023-07-12 19:56:16 +02:00
09c300661a Feature/improve language localization for german 20230711 (#2144)
* Improve i18n

* Update changelog
2023-07-12 19:53:55 +02:00
92382e0b4d Feature/improve loading state during filtering on allocations page (#2141)
* Improve loading state

* Update changelog
2023-07-11 21:41:12 +02:00
c25f532487 Improve product pages (#2143) 2023-07-11 21:40:45 +02:00
5d26d94586 Sort imports (#2142) 2023-07-11 20:27:54 +02:00
73b6784e9f Feature/beautify ampersand in asset profile names (#2138)
* Beautify ampersand

* Update changelog
2023-07-10 20:16:38 +02:00
6159f48a62 Feature/setup personal finance tools pages 2 (#2140) 2023-07-10 20:16:20 +02:00
7d34fba7c1 Release 1.287.0 (#2136) 2023-07-09 10:44:41 +02:00
c434b730a8 Feature/hide average buy price in position detail chart if no holding (#2133)
* Hide the average buy price if no holding

* Update changelog
2023-07-09 10:42:53 +02:00
2d23c566f1 Feature/setup personal finance tools pages (#2135) 2023-07-09 10:42:10 +02:00
ba220eaee9 Bugfix/fix sorting by currency in activities table (#2122)
* Fix sorting by currency

* Update changelog
2023-07-09 09:38:48 +02:00
09023214ce Feature/French translation update (#2130)
* French translation update

* Update changelog
2023-07-07 21:26:51 +02:00
1ceabb6e6b Feature/refactor blog articles to standalone components (#2117)
* Refactor blog articles to standalone components

* Update changelog
2023-07-04 18:42:40 +02:00
421072c7fa Release 1.286.0 (#2120) 2023-07-03 22:30:33 +02:00
0d421e7181 Bugfix/fix adding 'Item' and 'Liability' activities (#2119)
* Fix adding activities of type item and liability

* Update changelog
2023-07-03 22:29:00 +02:00
f5180ce88f Improve wording (#2118) 2023-07-03 10:10:08 +02:00
aabf27dc96 Remove empty style files (#2116) 2023-07-02 08:17:39 +02:00
640 changed files with 129907 additions and 17694 deletions

View File

@ -11,6 +11,5 @@ POSTGRES_USER=user
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
ALPHA_VANTAGE_API_KEY=
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>

View File

@ -4,6 +4,9 @@ on:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@ -13,12 +16,12 @@ jobs:
- 18
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
cache: 'yarn'
@ -33,4 +36,4 @@ jobs:
run: yarn test
- name: Build application
run: yarn build:all
run: yarn build:production

View File

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

1
.gitignore vendored
View File

@ -27,6 +27,7 @@
/.angular/cache
.env
.env.prod
.nx/cache
/.sass-cache
/connect.lock
/coverage

View File

@ -1,2 +1,3 @@
/.nx/cache
/dist
/test/import

View File

@ -9,6 +9,7 @@
],
"attributeSort": "ASC",
"endOfLine": "auto",
"plugins": ["prettier-plugin-organize-attributes"],
"printWidth": 80,
"singleQuote": true,
"tabWidth": 2,

View File

@ -5,6 +5,708 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.29.0 - 2023-12-09
### Added
- Introduced a lazy-loaded activities table on the portfolio activities page (experimental)
### Changed
- Set the actions columns of various tables to stick at the end
- Increased the height of the tabs on mobile
- Improved the language localization for German (`de`)
- Improved the language localization for Türkçe (`tr`)
- Upgraded `marked` from version `4.2.12` to `9.1.6`
- Upgraded `ngx-markdown` from version `15.1.0` to `17.1.1`
- Upgraded `ng-extract-i18n-merge` from version `2.8.3` to `2.9.0`
### Fixed
- Fixed an issue in the biometric authentication registration
## 2.28.0 - 2023-12-02
### Added
- Added a historical cash balances table to the account detail dialog
- Introduced a `HasPermission` annotation for endpoints
### Changed
- Relaxed the check for duplicates in the preview step of the activities import (allow same day)
- Respected the `withExcludedAccounts` flag in the account balance time series
### Fixed
- Changed the mechanism of the `INTRADAY` data gathering to operate synchronously avoiding database deadlocks
## 2.27.1 - 2023-11-28
### Changed
- Reverted `Nx` from version `17.1.3` to `17.0.2`
## 2.27.0 - 2023-11-26
### Changed
- Extended the chart in the account detail dialog by historical cash balances
- Improved the error log for a timeout in the data source request
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `16.2.12` to `17.0.4`
- Upgraded `Nx` from version `17.0.2` to `17.1.3`
## 2.26.0 - 2023-11-24
### Changed
- Upgraded `prisma` from version `5.5.2` to `5.6.0`
- Upgraded `yahoo-finance2` from version `2.8.1` to `2.9.0`
## 2.25.1 - 2023-11-19
### Added
- Added a blog post: _Black Friday 2023_
### Changed
- Upgraded `http-status-codes` from version `2.2.0` to `2.3.0`
### Fixed
- Handled reading items from missing transaction point while getting the position (`getPosition()`) in portfolio service
## 2.24.0 - 2023-11-16
### Changed
- Improved the language localization for German (`de`)
### Fixed
- Fixed the "too many bind variables in prepared statement" issue of the data range functionality (`getRange()`) in the market data service
## 2.23.0 - 2023-11-15
### Added
- Extended the benchmarks in the markets overview by 50-Day and 200-Day trends (experimental)
- Set up the language localization for Polski (`pl`)
### Changed
- Improved the data source validation in the activities import
- Changed _Twitter_ to _𝕏_
- Improved the selection in the twitter bot service
- Improved the language localization for German (`de`)
- Upgraded `ng-extract-i18n-merge` from version `2.7.0` to `2.8.3`
- Upgraded `prettier` from version `3.0.3` to `3.1.0`
## 2.22.0 - 2023-11-11
### Added
- Added the platform icon to the account selectors in the cash balance transfer from one to another account
- Added the platform icon to the account selector of the create or edit activity dialog
### Changed
- Optimized the style of the carousel component on mobile for the testimonial section on the landing page
- Introduced action menus in the overview of the admin control panel
- Harmonized the name column in the historical market data table of the admin control panel
- Refactored the implementation of the data range functionality (`getRange()`) in the market data service
## 2.21.0 - 2023-11-09
### Changed
- Extended the system message
### Fixed
- Fixed the unit for the _Zen Mode_ in the overview tab of the home page
- Fixed an issue to get quotes in the _Financial Modeling Prep_ service
## 2.20.0 - 2023-11-08
### Changed
- Removed the loading indicator of the unit in the overview tab of the home page
- Improved the import of historical market data in the admin control panel
- Increased the timeout in the health check endpoint for data enhancers
- Increased the timeout in the health check endpoint for data providers
- Removed the account type from the `Account` database schema
## 2.19.0 - 2023-11-06
### Added
- Added a data migration to set `accountType` to `NULL` in the account database table
### Changed
- Improved the language localization for the _Fear & Greed Index_ (market mood)
- Improved the language localization for German (`de`)
### Fixed
- Improved the handling of derived currencies (`GBp`, `ILA`, `ZAc`)
## 2.18.0 - 2023-11-05
### Added
- Added support to import activities by `isin` in the _Yahoo Finance_ service
- Added a new tag with the major version to the docker image on _Docker Hub_
- Added a blog post: _Hacktoberfest 2023 Debriefing_
### Changed
- Upgraded `angular` from version `16.2.1` to `16.2.12`
### Fixed
- Fixed an issue to get quotes in the _CoinGecko_ service
- Loosened the validation in the activities import (expects values greater than or equal to 0 for `fee`, `quantity` and `unitPrice`)
- Handled an issue with a failing database query (`account.findMany()`) related to activities without account
## 2.17.0 - 2023-11-02
### Added
- Added a button to edit the exchange rates in the admin control panel
### Changed
- Improved the language localization for German (`de`)
### Fixed
- Fixed an issue in the biometric authentication
- Fixed the alignment of the icons in various menus
## 2.16.0 - 2023-10-29
### Changed
- Relaxed the check for duplicates in the preview step of the activities import (allow different accounts)
- Improved the usability and validation in the cash balance transfer from one to another account
- Changed the checkboxes to slide toggles in the overview of the admin control panel
- Switched from the deprecated (`PUT`) to the new endpoint (`POST`) to manage historical market data in the asset profile details dialog of the admin control panel
- Improved the date parsing in the import historical market data of the admin control panel
- Improved the localized meta data (keywords) in `html` files
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `5.4.2` to `5.5.2`
## 2.15.0 - 2023-10-26
### Added
- Added support to edit the name, asset class and asset sub class of asset profiles with `MANUAL` data source in the asset profile details dialog of the admin control panel
### Changed
- Improved the style and wording of the position detail dialog
- Improved the validation in the activities import (expects positive values for `fee`, `quantity` and `unitPrice`)
- Improved the validation in the cash balance transfer from one to another account (expects a positive value)
- Changed the currency selector in the create or update account dialog to `@angular/material/autocomplete`
- Upgraded `Nx` from version `16.7.4` to `17.0.2`
- Upgraded `uuid` from version `9.0.0` to `9.0.1`
- Upgraded `yahoo-finance2` from version `2.8.0` to `2.8.1`
### Fixed
- Fixed the chart in the account detail dialog for accounts excluded from analysis
- Verified the current benchmark before loading it on the analysis page
## 2.14.0 - 2023-10-21
### Added
- Added the _OpenFIGI_ data enhancer for _Financial Instrument Global Identifier_ (FIGI)
- Added `figi`, `figiComposite` and `figiShareClass` to the asset profile model
### Changed
- Moved the fees on account level feature from experimental to general availability
- Moved the interest on account level feature from experimental to general availability
- Moved the search for a holding from experimental to general availability
- Improved the error message in the activities import for `csv` files
- Removed the application version from the client
- Allowed to edit todays historical market data in the asset profile details dialog of the admin control panel
### Fixed
- Fixed the style of the active page in the header navigation
- Trimmed text in `i18n` service to query `messages.*.xlf` files on the server
## 2.13.0 - 2023-10-20
### Added
- Added a chart to the account detail dialog
- Added an `i18n` service to query `messages.*.xlf` files on the server
### Changed
- Changed the users table in the admin control panel to an `@angular/material` data table
- Improved the styling of the membership status
### Fixed
- Fixed an issue where holdings were requested twice from the server
## 2.12.0 - 2023-10-17
### Added
- Added the endpoint `GET api/v1/account/:id/balances` which provides historical cash balances
- Added support to search for an asset profile by `isin`, `name` and `symbol` as an administrator (experimental)
- Added support for creating asset profiles with `MANUAL` data source
### Changed
- Changed the checkboxes to slide toggles in the user settings of the user account page
- Extended the `copy-assets` `Nx` target to copy the locales to the servers assets
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `5.2.1` to `8.3`
### Fixed
- Displayed the transfer cash balance button based on a permission
- Fixed the biometric authentication
- Fixed the query to get asset profiles that match both the `dataSource` and `symbol` values
## 2.11.0 - 2023-10-14
### Added
- Added support to transfer a part of the cash balance from one to another account
- Extended the benchmarks in the markets overview by the date of the last all time high
- Added support to import historical market data in the admin control panel
### Changed
- Harmonized the style of the create button on the page for granting and revoking public access to share the portfolio
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `5.3.1` to `5.4.2`
### Fixed
- Fixed `FEE` and `INTEREST` types in the activities import of `csv` files
- Fixed the displayed currency of the cash balance in the create or update account dialog
## 2.10.0 - 2023-10-09
### Added
- Supported enter key press to submit the form of the create or update access dialog
### Changed
- Improved the display of the results in the search for a holding
- Changed the queue jobs view in the admin control panel to an `@angular/material` data table
- Improved the symbol conversion in the _EOD Historical Data_ service
## 2.9.0 - 2023-10-08
### Added
- Added support to search for a holding by `isin`, `name` and `symbol` (experimental)
- Added support for notes in the activities import
- Added support to search in the platform selector of the create or update account dialog
- Added support for a search query in the portfolio position endpoint
- Added the application version to the endpoint `GET api/v1/admin`
- Introduced a carousel component for the testimonial section on the landing page
### Changed
- Displayed the link to the markets overview on the home page without any permission
### Fixed
- Fixed the style of the active features page in the navigation on desktop
## 2.8.0 - 2023-10-03
### Added
- Supported enter key press to submit the form of the create or update account dialog
- Added the application version to the admin control panel
- Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/order`
### Changed
- Harmonized the settings icon of the user account page
- Improved the usability to set an asset profile as a benchmark
- Reload platforms after making a change in the admin control panel
- Reload tags after making a change in the admin control panel
### Fixed
- Fixed the sidebar navigation on the user account page
## 2.7.0 - 2023-09-30
### Added
- Added a new static portfolio analysis rule: Emergency fund setup
- Added tabs to the user account page
### Changed
- Set up the _Inter_ font family
- Upgraded `yahoo-finance2` from version `2.7.0` to `2.8.0`
### Fixed
- Fixed a link on the features page
## 2.6.0 - 2023-09-26
### Added
- Added the management of tags in the admin control panel
- Added a blog post: _Hacktoberfest 2023_
### Changed
- Upgraded `prettier` from version `3.0.2` to `3.0.3`
- Upgraded `yahoo-finance2` from version `2.5.0` to `2.7.0`
## 2.5.0 - 2023-09-23
### Added
- Added support for translated activity types in the activities table
- Added support for dates in `DD.MM.YYYY` format in the activities import
- Set up the language localization for Türkçe (`tr`)
### Changed
- Skipped creating queue jobs for asset profiles with `MANUAL` data source on creating a new activity
### Fixed
- Fixed an issue with the cash position in the holdings table
## 2.4.0 - 2023-09-19
### Added
- Added support for interest on account level (experimental)
### Changed
- Improved the preselected currency based on the accounts currency in the create or edit activity dialog
- Unlocked the experimental features setting for all users
- Upgraded `prisma` from version `5.2.0` to `5.3.1`
### Fixed
- Fixed a memory leak related to the servers timezone (behind UTC) in the data gathering
## 2.3.0 - 2023-09-17
### Added
- Added support for fees on account level (experimental)
### Fixed
- Fixed the export functionality for liabilities
## 2.2.0 - 2023-09-17
### Added
- Introduced a sidebar navigation on desktop
### Changed
- Improved the style of the system message
- Upgraded _Postgres_ from version `12` to `15` in the `docker-compose` files
## 2.1.0 - 2023-09-15
### Added
- Added support to drop a file in the import activities dialog
- Added a timeout to all data source requests
### Changed
- Harmonized the style of the user interface for granting and revoking public access to share the portfolio
- Removed the account type from the user interface as a preparation to remove it from the `Account` database schema
- Improved the logger output of the info service
- Harmonized the logger output: `<symbol> (<dataSource>)`
- Improved the language localization for German (`de`)
- Improved the language localization for Italian (`it`)
- Improved the language localization for Dutch (`nl`)
- Improved the read-only mode
### Fixed
- Fixed the timeout in _EOD Historical Data_ requests
- Fixed an issue with the portfolio summary caused by the language localization for Dutch (`nl`)
## 2.0.0 - 2023-09-09
### Added
- Added support for the cryptocurrency _CyberConnect_
- Added a blog post: _Announcing Ghostfolio 2.0_
### Changed
- **Breaking Change**: Removed the deprecated environment variable `BASE_CURRENCY`
- Improved the validation in the activities import
- Deactivated _Internet Identity_ as a social login provider for the account registration
- Improved the language localization for German (`de`)
- Refreshed the cryptocurrencies list
- Changed the version in the `docker-compose` files from `3.7` to `3.9`
- Upgraded `yahoo-finance2` from version `2.4.4` to `2.5.0`
### Fixed
- Fixed an issue in the _Yahoo Finance_ data enhancer where countries and sectors have been removed
## 1.305.0 - 2023-09-03
### Added
- Added _Hacker News_ to the _As seen in_ section on the landing page
### Changed
- Shortened the page titles
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `4.16.2` to `5.2.0`
- Upgraded `replace-in-file` from version `6.3.5` to `7.0.1`
- Upgraded `yahoo-finance2` from version `2.4.3` to `2.4.4`
### Fixed
- Fixed the alignment in the header navigation
- Fixed the alignment in the menu of the impersonation mode
## 1.304.0 - 2023-08-27
### Added
- Added a health check endpoint for data enhancers
### Changed
- Upgraded `Nx` from version `16.7.2` to `16.7.4`
- Upgraded `prettier` from version `2.8.4` to `3.0.2`
## 1.303.0 - 2023-08-23
### Added
- Added a blog post: _Ghostfolio joins OSS Friends_
### Changed
- Refreshed the cryptocurrencies list
- Improved the _OSS Friends_ page
### Fixed
- Fixed an issue with the _Trackinsight_ data enhancer for asset profile data
## 1.302.0 - 2023-08-20
### Changed
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `16.1.8` to `16.2.1`
- Upgraded `Nx` from version `16.6.0` to `16.7.2`
## 1.301.1 - 2023-08-19
### Added
- Added the data export feature to the user account page
- Added a currencies preset to the historical market data table of the admin control panel
- Added the _OSS Friends_ page
### Changed
- Improved the localized meta data in `html` files
### Fixed
- Fixed the rows with cash positions in the holdings table
- Fixed an issue with the date parsing in the historical market data editor of the admin control panel
## 1.300.0 - 2023-08-11
### Added
- Added more durations in the coupon system
### Changed
- Migrated the remaining requests from `bent` to `got`
## 1.299.1 - 2023-08-10
### Changed
- Optimized the activities import by allowing a different currency than the assets official one
- Added a timeout to the _EOD Historical Data_ requests
- Migrated the requests from `bent` to `got` in the _EOD Historical Data_ service
### Fixed
- Fixed the editing of the emergency fund
- Fixed the historical data gathering interval for asset profiles used as benchmarks having activities
## 1.298.0 - 2023-08-06
### Changed
- Improved the language localization for German (`de`)
- Upgraded `ng-extract-i18n-merge` from version `2.6.0` to `2.7.0`
- Upgraded `Nx` from version `16.5.5` to `16.6.0`
### Fixed
- Fixed the styles of various components (card, progress, tab) after the upgrade to `@angular/material` `16`
## 1.297.4 - 2023-08-05
### Added
- Added the footer to the public page
- Added a `copy-assets` `Nx` target to the client build
### Changed
- Improved the alignment of the region percentages on the allocations page
- Improved the alignment of the region percentages on the public page
- Improved the redirection of the home page to the localized home page
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `15.2.5` to `16.1.8`
- Upgraded `nestjs` from version `9.1.4` to `10.1.3`
- Upgraded `Nx` from version `16.0.3` to `16.5.5`
## 1.296.0 - 2023-08-01
### Changed
- Optimized the validation in the activities import by reducing the list to unique asset profiles
- Optimized the data gathering in the activities import
## 1.295.0 - 2023-07-30
### Added
- Added a step by step introduction for new users
### Fixed
- Removed the _Stay signed in_ setting on _Sign in with fingerprint_ activation
## 1.294.0 - 2023-07-29
### Changed
- Extended the allocations by market chart on the allocations page by unavailable data
### Fixed
- Considered liabilities in the total account value calculation
## 1.293.0 - 2023-07-26
### Added
- Added error handling for the _Redis_ connections to keep the app running if the connection fails
### Changed
- Set the `lastmod` dates of `sitemap.xml` dynamically
### Fixed
- Fixed the missing values in the holdings table
- Fixed the `no such file or directory` error caused by the missing `favicon.ico` file
## 1.292.0 - 2023-07-24
### Added
- Introduced the allocations by market chart on the allocations page
### Changed
- Upgraded `yahoo-finance2` from version `2.4.2` to `2.4.3`
### Fixed
- Fixed an issue in the public page
## 1.291.0 - 2023-07-23
### Added
- Broken down the emergency fund by cash and assets
- Added support for account balance time series
### Changed
- Renamed queries to presets in the historical market data table of the admin control panel
## 1.290.0 - 2023-07-16
### Added
- Added hints to the activity types in the create or edit activity dialog
- Added queries to the historical market data table of the admin control panel
### Changed
- Improved the usability of the login dialog
- Disabled the caching in the health check endpoint for data providers
- Improved the content of the Frequently Asked Questions (FAQ) page
- Upgraded `prisma` from version `4.15.0` to `4.16.2`
## 1.289.0 - 2023-07-14
### Changed
- Upgraded `yahoo-finance2` from version `2.4.1` to `2.4.2`
## 1.288.0 - 2023-07-12
### Changed
- Improved the loading state during filtering on the allocations page
- Beautified the names with ampersand (`&amp;`) in the asset profile
- Improved the language localization for German (`de`)
## 1.287.0 - 2023-07-09
### Changed
- Hid the average buy price in the position detail chart if there is no holding
- Improved the language localization for French (`fr`)
- Refactored the blog articles to standalone components
### Fixed
- Fixed the sorting by currency in the activities table
## 1.286.0 - 2023-07-03
### Fixed
- Fixed the creation of (wealth) items and liabilities
## 1.285.0 - 2023-07-01
### Added
@ -357,11 +1059,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a fallback to historical market data if a data provider does not provide live data
- Added a general health check endpoint
- Added health check endpoints for data providers
- Added a health check endpoint for data providers
### Changed
- Persisted today's market data continuously
- Persisted todays market data continuously
### Fixed
@ -446,7 +1148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Changed the slide toggles to checkboxes on the account page
- Changed the slide toggles to checkboxes on the user account page
- Changed the slide toggles to checkboxes in the admin control panel
- Increased the density of the theme
- Migrated the style of various components to `@angular/material` `15` (mdc)
@ -595,7 +1297,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Filtered activities with type `ITEM` from search results
- Considered the user's language in the _Stripe_ checkout
- Considered the users language in the _Stripe_ checkout
- Upgraded the _Stripe_ dependencies
- Upgraded `twitter-api-v2` from version `1.10.3` to `1.14.2`
@ -1008,7 +1710,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the language selector on the account page
- Improved the language selector on the user account page
- Improved the wording in the _X-ray_ section (net worth instead of investment)
- Extended the asset profile details dialog in the admin control panel
- Updated the browserslist database
@ -1253,7 +1955,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Set up the language localization for Italiano (`it`)
- Set up the language localization for Italian (`it`)
- Extended the landing page
## 1.195.0 - 20.09.2022
@ -1426,7 +2128,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a language selector to the account page
- Added a language selector to the user account page
- Added support for translated labels in the value component
### Changed
@ -1755,7 +2457,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the user id to the account page
- Added the user id to the user account page
- Added a new view with jobs of the queue to the admin control panel
### Changed
@ -1821,7 +2523,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the _Ghostfolio_ trailer to the landing page
- Extended the markets overview by benchmarks (current change to the all time high)
- Extended the benchmarks in the markets overview by the current change to the all time high
## 1.151.0 - 24.05.2022
@ -2269,7 +2971,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Moved the countries and sectors charts in the position detail dialog
- Distinguished today's data point of historical data in the admin control panel
- Distinguished todays data point of historical data in the admin control panel
- Restructured the server modules
### Fixed
@ -2676,7 +3378,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Supported the management of additional currencies in the admin control panel
- Introduced the system message
- Introduced the read only mode
- Introduced the read-only mode
### Changed
@ -3410,7 +4112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Respected the cash balance on the analysis page
- Improved the settings selectors on the account page
- Improved the settings selectors on the user account page
- Harmonized the slogan to "Open Source Wealth Management Software"
### Fixed
@ -3876,7 +4578,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a gradient to the line charts
- Added a selector to set the base currency on the account page
- Added a selector to set the base currency on the user account page
## 0.81.0 - 06.04.2021
@ -4190,7 +4892,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added the membership status to the account page
- Added the membership status to the user account page
### Fixed

View File

@ -1,5 +1,17 @@
# Ghostfolio Development Guide
## Experimental Features
New functionality can be enabled using a feature flag switch from the user settings.
### Backend
Remove permission in `UserService` using `without()`
### Frontend
Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
## Git
### Rebase
@ -8,16 +20,28 @@
## Dependencies
### Angular
#### Upgrade (minor versions)
1. Run `npx npm-check-updates --upgrade --target "minor" --filter "/@angular.*/"`
### Nx
#### Upgrade
1. Run `yarn nx migrate latest`
1. Make sure `package.json` changes make sense and then run `yarn install`
1. Run `yarn nx migrate --run-migrations`
1. Run `yarn nx migrate --run-migrations` (Run `YARN_NODE_LINKER="node-modules" NX_MIGRATE_SKIP_INSTALL=1 yarn nx migrate --run-migrations` due to https://github.com/nrwl/nx/issues/16338)
### Prisma
#### Access database via GUI
Run `yarn database:gui`
https://www.prisma.io/studio
#### Synchronize schema with database for prototyping
Run `yarn database:push`

View File

@ -33,7 +33,7 @@ COPY ./tsconfig.base.json tsconfig.base.json
COPY ./libs libs
COPY ./apps apps
RUN yarn build:all
RUN yarn build:production
# Prepare the dist image with additional node_modules
WORKDIR /ghostfolio/dist/apps/api
@ -58,4 +58,4 @@ RUN apt update && apt install -y \
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
WORKDIR /ghostfolio/apps/api
EXPOSE ${PORT:-3333}
CMD [ "yarn", "start:prod" ]
CMD [ "yarn", "start:production" ]

View File

@ -13,6 +13,8 @@
[![Shield: Contributions Welcome](https://img.shields.io/badge/Contributions-Welcome-orange.svg)](#contributing)
[![Shield: License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2)
</div>
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
@ -25,7 +27,7 @@
## Ghostfolio Premium
Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. Revenue is used to cover the costs of the hosting infrastructure and to fund ongoing development.
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
@ -136,9 +138,9 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
At each start, the container will automatically apply the database schema migrations if needed.
### Run with _Unraid_ (Community)
### Home Server Systems (Community)
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
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).
## Development
@ -153,7 +155,6 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
### Setup
1. Run `yarn install`
1. Run `yarn build:dev` to build the source code including the assets
1. Run `docker-compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
1. Run `yarn database:setup` to initialize the database schema
1. Start the server and the client (see [_Development_](#Development))
@ -229,18 +230,18 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
}
```
| Field | Type | Description |
| ---------- | ------------------- | -------------------------------------------------- |
| accountId | string (`optional`) | Id of the account |
| comment | string (`optional`) | Comment of the activity |
| currency | string | `CHF` \| `EUR` \| `USD` etc. |
| dataSource | string | `MANUAL` (for type `ITEM`) \| `YAHOO` |
| date | string | Date in the format `ISO-8601` |
| fee | number | Fee of the activity |
| quantity | number | Quantity of the activity |
| symbol | string | Symbol of the activity (suitable for `dataSource`) |
| type | string | `BUY` \| `DIVIDEND` \| `ITEM` \| `SELL` |
| unitPrice | number | Price per unit of the activity |
| Field | Type | Description |
| ---------- | ------------------- | ----------------------------------------------------------------------------- |
| accountId | string (`optional`) | Id of the account |
| comment | string (`optional`) | Comment of the activity |
| currency | string | `CHF` \| `EUR` \| `USD` etc. |
| dataSource | string | `COINGECKO` \| `MANUAL` (for type `ITEM`) \| `YAHOO` |
| date | string | Date in the format `ISO-8601` |
| fee | number | Fee of the activity |
| quantity | number | Quantity of the activity |
| symbol | string | Symbol of the activity (suitable for `dataSource`) |
| type | string | `BUY` \| `DIVIDEND` \| `FEE` \| `INTEREST` \| `ITEM` \| `LIABILITY` \| `SELL` |
| unitPrice | number | Price per unit of the activity |
#### Response
@ -263,13 +264,15 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
## Community Projects
- [ghostfolio-cli](https://github.com/DerAndereJohannes/ghostfolio-cli): Command-line interface to access your portfolio
Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio
Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ repository to get listed as well. [Learn more →](https://docs.github.com/en/articles/classifying-your-repository-with-topics)
## Contributing
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you.
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).

View File

@ -33,7 +33,7 @@
"outputs": ["{options.outputPath}"]
},
"serve": {
"executor": "@nx/node:node",
"executor": "@nx/js:node",
"options": {
"buildTarget": "api:build"
}
@ -47,8 +47,7 @@
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/api/jest.config.ts",
"passWithNoTests": true
"jestConfig": "apps/api/jest.config.ts"
},
"outputs": ["{workspaceRoot}/coverage/apps/api"]
}

View File

@ -17,7 +17,6 @@ import { AuthGuard } from '@nestjs/passport';
import { Access as AccessModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccessModule } from './access.module';
import { AccessService } from './access.service';
import { CreateAccessDto } from './create-access.dto';
@ -83,7 +82,7 @@ export class AccessController {
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
public async deleteAccess(@Param('id') id: string): Promise<AccessModule> {
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> {
const access = await this.accessService.access({ id });
if (

View File

@ -8,4 +8,8 @@ export class CreateAccessDto {
@IsOptional()
@IsString()
granteeUserId?: string;
@IsOptional()
@IsString()
type?: 'PUBLIC';
}

View File

@ -0,0 +1,51 @@
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
Delete,
HttpException,
Inject,
Param,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AccountBalanceService } from './account-balance.service';
import { AuthGuard } from '@nestjs/passport';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccountBalance } from '@prisma/client';
@Controller('account-balance')
export class AccountBalanceController {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
public async deleteAccountBalance(
@Param('id') id: string
): Promise<AccountBalance> {
const accountBalance = await this.accountBalanceService.accountBalance({
id
});
if (
!hasPermission(
this.request.user.permissions,
permissions.deleteAccountBalance
) ||
!accountBalance ||
accountBalance.userId !== this.request.user.id
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.accountBalanceService.deleteAccountBalance({
id
});
}
}

View File

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

View File

@ -0,0 +1,91 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { AccountBalance, Prisma } from '@prisma/client';
@Injectable()
export class AccountBalanceService {
public constructor(
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
public async accountBalance(
accountBalanceWhereInput: Prisma.AccountBalanceWhereInput
): Promise<AccountBalance | null> {
return this.prismaService.accountBalance.findFirst({
include: {
Account: true
},
where: accountBalanceWhereInput
});
}
public async createAccountBalance(
data: Prisma.AccountBalanceCreateInput
): Promise<AccountBalance> {
return this.prismaService.accountBalance.create({
data
});
}
public async deleteAccountBalance(
where: Prisma.AccountBalanceWhereUniqueInput
): Promise<AccountBalance> {
return this.prismaService.accountBalance.delete({
where
});
}
public async getAccountBalances({
filters,
user,
withExcludedAccounts
}: {
filters?: Filter[];
user: UserWithSettings;
withExcludedAccounts?: boolean;
}): Promise<AccountBalancesResponse> {
const where: Prisma.AccountBalanceWhereInput = { userId: user.id };
const accountFilter = filters?.find(({ type }) => {
return type === 'ACCOUNT';
});
if (accountFilter) {
where.accountId = accountFilter.id;
}
if (withExcludedAccounts === false) {
where.Account = { isExcluded: false };
}
const balances = await this.prismaService.accountBalance.findMany({
where,
orderBy: {
date: 'asc'
},
select: {
Account: true,
date: true,
id: true,
value: true
}
});
return {
balances: balances.map((balance) => {
return {
...balance,
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
balance.value,
balance.Account.currency,
user.Settings.settings.baseCurrency
)
};
})
};
}
}

View File

@ -1,8 +1,12 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { Accounts } from '@ghostfolio/common/interfaces';
import {
AccountBalancesResponse,
Accounts
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type {
AccountWithValue,
@ -29,11 +33,13 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccountService } from './account.service';
import { CreateAccountDto } from './create-account.dto';
import { TransferBalanceDto } from './transfer-balance.dto';
import { UpdateAccountDto } from './update-account.dto';
@Controller('account')
export class AccountController {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService,
private readonly impersonationService: ImpersonationService,
private readonly portfolioService: PortfolioService,
@ -115,6 +121,18 @@ export class AccountController {
return accountsWithAggregations.accounts[0];
}
@Get(':id/balances')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountBalancesById(
@Param('id') id: string
): Promise<AccountBalancesResponse> {
return this.accountBalanceService.getAccountBalances({
filters: [{ id, type: 'ACCOUNT' }],
user: this.request.user
});
}
@Post()
@UseGuards(AuthGuard('jwt'))
public async createAccount(
@ -154,6 +172,68 @@ export class AccountController {
}
}
@Post('transfer-balance')
@UseGuards(AuthGuard('jwt'))
public async transferAccountBalance(
@Body() { accountIdFrom, accountIdTo, balance }: TransferBalanceDto
) {
if (
!hasPermission(this.request.user.permissions, permissions.updateAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const accountsOfUser = await this.accountService.getAccounts(
this.request.user.id
);
const accountFrom = accountsOfUser.find(({ id }) => {
return id === accountIdFrom;
});
const accountTo = accountsOfUser.find(({ id }) => {
return id === accountIdTo;
});
if (!accountFrom || !accountTo) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
if (accountFrom.id === accountTo.id) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
if (accountFrom.balance < balance) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
await this.accountService.updateAccountBalance({
accountId: accountFrom.id,
amount: -balance,
currency: accountFrom.currency,
userId: this.request.user.id
});
await this.accountService.updateAccountBalance({
accountId: accountTo.id,
amount: balance,
currency: accountFrom.currency,
userId: this.request.user.id
});
}
@Put(':id')
@UseGuards(AuthGuard('jwt'))
public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) {

View File

@ -1,3 +1,4 @@
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
@ -15,6 +16,7 @@ import { AccountService } from './account.service';
controllers: [AccountController],
exports: [AccountService],
imports: [
AccountBalanceModule,
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,

View File

@ -1,3 +1,4 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Filter } from '@ghostfolio/common/interfaces';
@ -11,16 +12,21 @@ import { CashDetails } from './interfaces/cash-details.interface';
@Injectable()
export class AccountService {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
public async account(
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput
): Promise<Account | null> {
return this.prismaService.account.findUnique({
where: accountWhereUniqueInput
public async account({
id_userId
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
const { id, userId } = id_userId;
const [account] = await this.accounts({
where: { id, userId }
});
return account;
}
public async accountWithOrders(
@ -50,9 +56,11 @@ export class AccountService {
Platform?: Platform;
})[]
> {
const { include, skip, take, cursor, where, orderBy } = params;
const { include = {}, skip, take, cursor, where, orderBy } = params;
return this.prismaService.account.findMany({
include.balances = { orderBy: { date: 'desc' }, take: 1 };
const accounts = await this.prismaService.account.findMany({
cursor,
include,
orderBy,
@ -60,15 +68,36 @@ export class AccountService {
take,
where
});
return accounts.map((account) => {
account = { ...account, balance: account.balances[0]?.value ?? 0 };
delete account.balances;
return account;
});
}
public async createAccount(
data: Prisma.AccountCreateInput,
aUserId: string
): Promise<Account> {
return this.prismaService.account.create({
const account = await this.prismaService.account.create({
data
});
await this.prismaService.accountBalance.create({
data: {
Account: {
connect: {
id_userId: { id: account.id, userId: aUserId }
}
},
value: data.balance
}
});
return account;
}
public async deleteAccount(
@ -80,7 +109,7 @@ export class AccountService {
});
}
public async getAccounts(aUserId: string) {
public async getAccounts(aUserId: string): Promise<Account[]> {
const accounts = await this.accounts({
include: { Order: true, Platform: true },
orderBy: { name: 'asc' },
@ -167,6 +196,18 @@ export class AccountService {
aUserId: string
): Promise<Account> {
const { data, where } = params;
await this.prismaService.accountBalance.create({
data: {
Account: {
connect: {
id_userId: where.id_userId
}
},
value: <number>data.balance
}
});
return this.prismaService.account.update({
data,
where
@ -177,13 +218,13 @@ export class AccountService {
accountId,
amount,
currency,
date,
date = new Date(),
userId
}: {
accountId: string;
amount: number;
currency: string;
date: Date;
date?: Date;
userId: string;
}) {
const { balance, currency: currencyOfAccount } = await this.account({
@ -202,16 +243,17 @@ export class AccountService {
);
if (amountInCurrencyOfAccount) {
await this.prismaService.account.update({
data: {
balance: new Big(balance).plus(amountInCurrencyOfAccount).toNumber()
},
where: {
id_userId: {
userId,
id: accountId
await this.accountBalanceService.createAccountBalance({
date,
Account: {
connect: {
id_userId: {
userId,
id: accountId
}
}
}
},
value: new Big(balance).plus(amountInCurrencyOfAccount).toNumber()
});
}
}

View File

@ -1,4 +1,3 @@
import { AccountType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsBoolean,
@ -10,9 +9,6 @@ import {
import { isString } from 'lodash';
export class CreateAccountDto {
@IsString()
accountType: AccountType;
@IsNumber()
balance: number;

View File

@ -0,0 +1,13 @@
import { IsNumber, IsPositive, IsString } from 'class-validator';
export class TransferBalanceDto {
@IsString()
accountIdFrom: string;
@IsString()
accountIdTo: string;
@IsNumber()
@IsPositive()
balance: number;
}

View File

@ -1,4 +1,3 @@
import { AccountType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsBoolean,
@ -10,9 +9,6 @@ import {
import { isString } from 'lodash';
export class UpdateAccountDto {
@IsString()
accountType: AccountType;
@IsNumber()
balance: number;

View File

@ -1,21 +1,27 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import {
DEFAULT_PAGE_SIZE,
GATHER_ASSET_PROFILE_PROCESS,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config';
import {
getAssetProfileIdentifier,
resetHours
} from '@ghostfolio/common/helper';
import {
AdminData,
AdminMarketData,
AdminMarketDataDetails,
EnhancedSymbolProfile,
Filter
EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import type {
MarketDataPreset,
RequestWithUser
} from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -34,17 +40,19 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client';
import { isDate } from 'date-fns';
import { isDate, parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service';
import { UpdateAssetProfileDto } from './update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto';
import { UpdateMarketDataDto } from './update-market-data.dto';
@Controller('admin')
export class AdminController {
public constructor(
private readonly adminService: AdminService,
private readonly apiService: ApiService,
private readonly dataGatheringService: DataGatheringService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser
@ -113,7 +121,7 @@ export class AdminController {
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
jobId: getAssetProfileIdentifier({ dataSource, symbol })
}
};
})
@ -149,7 +157,7 @@ export class AdminController {
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
jobId: getAssetProfileIdentifier({ dataSource, symbol })
}
};
})
@ -182,7 +190,7 @@ export class AdminController {
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
jobId: getAssetProfileIdentifier({ dataSource, symbol })
}
});
}
@ -229,7 +237,7 @@ export class AdminController {
);
}
const date = new Date(dateString);
const date = parseISO(dateString);
if (!isDate(date)) {
throw new HttpException(
@ -249,6 +257,8 @@ export class AdminController {
@UseGuards(AuthGuard('jwt'))
public async getMarketData(
@Query('assetSubClasses') filterByAssetSubClasses?: string,
@Query('presetId') presetId?: MarketDataPreset,
@Query('query') filterBySearchQuery?: string,
@Query('skip') skip?: number,
@Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
@ -266,19 +276,14 @@ export class AdminController {
);
}
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
const filters: Filter[] = [
...assetSubClasses.map((assetSubClass) => {
return <Filter>{
id: assetSubClass,
type: 'ASSET_SUB_CLASS'
};
})
];
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAssetSubClasses,
filterBySearchQuery
});
return this.adminService.getMarketData({
filters,
presetId,
sortColumn,
sortDirection,
skip: isNaN(skip) ? undefined : skip,
@ -307,6 +312,43 @@ export class AdminController {
return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
}
@Post('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
public async updateMarketData(
@Body() data: UpdateBulkMarketDataDto,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map(
({ date, marketPrice }) => ({
dataSource,
marketPrice,
symbol,
date: resetHours(parseISO(date)),
state: 'CLOSE'
})
);
return this.marketDataService.updateMany({
data: dataBulkUpdate
});
}
/**
* @deprecated
*/
@Put('market-data/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
public async update(
@ -327,7 +369,7 @@ export class AdminController {
);
}
const date = new Date(dateString);
const date = parseISO(dateString);
return this.marketDataService.updateMarketData({
data: { marketPrice: data.marketPrice, state: 'CLOSE' },
@ -359,8 +401,11 @@ export class AdminController {
StatusCodes.FORBIDDEN
);
}
return this.adminService.addAssetProfile({ dataSource, symbol });
return this.adminService.addAssetProfile({
dataSource,
symbol,
currency: this.request.user.Settings.settings.baseCurrency
});
}
@Delete('profile-data/:dataSource/:symbol')

View File

@ -1,4 +1,5 @@
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -15,6 +16,7 @@ import { QueueModule } from './queue/queue.module';
@Module({
imports: [
ApiModule,
ConfigurationModule,
DataGatheringModule,
DataProviderModule,

View File

@ -1,4 +1,5 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -7,25 +8,33 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
DEFAULT_PAGE_SIZE,
PROPERTY_CURRENCIES
DEFAULT_CURRENCY,
PROPERTY_CURRENCIES,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED
} from '@ghostfolio/common/config';
import {
AdminData,
AdminMarketData,
AdminMarketDataDetails,
AdminMarketDataItem,
Filter,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types';
import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client';
import {
AssetSubClass,
DataSource,
Prisma,
Property,
SymbolProfile
} from '@prisma/client';
import { differenceInDays } from 'date-fns';
import { groupBy } from 'lodash';
@Injectable()
export class AdminService {
private baseCurrency: string;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
@ -35,15 +44,22 @@ export class AdminService {
private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService,
private readonly symbolProfileService: SymbolProfileService
) {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
}
) {}
public async addAssetProfile({
currency,
dataSource,
symbol
}: UniqueAsset): Promise<SymbolProfile | never> {
}: UniqueAsset & { currency?: string }): Promise<SymbolProfile | never> {
try {
if (dataSource === 'MANUAL') {
return this.symbolProfileService.add({
currency,
dataSource,
symbol
});
}
const assetProfiles = await this.dataProviderService.getAssetProfiles([
{ dataSource, symbol }
]);
@ -81,15 +97,23 @@ export class AdminService {
exchangeRates: this.exchangeRateDataService
.getCurrencies()
.filter((currency) => {
return currency !== this.baseCurrency;
return currency !== DEFAULT_CURRENCY;
})
.map((currency) => {
const label1 = DEFAULT_CURRENCY;
const label2 = currency;
return {
label1: this.baseCurrency,
label2: currency,
label1,
label2,
dataSource:
DataSource[
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
],
symbol: `${label1}${label2}`,
value: this.exchangeRateDataService.toCurrency(
1,
this.baseCurrency,
DEFAULT_CURRENCY,
currency
)
};
@ -97,18 +121,21 @@ export class AdminService {
settings: await this.propertyService.get(),
transactionCount: await this.prismaService.order.count(),
userCount: await this.prismaService.user.count(),
users: await this.getUsersWithAnalytics()
users: await this.getUsersWithAnalytics(),
version: environment.version
};
}
public async getMarketData({
filters,
presetId,
sortColumn,
sortDirection,
skip,
take = DEFAULT_PAGE_SIZE
take = Number.MAX_SAFE_INTEGER
}: {
filters?: Filter[];
presetId?: MarketDataPreset;
skip?: number;
sortColumn?: string;
sortDirection?: Prisma.SortOrder;
@ -118,10 +145,23 @@ export class AdminService {
[{ symbol: 'asc' }];
const where: Prisma.SymbolProfileWhereInput = {};
if (presetId === 'CURRENCIES') {
return this.getMarketDataForCurrencies();
} else if (
presetId === 'ETF_WITHOUT_COUNTRIES' ||
presetId === 'ETF_WITHOUT_SECTORS'
) {
filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
}
const searchQuery = filters.find(({ type }) => {
return type === 'SEARCH_QUERY';
})?.id;
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
filters,
(filter) => {
return filter.type;
({ type }) => {
return type;
}
);
@ -134,6 +174,14 @@ export class AdminService {
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
}
if (searchQuery) {
where.OR = [
{ isin: { mode: 'insensitive', startsWith: searchQuery } },
{ name: { mode: 'insensitive', startsWith: searchQuery } },
{ symbol: { mode: 'insensitive', startsWith: searchQuery } }
];
}
if (sortColumn) {
orderBy = [{ [sortColumn]: sortDirection }];
@ -146,7 +194,7 @@ export class AdminService {
}
}
const [assetProfiles, count] = await Promise.all([
let [assetProfiles, count] = await Promise.all([
this.prismaService.symbolProfile.findMany({
orderBy,
skip,
@ -160,7 +208,9 @@ export class AdminService {
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
name: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
@ -174,44 +224,64 @@ export class AdminService {
this.prismaService.symbolProfile.count({ where })
]);
return {
count,
marketData: assetProfiles.map(
({
_count,
let marketData = assetProfiles.map(
({
_count,
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
name,
Order,
sectors,
symbol
}) => {
const countriesCount = countries ? Object.keys(countries).length : 0;
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
return {
assetClass,
assetSubClass,
comment,
countries,
currency,
countriesCount,
dataSource,
Order,
sectors,
symbol
}) => {
const countriesCount = countries ? Object.keys(countries).length : 0;
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
name,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.Order,
date: Order?.[0]?.date
};
}
);
return {
assetClass,
assetSubClass,
comment,
countriesCount,
dataSource,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.Order,
date: Order?.[0]?.date
};
}
)
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
}
count = marketData.length;
}
return {
count,
marketData
};
}
@ -247,15 +317,21 @@ export class AdminService {
}
public async patchAssetProfileData({
assetClass,
assetSubClass,
comment,
dataSource,
name,
scraperConfiguration,
symbol,
symbolMapping
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
await this.symbolProfileService.updateSymbolProfile({
assetClass,
assetSubClass,
comment,
dataSource,
name,
scraperConfiguration,
symbol,
symbolMapping
@ -280,13 +356,47 @@ export class AdminService {
response = await this.propertyService.delete({ key });
}
if (key === PROPERTY_CURRENCIES) {
if (key === PROPERTY_IS_READ_ONLY_MODE && value === 'true') {
await this.putSetting(PROPERTY_IS_USER_SIGNUP_ENABLED, 'false');
} else if (key === PROPERTY_CURRENCIES) {
await this.exchangeRateDataService.initialize();
}
return response;
}
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
const marketDataItems = await this.prismaService.marketData.groupBy({
_count: true,
by: ['dataSource', 'symbol']
});
const marketData: AdminMarketDataItem[] = this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
return {
dataSource,
marketDataItemCount,
symbol,
assetClass: 'CASH',
countriesCount: 0,
currency: symbol.replace(DEFAULT_CURRENCY, ''),
name: symbol,
sectorsCount: 0
};
});
return { marketData, count: marketData.length };
}
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
let orderBy: any = {
createdAt: 'desc'

View File

@ -1,11 +1,23 @@
import { Prisma } from '@prisma/client';
import { IsObject, IsOptional, IsString } from 'class-validator';
import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
import { IsEnum, IsObject, IsOptional, IsString } from 'class-validator';
export class UpdateAssetProfileDto {
@IsEnum(AssetClass, { each: true })
@IsOptional()
assetClass?: AssetClass;
@IsEnum(AssetSubClass, { each: true })
@IsOptional()
assetSubClass?: AssetSubClass;
@IsString()
@IsOptional()
comment?: string;
@IsString()
@IsOptional()
name?: string;
@IsObject()
@IsOptional()
scraperConfiguration?: Prisma.InputJsonObject;

View File

@ -0,0 +1,11 @@
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, isNotEmptyObject } from 'class-validator';
import { UpdateMarketDataDto } from './update-market-data.dto';
export class UpdateBulkMarketDataDto {
@ArrayNotEmpty()
@IsArray()
@Type(() => UpdateMarketDataDto)
marketData: UpdateMarketDataDto[];
}

View File

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

View File

@ -7,11 +7,16 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import {
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { StatusCodes } from 'http-status-codes';
import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module';
@ -23,7 +28,6 @@ import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module';
import { FrontendMiddleware } from './frontend.middleware';
import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
@ -32,8 +36,10 @@ import { OrderModule } from './order/order.module';
import { PlatformModule } from './platform/platform.module';
import { PortfolioModule } from './portfolio/portfolio.module';
import { RedisCacheModule } from './redis-cache/redis-cache.module';
import { SitemapModule } from './sitemap/sitemap.module';
import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module';
import { TagModule } from './tag/tag.module';
import { UserModule } from './user/user.module';
@Module({
@ -70,31 +76,37 @@ import { UserModule } from './user/user.module';
RedisCacheModule,
ScheduleModule.forRoot(),
ServeStaticModule.forRoot({
serveStaticOptions: {
/*etag: false // Disable etag header to fix PWA
setHeaders: (res, path) => {
if (path.includes('ngsw.json')) {
// Disable cache (https://stackoverflow.com/questions/22632593/how-to-disable-webpage-caching-in-expressjs-nodejs/39775595)
// https://gertjans.home.xs4all.nl/javascript/cache-control.html#no-cache
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
}
}*/
},
exclude: ['/api*', '/sitemap.xml'],
rootPath: join(__dirname, '..', 'client'),
exclude: ['/api*']
serveStaticOptions: {
setHeaders: (res) => {
if (res.req?.path === '/') {
let languageCode = DEFAULT_LANGUAGE_CODE;
try {
const code = res.req.headers['accept-language']
.split(',')[0]
.split('-')[0];
if (SUPPORTED_LANGUAGE_CODES.includes(code)) {
languageCode = code;
}
} catch {}
res.set('Location', `/${languageCode}`);
res.statusCode = StatusCodes.MOVED_PERMANENTLY;
}
}
}
}),
SitemapModule,
SubscriptionModule,
SymbolModule,
TagModule,
TwitterBotModule,
UserModule
],
controllers: [AppController],
providers: [CronService]
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(FrontendMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}
export class AppModule {}

View File

@ -41,9 +41,8 @@ export class AuthController {
@Param('accessToken') accessToken: string
): Promise<OAuthResponse> {
try {
const authToken = await this.authService.validateAnonymousLogin(
accessToken
);
const authToken =
await this.authService.validateAnonymousLogin(accessToken);
return { authToken };
} catch {
throw new HttpException(

View File

@ -55,7 +55,7 @@ export class AuthService {
const isUserSignupEnabled =
await this.propertyService.isUserSignupEnabled();
if (!isUserSignupEnabled) {
if (!isUserSignupEnabled || true) {
throw new Error('Sign up forbidden');
}

View File

@ -64,7 +64,7 @@ export class WebAuthService {
}
};
const options = generateRegistrationOptions(opts);
const options = await generateRegistrationOptions(opts);
await this.userService.updateUser({
data: {
@ -88,10 +88,16 @@ export class WebAuthService {
let verification: VerifiedRegistrationResponse;
try {
const opts: VerifyRegistrationResponseOpts = {
credential,
expectedChallenge,
expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID
expectedRPID: this.rpID,
response: {
clientExtensionResults: credential.clientExtensionResults,
id: credential.id,
rawId: credential.rawId,
response: credential.response,
type: 'public-key'
}
};
verification = await verifyRegistrationResponse(opts);
} catch (error) {
@ -117,8 +123,8 @@ export class WebAuthService {
*/
existingDevice = await this.deviceService.createAuthDevice({
counter,
credentialPublicKey,
credentialId: credentialID,
credentialId: Buffer.from(credentialID),
credentialPublicKey: Buffer.from(credentialPublicKey),
User: { connect: { id: user.id } }
});
}
@ -152,7 +158,7 @@ export class WebAuthService {
userVerification: 'preferred'
};
const options = generateAuthenticationOptions(opts);
const options = await generateAuthenticationOptions(opts);
await this.userService.updateUser({
data: {
@ -181,7 +187,6 @@ export class WebAuthService {
let verification: VerifiedAuthenticationResponse;
try {
const opts: VerifyAuthenticationResponseOpts = {
credential,
authenticator: {
credentialID: device.credentialId,
credentialPublicKey: device.credentialPublicKey,
@ -189,9 +194,16 @@ export class WebAuthService {
},
expectedChallenge: `${user.authChallenge}`,
expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID
expectedRPID: this.rpID,
response: {
clientExtensionResults: credential.clientExtensionResults,
id: credential.id,
rawId: credential.rawId,
response: credential.response,
type: 'public-key'
}
};
verification = verifyAuthenticationResponse(opts);
verification = await verifyAuthenticationResponse(opts);
} catch (error) {
Logger.error(error, 'WebAuthService');
throw new InternalServerErrorException({ error: error.message });

View File

@ -10,6 +10,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
Delete,
Get,
HttpException,
Inject,
@ -32,32 +33,6 @@ export class BenchmarkController {
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {
return {
benchmarks: await this.benchmarkService.getBenchmarks()
};
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string
): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString);
return this.benchmarkService.getMarketDataBySymbol({
dataSource,
startDate,
symbol
});
}
@Post()
@UseGuards(AuthGuard('jwt'))
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
@ -94,4 +69,70 @@ export class BenchmarkController {
);
}
}
@Delete(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
public async deleteBenchmark(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
try {
const benchmark = await this.benchmarkService.deleteBenchmark({
dataSource,
symbol
});
if (!benchmark) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return benchmark;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {
return {
benchmarks: await this.benchmarkService.getBenchmarks()
};
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string
): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString);
return this.benchmarkService.getMarketDataBySymbol({
dataSource,
startDate,
symbol
});
}
}

View File

@ -9,17 +9,21 @@ import {
MAX_CHART_ITEMS,
PROPERTY_BENCHMARKS
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
DATE_FORMAT,
calculateBenchmarkTrend
} from '@ghostfolio/common/helper';
import {
BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { BenchmarkTrend } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import Big from 'big.js';
import { format } from 'date-fns';
import { format, subDays } from 'date-fns';
import { uniqBy } from 'lodash';
import ms from 'ms';
@ -45,9 +49,34 @@ export class BenchmarkService {
return 0;
}
public async getBenchmarks({ useCache = true } = {}): Promise<
BenchmarkResponse['benchmarks']
> {
public async getBenchmarkTrends({ dataSource, symbol }: UniqueAsset) {
const historicalData = await this.marketDataService.marketDataItems({
orderBy: {
date: 'desc'
},
where: {
dataSource,
symbol,
date: { gte: subDays(new Date(), 400) }
}
});
const fiftyDayAverage = calculateBenchmarkTrend({
historicalData,
days: 50
});
const twoHundredDayAverage = calculateBenchmarkTrend({
historicalData,
days: 200
});
return { trend50d: fiftyDayAverage, trend200d: twoHundredDayAverage };
}
public async getBenchmarks({
enableSharing = false,
useCache = true
} = {}): Promise<BenchmarkResponse['benchmarks']> {
let benchmarks: BenchmarkResponse['benchmarks'];
if (useCache) {
@ -62,21 +91,36 @@ export class BenchmarkService {
} catch {}
}
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles();
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({
enableSharing
});
const promises: Promise<number>[] = [];
const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] =
[];
const promisesBenchmarkTrends: Promise<{
trend50d: BenchmarkTrend;
trend200d: BenchmarkTrend;
}>[] = [];
const quotes = await this.dataProviderService.getQuotes(
benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
const quotes = await this.dataProviderService.getQuotes({
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
})
);
});
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
promisesAllTimeHighs.push(
this.marketDataService.getMax({ dataSource, symbol })
);
promisesBenchmarkTrends.push(
this.getBenchmarkTrends({ dataSource, symbol })
);
}
const allTimeHighs = await Promise.all(promises);
const [allTimeHighs, benchmarkTrends] = await Promise.all([
Promise.all(promisesAllTimeHighs),
Promise.all(promisesBenchmarkTrends)
]);
let storeInCache = true;
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
@ -85,9 +129,9 @@ export class BenchmarkService {
let performancePercentFromAllTimeHigh = 0;
if (allTimeHigh && marketPrice) {
if (allTimeHigh?.marketPrice && marketPrice) {
performancePercentFromAllTimeHigh = this.calculateChangeInPercentage(
allTimeHigh,
allTimeHigh.marketPrice,
marketPrice
);
} else {
@ -101,9 +145,12 @@ export class BenchmarkService {
name: benchmarkAssetProfiles[index].name,
performances: {
allTimeHigh: {
date: allTimeHigh?.date,
performancePercent: performancePercentFromAllTimeHigh
}
}
},
trend50d: benchmarkTrends[index].trend50d,
trend200d: benchmarkTrends[index].trend200d
};
});
@ -118,14 +165,24 @@ export class BenchmarkService {
return benchmarks;
}
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
public async getBenchmarkAssetProfiles({
enableSharing = false
} = {}): Promise<Partial<SymbolProfile>[]> {
const symbolProfileIds: string[] = (
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? []
).map(({ symbolProfileId }) => {
return symbolProfileId;
});
)
.filter((benchmark) => {
if (enableSharing) {
return benchmark.enableSharing;
}
return true;
})
.map(({ symbolProfileId }) => {
return symbolProfileId;
});
const assetProfiles =
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);
@ -245,6 +302,43 @@ export class BenchmarkService {
};
}
public async deleteBenchmark({
dataSource,
symbol
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: {
dataSource,
symbol
}
});
if (!assetProfile) {
return null;
}
let benchmarks =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => {
return symbolProfileId !== assetProfile.id;
});
await this.propertyService.put({
key: PROPERTY_BENCHMARKS,
value: JSON.stringify(benchmarks)
});
return {
dataSource,
symbol,
id: assetProfile.id,
name: assetProfile.name
};
}
private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
}

View File

@ -7,6 +7,7 @@ import {
UseGuards
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ExchangeRateService } from './exchange-rate.service';
@ -23,7 +24,7 @@ export class ExchangeRateController {
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString);
const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({
date,

View File

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

View File

@ -1,11 +1,15 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ExportService {
public constructor(private readonly prismaService: PrismaService) {}
public constructor(
private readonly accountService: AccountService,
private readonly orderService: OrderService
) {}
public async export({
activityIds,
@ -14,36 +18,30 @@ export class ExportService {
activityIds?: string[];
userId: string;
}): Promise<Export> {
const accounts = await this.prismaService.account.findMany({
orderBy: {
name: 'asc'
},
select: {
accountType: true,
balance: true,
comment: true,
currency: true,
id: true,
isExcluded: true,
name: true,
platformId: true
},
where: { userId }
});
const accounts = (
await this.accountService.accounts({
orderBy: {
name: 'asc'
},
where: { userId }
})
).map(
({ balance, comment, currency, id, isExcluded, name, platformId }) => {
return {
balance,
comment,
currency,
id,
isExcluded,
name,
platformId
};
}
);
let activities = await this.prismaService.order.findMany({
let activities = await this.orderService.orders({
include: { SymbolProfile: true },
orderBy: { date: 'desc' },
select: {
accountId: true,
comment: true,
date: true,
fee: true,
id: true,
quantity: true,
SymbolProfile: true,
type: true,
unitPrice: true
},
where: { userId }
});
@ -79,7 +77,13 @@ export class ExportService {
currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource,
date: date.toISOString(),
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
symbol:
type === 'FEE' ||
type === 'INTEREST' ||
type === 'ITEM' ||
type === 'LIABILITY'
? SymbolProfile.name
: SymbolProfile.symbol
};
}
)

View File

@ -1,237 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { format } from 'date-fns';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = '';
public indexHtmlEn = '';
public indexHtmlEs = '';
public indexHtmlFr = '';
public indexHtmlIt = '';
public indexHtmlNl = '';
public indexHtmlPt = '';
private static readonly DEFAULT_DESCRIPTION =
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
public constructor(
private readonly configurationService: ConfigurationService
) {
try {
this.indexHtmlDe = fs.readFileSync(
this.getPathOfIndexHtmlFile('de'),
'utf8'
);
this.indexHtmlEn = fs.readFileSync(
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8'
);
this.indexHtmlEs = fs.readFileSync(
this.getPathOfIndexHtmlFile('es'),
'utf8'
);
this.indexHtmlFr = fs.readFileSync(
this.getPathOfIndexHtmlFile('fr'),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
this.indexHtmlNl = fs.readFileSync(
this.getPathOfIndexHtmlFile('nl'),
'utf8'
);
this.indexHtmlPt = fs.readFileSync(
this.getPathOfIndexHtmlFile('pt'),
'utf8'
);
} catch {}
}
public use(request: Request, response: Response, next: NextFunction) {
const currentDate = format(new Date(), DATE_FORMAT);
let featureGraphicPath = 'assets/cover.png';
let title = 'Ghostfolio Open Source Wealth Management Software';
if (request.path.startsWith('/en/blog/2022/08/500-stars-on-github')) {
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
title = `500 Stars - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/10/hacktoberfest-2022')) {
featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png';
title = `Hacktoberfest 2022 - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/11/black-friday-2022')) {
featureGraphicPath = 'assets/images/blog/black-friday-2022.jpg';
title = `Black Friday 2022 - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances'
)
) {
featureGraphicPath = 'assets/images/blog/20221226.jpg';
title = `The importance of tracking your personal finances - ${title}`;
} else if (
request.path.startsWith(
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt'
)
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png';
title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`;
} else if (
request.path.startsWith('/en/blog/2023/02/ghostfolio-meets-umbrel')
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
title = `Ghostfolio meets Umbrel - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
)
) {
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
title = `Ghostfolio reaches 1000 Stars on GitHub - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio'
)
) {
featureGraphicPath = 'assets/images/blog/20230520.jpg';
title = `Unlock your Financial Potential with Ghostfolio - ${title}`;
} else if (
request.path.startsWith('/en/blog/2023/07/exploring-the-path-to-fire')
) {
featureGraphicPath = 'assets/images/blog/20230701.jpg';
title = `Exploring the Path to FIRE - ${title}`;
}
if (
request.path.startsWith('/api/') ||
this.isFileRequest(request.url) ||
!environment.production
) {
// Skip
next();
} else if (request.path === '/de' || request.path.startsWith('/de/')) {
response.send(
this.interpolate(this.indexHtmlDe, {
currentDate,
featureGraphicPath,
title,
description:
'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
languageCode: 'de',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/es' || request.path.startsWith('/es/')) {
response.send(
this.interpolate(this.indexHtmlEs, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
languageCode: 'es',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/fr' || request.path.startsWith('/fr/')) {
response.send(
this.interpolate(this.indexHtmlFr, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
languageCode: 'fr',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/it' || request.path.startsWith('/it/')) {
response.send(
this.interpolate(this.indexHtmlIt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
languageCode: 'it',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/nl' || request.path.startsWith('/nl/')) {
response.send(
this.interpolate(this.indexHtmlNl, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETFs of cryptocurrencies over meerdere platforms bij te houden.',
languageCode: 'nl',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/pt' || request.path.startsWith('/pt/')) {
response.send(
this.interpolate(this.indexHtmlPt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
languageCode: 'pt',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
response.send(
this.interpolate(this.indexHtmlEn, {
currentDate,
featureGraphicPath,
title,
description: FrontendMiddleware.DEFAULT_DESCRIPTION,
languageCode: DEFAULT_LANGUAGE_CODE,
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
}
}
private getPathOfIndexHtmlFile(aLocale: string) {
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
}
private interpolate(template: string, context: any) {
return template.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
const properties = objectPath.split('.');
return properties.reduce(
(previous, current) => previous?.[current],
context
);
});
}
private isFileRequest(filename: string) {
if (filename === '/assets/LICENSE') {
return true;
} else if (filename.includes('auth/ey')) {
return false;
}
return filename.split('.').pop() !== filename;
}
}

View File

@ -18,6 +18,19 @@ export class HealthController {
@Get()
public async getHealth() {}
@Get('data-enhancer/:name')
public async getHealthOfDataEnhancer(@Param('name') name: string) {
const hasResponse =
await this.healthService.hasResponseFromDataEnhancer(name);
if (hasResponse !== true) {
throw new HttpException(
getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE),
StatusCodes.SERVICE_UNAVAILABLE
);
}
}
@Get('data-provider/:dataSource')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getHealthOfDataProvider(
@ -30,9 +43,8 @@ export class HealthController {
);
}
const hasResponse = await this.healthService.hasResponseFromDataProvider(
dataSource
);
const hasResponse =
await this.healthService.hasResponseFromDataProvider(dataSource);
if (hasResponse !== true) {
throw new HttpException(

View File

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

View File

@ -1,3 +1,4 @@
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@ -5,9 +6,14 @@ import { DataSource } from '@prisma/client';
@Injectable()
export class HealthService {
public constructor(
private readonly dataEnhancerService: DataEnhancerService,
private readonly dataProviderService: DataProviderService
) {}
public async hasResponseFromDataEnhancer(aName: string) {
return this.dataEnhancerService.enhance(aName);
}
public async hasResponseFromDataProvider(aDataSource: DataSource) {
return this.dataProviderService.checkQuote(aDataSource);
}

View File

@ -8,10 +8,16 @@ import {
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { parseDate } from '@ghostfolio/common/helper';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
parseDate
} from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import {
AccountWithPlatform,
@ -20,13 +26,16 @@ import {
import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import Big from 'big.js';
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns';
import { uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class ImportService {
public constructor(
private readonly accountService: AccountService,
private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly orderService: OrderService,
@ -74,11 +83,13 @@ export class ImportService {
const value = new Big(quantity).mul(marketPrice).toNumber();
const date = parseDate(dateString);
const isDuplicate = orders.some((activity) => {
return (
activity.accountId === Account?.id &&
activity.SymbolProfile.currency === assetProfile.currency &&
activity.SymbolProfile.dataSource === assetProfile.dataSource &&
isSameDay(activity.date, parseDate(dateString)) &&
isSameSecond(activity.date, date) &&
activity.quantity === quantity &&
activity.SymbolProfile.symbol === assetProfile.symbol &&
activity.type === 'DIVIDEND' &&
@ -92,6 +103,7 @@ export class ImportService {
return {
Account,
date,
error,
quantity,
value,
@ -99,7 +111,6 @@ export class ImportService {
accountUserId: undefined,
comment: undefined,
createdAt: undefined,
date: parseDate(dateString),
fee: 0,
feeInBaseCurrency: 0,
id: assetProfile.id,
@ -220,8 +231,7 @@ export class ImportService {
const assetProfiles = await this.validateActivities({
activitiesDto,
maxActivitiesToImport,
userId
maxActivitiesToImport
});
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
@ -243,17 +253,50 @@ export class ImportService {
const activities: Activity[] = [];
for (const {
accountId,
comment,
date,
error,
fee,
quantity,
SymbolProfile: assetProfile,
type,
unitPrice
} of activitiesExtendedWithErrors) {
for (let [
index,
{
accountId,
comment,
date,
error,
fee,
quantity,
SymbolProfile,
type,
unitPrice
}
] of activitiesExtendedWithErrors.entries()) {
const assetProfile = assetProfiles[
getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource,
symbol: SymbolProfile.symbol
})
] ?? {
currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource,
symbol: SymbolProfile.symbol
};
const {
assetClass,
assetSubClass,
countries,
createdAt,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
id,
isin,
name,
scraperConfiguration,
sectors,
symbol,
symbolMapping,
url,
updatedAt
} = assetProfile;
const validatedAccount = accounts.find(({ id }) => {
return id === accountId;
});
@ -264,6 +307,35 @@ export class ImportService {
Account?: { id: string; name: string };
});
if (SymbolProfile.currency !== assetProfile.currency) {
// Convert the unit price and fee to the asset currency if the imported
// activity is in a different currency
unitPrice = await this.exchangeRateDataService.toCurrencyAtDate(
unitPrice,
SymbolProfile.currency,
assetProfile.currency,
date
);
if (!unitPrice) {
throw new Error(
`activities.${index} historical exchange rate at ${format(
date,
DATE_FORMAT
)} is not available from "${SymbolProfile.currency}" to "${
assetProfile.currency
}"`
);
}
fee = await this.exchangeRateDataService.toCurrencyAtDate(
fee,
SymbolProfile.currency,
assetProfile.currency,
date
);
}
if (isDryRun) {
order = {
comment,
@ -279,23 +351,25 @@ export class ImportService {
id: uuidv4(),
isDraft: isAfter(date, endOfToday()),
SymbolProfile: {
assetClass: assetProfile.assetClass,
assetSubClass: assetProfile.assetSubClass,
comment: assetProfile.comment,
countries: assetProfile.countries,
createdAt: assetProfile.createdAt,
currency: assetProfile.currency,
dataSource: assetProfile.dataSource,
id: assetProfile.id,
isin: assetProfile.isin,
name: assetProfile.name,
scraperConfiguration: assetProfile.scraperConfiguration,
sectors: assetProfile.sectors,
symbol: assetProfile.currency,
symbolMapping: assetProfile.symbolMapping,
updatedAt: assetProfile.updatedAt,
url: assetProfile.url,
...assetProfiles[assetProfile.symbol]
assetClass,
assetSubClass,
countries,
createdAt,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
id,
isin,
name,
scraperConfiguration,
sectors,
symbol,
symbolMapping,
updatedAt,
url,
comment: assetProfile.comment
},
Account: validatedAccount,
symbolProfileId: undefined,
@ -318,14 +392,14 @@ export class ImportService {
SymbolProfile: {
connectOrCreate: {
create: {
currency: assetProfile.currency,
dataSource: assetProfile.dataSource,
symbol: assetProfile.symbol
currency,
dataSource,
symbol
},
where: {
dataSource_symbol: {
dataSource: assetProfile.dataSource,
symbol: assetProfile.symbol
dataSource,
symbol
}
}
}
@ -337,24 +411,49 @@ export class ImportService {
const value = new Big(quantity).mul(unitPrice).toNumber();
//@ts-ignore
activities.push({
...order,
error,
value,
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee,
assetProfile.currency,
currency,
userCurrency
),
// @ts-ignore
SymbolProfile: assetProfile,
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
assetProfile.currency,
currency,
userCurrency
)
});
}
activities.sort((activity1, activity2) => {
return Number(activity1.date) - Number(activity2.date);
});
if (!isDryRun) {
// Gather symbol data in the background, if not dry run
const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => {
return getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource,
symbol: SymbolProfile.symbol
});
});
this.dataGatheringService.gatherSymbols(
uniqueActivities.map(({ date, SymbolProfile }) => {
return {
date,
dataSource: SymbolProfile.dataSource,
symbol: SymbolProfile.symbol
};
})
);
}
return activities;
}
@ -384,12 +483,13 @@ export class ImportService {
type,
unitPrice
}) => {
const date = parseISO(<string>(<unknown>dateString));
const date = parseISO(dateString);
const isDuplicate = existingActivities.some((activity) => {
return (
activity.accountId === accountId &&
activity.SymbolProfile.currency === currency &&
activity.SymbolProfile.dataSource === dataSource &&
isSameDay(activity.date, date) &&
isSameSecond(activity.date, date) &&
activity.fee === fee &&
activity.quantity === quantity &&
activity.SymbolProfile.symbol === symbol &&
@ -420,6 +520,9 @@ export class ImportService {
comment: null,
countries: null,
createdAt: undefined,
figi: null,
figiComposite: null,
figiShareClass: null,
id: undefined,
isin: null,
name: null,
@ -446,25 +549,36 @@ export class ImportService {
private async validateActivities({
activitiesDto,
maxActivitiesToImport,
userId
maxActivitiesToImport
}: {
activitiesDto: Partial<CreateOrderDto>[];
maxActivitiesToImport: number;
userId: string;
}) {
if (activitiesDto?.length > maxActivitiesToImport) {
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
}
const assetProfiles: {
[symbol: string]: Partial<SymbolProfile>;
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
} = {};
const uniqueActivitiesDto = uniqBy(
activitiesDto,
({ dataSource, symbol }) => {
return getAssetProfileIdentifier({ dataSource, symbol });
}
);
for (const [
index,
{ currency, dataSource, symbol }
] of activitiesDto.entries()) {
] of uniqueActivitiesDto.entries()) {
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
if (dataSource !== 'MANUAL') {
const assetProfile = (
await this.dataProviderService.getAssetProfiles([
@ -472,19 +586,26 @@ export class ImportService {
])
)?.[symbol];
if (assetProfile === undefined) {
if (!assetProfile?.name) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
);
}
if (assetProfile.currency !== currency) {
if (
assetProfile.currency !== currency &&
!this.exchangeRateDataService.hasCurrencyPair(
currency,
assetProfile.currency
)
) {
throw new Error(
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}"`
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
);
}
assetProfiles[symbol] = assetProfile;
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
assetProfile;
}
}

View File

@ -1,6 +1,7 @@
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -28,11 +29,11 @@ import { InfoService } from './info.service';
signOptions: { expiresIn: '30 days' }
}),
PlatformModule,
PrismaModule,
PropertyModule,
RedisCacheModule,
SymbolProfileModule,
TagModule
TagModule,
UserModule
],
providers: [InfoService]
})

View File

@ -1,19 +1,20 @@
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT,
PROPERTY_BETTER_UPTIME_MONITOR_ID,
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
PROPERTY_DEMO_USER_ID,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SLACK_COMMUNITY_USERS,
PROPERTY_STRIPE_CONFIG,
PROPERTY_SYSTEM_MESSAGE,
ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config';
import {
@ -30,9 +31,9 @@ import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOffer } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bent from 'bent';
import * as cheerio from 'cheerio';
import { format, subDays } from 'date-fns';
import got from 'got';
@Injectable()
export class InfoService {
@ -44,23 +45,18 @@ export class InfoService {
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly jwtService: JwtService,
private readonly platformService: PlatformService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService,
private readonly tagService: TagService
private readonly tagService: TagService,
private readonly userService: UserService
) {}
public async get(): Promise<InfoItem> {
const info: Partial<InfoItem> = {};
let isReadOnlyMode: boolean;
const platforms = (
await this.platformService.getPlatforms({
orderBy: { name: 'asc' }
})
).map(({ id, name }) => {
return { id, name };
const platforms = await this.platformService.getPlatforms({
orderBy: { name: 'asc' }
});
let systemMessage: string;
const globalPermissions: string[] = [];
@ -106,10 +102,6 @@ export class InfoService {
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {
globalPermissions.push(permissions.enableSystemMessage);
systemMessage = (await this.propertyService.getByKey(
PROPERTY_SYSTEM_MESSAGE
)) as string;
}
const isUserSignupEnabled =
@ -137,20 +129,14 @@ export class InfoService {
platforms,
statistics,
subscriptions,
systemMessage,
tags,
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies()
};
}
private async countActiveUsers(aDays: number) {
return await this.prismaService.user.count({
orderBy: {
Analytics: {
updatedAt: 'desc'
}
},
return this.userService.count({
where: {
AND: [
{
@ -172,20 +158,24 @@ export class InfoService {
private async countDockerHubPulls(): Promise<number> {
try {
const get = bent(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{
'User-Agent': 'request'
}
);
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { pull_count } = await got(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
{
headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: abortController.signal
}
).json<any>();
const { pull_count } = await get();
return pull_count;
} catch (error) {
Logger.error(error, 'InfoService');
Logger.error(error, 'InfoService - DockerHub');
return undefined;
}
@ -193,16 +183,18 @@ export class InfoService {
private async countGitHubContributors(): Promise<number> {
try {
const get = bent(
'https://github.com/ghostfolio/ghostfolio',
'GET',
'string',
200,
{}
);
const abortController = new AbortController();
const html = await get();
const $ = cheerio.load(html);
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { body } = await got('https://github.com/ghostfolio/ghostfolio', {
// @ts-ignore
signal: abortController.signal
});
const $ = cheerio.load(body);
return extractNumberFromString(
$(
@ -210,7 +202,7 @@ export class InfoService {
).text()
);
} catch (error) {
Logger.error(error, 'InfoService');
Logger.error(error, 'InfoService - GitHub');
return undefined;
}
@ -218,30 +210,31 @@ export class InfoService {
private async countGitHubStargazers(): Promise<number> {
try {
const get = bent(
`https://api.github.com/repos/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{
'User-Agent': 'request'
}
);
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { stargazers_count } = await got(
`https://api.github.com/repos/ghostfolio/ghostfolio`,
{
headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: abortController.signal
}
).json<any>();
const { stargazers_count } = await get();
return stargazers_count;
} catch (error) {
Logger.error(error, 'InfoService');
Logger.error(error, 'InfoService - GitHub');
return undefined;
}
}
private async countNewUsers(aDays: number) {
return await this.prismaService.user.count({
orderBy: {
createdAt: 'desc'
},
return this.userService.count({
where: {
AND: [
{
@ -332,11 +325,10 @@ export class InfoService {
return undefined;
}
const stripeConfig = (await this.prismaService.property.findUnique({
where: { key: PROPERTY_STRIPE_CONFIG }
})) ?? { value: '{}' };
return JSON.parse(stripeConfig.value);
return (
((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
{}
);
}
private async getUptime(): Promise<number> {
@ -346,25 +338,31 @@ export class InfoService {
PROPERTY_BETTER_UPTIME_MONITOR_ID
)) as string;
const get = bent(
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { data } = await got(
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90),
DATE_FORMAT
)}&to${format(new Date(), DATE_FORMAT)}`,
'GET',
'json',
200,
{
Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY'
)}`
headers: {
Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY'
)}`
},
// @ts-ignore
signal: abortController.signal
}
);
).json<any>();
const { data } = await get();
return data.attributes.availability / 100;
} catch (error) {
Logger.error(error, 'InfoService');
Logger.error(error, 'InfoService - Better Stack');
return undefined;
}

View File

@ -1,8 +1,9 @@
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import * as bent from 'bent';
import got from 'got';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable()
@ -41,15 +42,19 @@ export class LogoService {
}
private getBuffer(aUrl: string) {
const get = bent(
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
return got(
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
'GET',
'buffer',
200,
{
'User-Agent': 'request'
headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: abortController.signal
}
);
return get();
).buffer();
}
}

View File

@ -13,7 +13,8 @@ import {
IsISO8601,
IsNumber,
IsOptional,
IsString
IsString,
Min
} from 'class-validator';
import { isString } from 'lodash';
@ -48,9 +49,11 @@ export class CreateOrderDto {
date: string;
@IsNumber()
@Min(0)
fee: number;
@IsNumber()
@Min(0)
quantity: number;
@IsString()
@ -64,6 +67,7 @@ export class CreateOrderDto {
type: Type;
@IsNumber()
@Min(0)
unitPrice: number;
@IsBoolean()

View File

@ -2,6 +2,7 @@ import { OrderWithAccount } from '@ghostfolio/common/types';
export interface Activities {
activities: Activity[];
count: number;
}
export interface Activity extends OrderWithAccount {

View File

@ -2,6 +2,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
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 { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -23,7 +24,7 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Order as OrderModel } from '@prisma/client';
import { Order as OrderModel, Prisma } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -36,6 +37,7 @@ import { UpdateOrderDto } from './update-order.dto';
export class OrderController {
public constructor(
private readonly apiService: ApiService,
private readonly dataGatheringService: DataGatheringService,
private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService,
@Inject(REQUEST) private readonly request: RequestWithUser
@ -87,7 +89,11 @@ export class OrderController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
@Query('skip') skip?: number,
@Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
@Query('tags') filterByTags?: string,
@Query('take') take?: number
): Promise<Activities> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
@ -99,15 +105,19 @@ export class OrderController {
await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const activities = await this.orderService.getOrders({
const { activities, count } = await this.orderService.getOrders({
filters,
sortColumn,
sortDirection,
userCurrency,
includeDrafts: true,
skip: isNaN(skip) ? undefined : skip,
take: isNaN(take) ? undefined : take,
userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true
});
return { activities };
return { activities, count };
}
@Post()
@ -123,7 +133,7 @@ export class OrderController {
);
}
return this.orderService.createOrder({
const order = await this.orderService.createOrder({
...data,
date: parseISO(data.date),
SymbolProfile: {
@ -144,6 +154,20 @@ export class OrderController {
User: { connect: { id: this.request.user.id } },
userId: this.request.user.id
});
if (data.dataSource && !order.isDraft) {
// Gather symbol data in the background, if data source is set
// (not MANUAL) and not draft
this.dataGatheringService.gatherSymbols([
{
dataSource: data.dataSource,
date: order.date,
symbol: data.symbol
}
]);
}
return order;
}
@Put(':id')

View File

@ -1,3 +1,4 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
@ -31,6 +32,6 @@ import { OrderService } from './order.service';
SymbolProfileModule,
UserModule
],
providers: [AccountService, OrderService]
providers: [AccountBalanceService, AccountService, OrderService]
})
export class OrderModule {}

View File

@ -7,6 +7,7 @@ import {
GATHER_ASSET_PROFILE_PROCESS,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
@ -24,7 +25,7 @@ import { endOfToday, isAfter } from 'date-fns';
import { groupBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { Activity } from './interfaces/activities.interface';
import { Activities, Activity } from './interfaces/activities.interface';
@Injectable()
export class OrderService {
@ -50,7 +51,7 @@ export class OrderService {
take?: number;
cursor?: Prisma.OrderWhereUniqueInput;
where?: Prisma.OrderWhereInput;
orderBy?: Prisma.OrderOrderByWithRelationInput;
orderBy?: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput>;
}): Promise<OrderWithAccount[]> {
const { include, skip, take, cursor, where, orderBy } = params;
@ -96,7 +97,12 @@ export class OrderService {
const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId;
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
if (
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
const assetClass = data.assetClass;
const assetSubClass = data.assetSubClass;
currency = data.SymbolProfile.connectOrCreate.create.currency;
@ -117,32 +123,21 @@ export class OrderService {
};
}
await this.dataGatheringService.addJobToQueue({
data: {
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
},
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}`
}
});
const isDraft =
data.type === 'LIABILITY'
? false
: isAfter(data.date as Date, endOfToday());
if (!isDraft) {
// Gather symbol data of order in the background, if not draft
this.dataGatheringService.gatherSymbols([
{
if (data.SymbolProfile.connectOrCreate.create.dataSource !== 'MANUAL') {
this.dataGatheringService.addJobToQueue({
data: {
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
date: <Date>data.date,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
},
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: getAssetProfileIdentifier({
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
})
}
]);
});
}
delete data.accountId;
@ -162,6 +157,14 @@ export class OrderService {
const orderData: Prisma.OrderCreateInput = data;
const isDraft =
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
? false
: isAfter(data.date as Date, endOfToday());
const order = await this.prismaService.order.create({
data: {
...orderData,
@ -204,7 +207,12 @@ export class OrderService {
where
});
if (order.type === 'ITEM' || order.type === 'LIABILITY') {
if (
order.type === 'FEE' ||
order.type === 'INTEREST' ||
order.type === 'ITEM' ||
order.type === 'LIABILITY'
) {
await this.symbolProfileService.deleteById(order.symbolProfileId);
}
@ -222,6 +230,10 @@ export class OrderService {
public async getOrders({
filters,
includeDrafts = false,
skip,
sortColumn,
sortDirection,
take = Number.MAX_SAFE_INTEGER,
types,
userCurrency,
userId,
@ -229,11 +241,18 @@ export class OrderService {
}: {
filters?: Filter[];
includeDrafts?: boolean;
skip?: number;
sortColumn?: string;
sortDirection?: Prisma.SortOrder;
take?: number;
types?: TypeOfOrder[];
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<Activity[]> {
}): Promise<Activities> {
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
{ date: 'asc' }
];
const where: Prisma.OrderWhereInput = { userId };
const {
@ -295,6 +314,10 @@ export class OrderService {
};
}
if (sortColumn) {
orderBy = [{ [sortColumn]: sortDirection }];
}
if (types) {
where.OR = types.map((type) => {
return {
@ -305,8 +328,11 @@ export class OrderService {
});
}
return (
await this.orders({
const [orders, count] = await Promise.all([
this.orders({
orderBy,
skip,
take,
where,
include: {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -318,10 +344,12 @@ export class OrderService {
// eslint-disable-next-line @typescript-eslint/naming-convention
SymbolProfile: true,
tags: true
},
orderBy: { date: 'asc' }
})
)
}
}),
this.prismaService.order.count({ where })
]);
const activities = orders
.filter((order) => {
return (
withExcludedAccounts ||
@ -347,6 +375,8 @@ export class OrderService {
)
};
});
return { activities, count };
}
public async updateOrder({
@ -375,7 +405,12 @@ export class OrderService {
let isDraft = false;
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
if (
data.type === 'FEE' ||
data.type === 'INTEREST' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
delete data.SymbolProfile.connect;
} else {
delete data.SymbolProfile.update;

View File

@ -8,12 +8,12 @@ import {
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEnum,
IsISO8601,
IsNumber,
IsOptional,
IsString
IsString,
Min
} from 'class-validator';
import { isString } from 'lodash';
@ -47,12 +47,14 @@ export class UpdateOrderDto {
date: string;
@IsNumber()
@Min(0)
fee: number;
@IsString()
id: string;
@IsNumber()
@Min(0)
quantity: number;
@IsString()
@ -66,5 +68,6 @@ export class UpdateOrderDto {
type: Type;
@IsNumber()
@Min(0)
unitPrice: number;
}

View File

@ -47,6 +47,7 @@ export class PlatformController {
StatusCodes.FORBIDDEN
);
}
return this.platformService.createPlatform(data);
}

View File

@ -6,6 +6,18 @@ import { Platform, Prisma } from '@prisma/client';
export class PlatformService {
public constructor(private readonly prismaService: PrismaService) {}
public async createPlatform(data: Prisma.PlatformCreateInput) {
return this.prismaService.platform.create({
data
});
}
public async deletePlatform(
where: Prisma.PlatformWhereUniqueInput
): Promise<Platform> {
return this.prismaService.platform.delete({ where });
}
public async getPlatform(
platformWhereUniqueInput: Prisma.PlatformWhereUniqueInput
): Promise<Platform> {
@ -56,12 +68,6 @@ export class PlatformService {
});
}
public async createPlatform(data: Prisma.PlatformCreateInput) {
return this.prismaService.platform.create({
data
});
}
public async updatePlatform({
data,
where
@ -74,10 +80,4 @@ export class PlatformService {
where
});
}
public async deletePlatform(
where: Prisma.PlatformWhereUniqueInput
): Promise<Platform> {
return this.prismaService.platform.delete({ where });
}
}

View File

@ -61,6 +61,7 @@ export const CurrentRateServiceMock = {
for (const dataGatheringItem of dataGatheringItems) {
values.push({
date,
dataSource: dataGatheringItem.dataSource,
marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol,
date
@ -74,6 +75,7 @@ export const CurrentRateServiceMock = {
for (const dataGatheringItem of dataGatheringItems) {
values.push({
date,
dataSource: dataGatheringItem.dataSource,
marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol,
date

View File

@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service';
@ -25,30 +26,30 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
getRange: ({
dateRangeEnd,
dateRangeStart,
symbols
uniqueAssets
}: {
dateRangeEnd: Date;
dateRangeStart: Date;
symbols: string[];
uniqueAssets: UniqueAsset[];
}) => {
return Promise.resolve<MarketData[]>([
{
createdAt: dateRangeStart,
dataSource: DataSource.YAHOO,
dataSource: uniqueAssets[0].dataSource,
date: dateRangeStart,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902,
state: 'CLOSE',
symbol: symbols[0]
symbol: uniqueAssets[0].symbol
},
{
createdAt: dateRangeEnd,
dataSource: DataSource.YAHOO,
dataSource: uniqueAssets[0].dataSource,
date: dateRangeEnd,
id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966,
state: 'CLOSE',
symbol: symbols[0]
symbol: uniqueAssets[0].symbol
}
]);
}
@ -105,7 +106,6 @@ describe('CurrentRateService', () => {
null,
null,
null,
null,
null
);
marketDataService = new MarketDataService(null);
@ -135,6 +135,7 @@ describe('CurrentRateService', () => {
errors: [],
values: [
{
dataSource: 'YAHOO',
date: undefined,
marketPriceInBaseCurrency: 1841.823902,
symbol: 'AMZN'

View File

@ -2,7 +2,11 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper';
import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces';
import {
DataProviderInfo,
ResponseError,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns';
import { flatten, isEmpty, uniqBy } from 'lodash';
@ -38,7 +42,7 @@ export class CurrentRateService {
if (includeToday) {
promises.push(
this.dataProviderService
.getQuotes(dataGatheringItems)
.getQuotes({ items: dataGatheringItems })
.then((dataResultProvider) => {
const result: GetValueObject[] = [];
for (const dataGatheringItem of dataGatheringItems) {
@ -52,6 +56,7 @@ export class CurrentRateService {
if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
result.push({
dataSource: dataGatheringItem.dataSource,
date: today,
marketPriceInBaseCurrency:
this.exchangeRateDataService.toCurrency(
@ -75,27 +80,30 @@ export class CurrentRateService {
);
}
const symbols = dataGatheringItems.map((dataGatheringItem) => {
return dataGatheringItem.symbol;
});
const uniqueAssets: UniqueAsset[] = dataGatheringItems.map(
({ dataSource, symbol }) => {
return { dataSource, symbol };
}
);
promises.push(
this.marketDataService
.getRange({
dateQuery,
symbols
uniqueAssets
})
.then((data) => {
return data.map((marketDataItem) => {
return data.map(({ dataSource, date, marketPrice, symbol }) => {
return {
date: marketDataItem.date,
dataSource,
date,
symbol,
marketPriceInBaseCurrency:
this.exchangeRateDataService.toCurrency(
marketDataItem.marketPrice,
currencies[marketDataItem.symbol],
marketPrice,
currencies[symbol],
userCurrency
),
symbol: marketDataItem.symbol
)
};
});
})
@ -112,7 +120,7 @@ export class CurrentRateService {
};
if (!isEmpty(quoteErrors)) {
for (const { symbol } of quoteErrors) {
for (const { dataSource, symbol } of quoteErrors) {
try {
// If missing quote, fallback to the latest available historical market price
let value: GetValueObject = response.values.find((currentValue) => {
@ -121,6 +129,7 @@ export class CurrentRateService {
if (!value) {
value = {
dataSource,
symbol,
date: today,
marketPriceInBaseCurrency: 0

View File

@ -1,5 +1,6 @@
export interface GetValueObject {
import { UniqueAsset } from '@ghostfolio/common/interfaces';
export interface GetValueObject extends UniqueAsset {
date: Date;
marketPriceInBaseCurrency: number;
symbol: string;
}

View File

@ -1,4 +1,4 @@
import { DataSource, Type as TypeOfOrder } from '@prisma/client';
import { DataSource, Tag, Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js';
export interface PortfolioOrder {
@ -9,6 +9,7 @@ export interface PortfolioOrder {
name: string;
quantity: Big;
symbol: string;
tags?: Tag[];
type: TypeOfOrder;
unitPrice: Big;
}

View File

@ -1,4 +1,4 @@
import { DataSource } from '@prisma/client';
import { DataSource, Tag } from '@prisma/client';
import Big from 'big.js';
export interface TransactionPointSymbol {
@ -9,5 +9,6 @@ export interface TransactionPointSymbol {
investment: Big;
quantity: Big;
symbol: string;
tags?: Tag[];
transactionCount: number;
}

View File

@ -114,6 +114,7 @@ export class PortfolioCalculator {
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
quantity: newQuantity,
symbol: order.symbol,
tags: order.tags,
transactionCount: oldAccumulatedSymbol.transactionCount + 1
};
} else {
@ -125,6 +126,7 @@ export class PortfolioCalculator {
investment: unitPrice.mul(order.quantity).mul(factor),
quantity: order.quantity.mul(factor),
symbol: order.symbol,
tags: order.tags,
transactionCount: 1
};
}
@ -492,6 +494,7 @@ export class PortfolioCalculator {
: null,
quantity: item.quantity,
symbol: item.symbol,
tags: item.tags,
transactionCount: item.transactionCount
});
@ -781,7 +784,7 @@ export class PortfolioCalculator {
);
} else if (!currentPosition.quantity.eq(0)) {
Logger.warn(
`Missing historical market data for symbol ${currentPosition.symbol}`,
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
'PortfolioCalculator'
);
hasErrors = true;

View File

@ -10,7 +10,10 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import {
DEFAULT_CURRENCY,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import {
PortfolioDetails,
PortfolioDividends,
@ -47,8 +50,6 @@ import { PortfolioService } from './portfolio.service';
@Controller('portfolio')
export class PortfolioController {
private baseCurrency: string;
public constructor(
private readonly accessService: AccessService,
private readonly apiService: ApiService,
@ -57,9 +58,7 @@ export class PortfolioController {
private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
}
) {}
@Get('details')
@UseGuards(AuthGuard('jwt'))
@ -134,7 +133,7 @@ export class PortfolioController {
portfolioPosition.netPerformance = null;
portfolioPosition.quantity = null;
portfolioPosition.valueInPercentage =
portfolioPosition.value / totalValue;
portfolioPosition.valueInBaseCurrency / totalValue;
}
for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {
@ -161,10 +160,12 @@ export class PortfolioController {
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'fireWealth',
'items',
'liabilities',
'netWorth',
'totalBuy',
'totalInvestment',
'totalSell'
]);
}
@ -172,11 +173,20 @@ export class PortfolioController {
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
holdings[symbol] = {
...portfolioPosition,
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
assetClass:
hasDetails || portfolioPosition.assetClass === 'CASH'
? portfolioPosition.assetClass
: undefined,
assetSubClass:
hasDetails || portfolioPosition.assetSubClass === 'CASH'
? portfolioPosition.assetSubClass
: undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
markets: hasDetails ? portfolioPosition.markets : undefined,
marketsAdvanced: hasDetails
? portfolioPosition.marketsAdvanced
: undefined,
sectors: hasDetails ? portfolioPosition.sectors : []
};
}
@ -313,7 +323,8 @@ export class PortfolioController {
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false
): Promise<PortfolioPerformanceResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
@ -325,6 +336,7 @@ export class PortfolioController {
dateRange,
filters,
impersonationId,
withExcludedAccounts,
userId: this.request.user.id
});
@ -334,16 +346,34 @@ export class PortfolioController {
this.userService.isRestrictedView(this.request.user)
) {
performanceInformation.chart = performanceInformation.chart.map(
({ date, netPerformanceInPercentage, totalInvestment, value }) => {
({
date,
netPerformanceInPercentage,
netWorth,
totalInvestment,
value
}) => {
return {
date,
netPerformanceInPercentage,
totalInvestment: new Big(totalInvestment)
.div(performanceInformation.performance.totalInvestment)
.toNumber(),
valueInPercentage: new Big(value)
.div(performanceInformation.performance.currentValue)
.toNumber()
netWorthInPercentage:
performanceInformation.performance.currentNetWorth === 0
? 0
: new Big(netWorth)
.div(performanceInformation.performance.currentNetWorth)
.toNumber(),
totalInvestment:
performanceInformation.performance.totalInvestment === 0
? 0
: new Big(totalInvestment)
.div(performanceInformation.performance.totalInvestment)
.toNumber(),
valueInPercentage:
performanceInformation.performance.currentValue === 0
? 0
: new Big(value)
.div(performanceInformation.performance.currentValue)
.toNumber()
};
}
);
@ -353,6 +383,7 @@ export class PortfolioController {
[
'currentGrossPerformance',
'currentNetPerformance',
'currentNetWorth',
'currentValue',
'totalInvestment'
]
@ -381,12 +412,14 @@ export class PortfolioController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('query') filterBySearchQuery?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioPositions> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterBySearchQuery,
filterByTags
});
@ -437,15 +470,15 @@ export class PortfolioController {
return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency,
this.request.user?.Settings?.settings.baseCurrency ??
this.baseCurrency
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY
);
})
.reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPublicDetails.holdings[symbol] = {
allocationInPercentage: portfolioPosition.value / totalValue,
allocationInPercentage:
portfolioPosition.valueInBaseCurrency / totalValue,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
dataSource: portfolioPosition.dataSource,
@ -456,7 +489,7 @@ export class PortfolioController {
sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol,
url: portfolioPosition.url,
valueInPercentage: portfolioPosition.value / totalValue
valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue
};
}

View File

@ -1,4 +1,5 @@
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
@ -36,6 +37,7 @@ import { RulesService } from './rules.service';
UserModule
],
providers: [
AccountBalanceService,
AccountService,
CurrentRateService,
PortfolioService,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
import { Cache } from 'cache-manager';
import type { RedisStore } from './redis-store.interface';
export interface RedisCache extends Cache {
store: RedisStore;
}

View File

@ -0,0 +1,8 @@
import { Store } from 'cache-manager';
import { createClient } from 'redis';
export interface RedisStore extends Store {
getClient: () => ReturnType<typeof createClient>;
isCacheableValue: (value: any) => boolean;
name: 'redis';
}

View File

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

View File

@ -1,21 +1,30 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable, Logger } from '@nestjs/common';
import type { RedisCache } from './interfaces/redis-cache.interface';
@Injectable()
export class RedisCacheService {
public constructor(
@Inject(CACHE_MANAGER) private readonly cache: Cache,
@Inject(CACHE_MANAGER) private readonly cache: RedisCache,
private readonly configurationService: ConfigurationService
) {}
) {
const client = cache.store.getClient();
client.on('error', (error) => {
Logger.error(error, 'RedisCacheService');
});
}
public async get(key: string): Promise<string> {
return await this.cache.get(key);
}
public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
return `quote-${dataSource}-${symbol}`;
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
}
public async remove(key: string) {
@ -27,8 +36,10 @@ export class RedisCacheService {
}
public async set(key: string, value: string, ttlInSeconds?: number) {
await this.cache.set(key, value, {
ttl: ttlInSeconds ?? this.configurationService.get('CACHE_TTL')
});
await this.cache.set(
key,
value,
ttlInSeconds ?? this.configurationService.get('CACHE_TTL')
);
}
}

View File

@ -0,0 +1,36 @@
import * as fs from 'fs';
import * as path from 'path';
import {
DATE_FORMAT,
getYesterday,
interpolate
} from '@ghostfolio/common/helper';
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
import { format } from 'date-fns';
import { Response } from 'express';
@Controller('sitemap.xml')
export class SitemapController {
public sitemapXml = '';
public constructor() {
try {
this.sitemapXml = fs.readFileSync(
path.join(__dirname, 'assets', 'sitemap.xml'),
'utf8'
);
} catch {}
}
@Get()
@Version(VERSION_NEUTRAL)
public async flushCache(@Res() response: Response): Promise<void> {
response.setHeader('content-type', 'application/xml');
response.send(
interpolate(this.sitemapXml, {
currentDate: format(getYesterday(), DATE_FORMAT)
})
);
}
}

View File

@ -0,0 +1,24 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
import { SitemapController } from './sitemap.controller';
@Module({
controllers: [SitemapController],
imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule
]
})
export class SitemapModule {}

View File

@ -104,7 +104,7 @@ export class SubscriptionController {
response.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/account`
)}/${DEFAULT_LANGUAGE_CODE}/account/membership`
);
}

View File

@ -93,9 +93,8 @@ export class SubscriptionService {
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
try {
const session = await this.stripe.checkout.sessions.retrieve(
aCheckoutSessionId
);
const session =
await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId);
await this.createSubscription({
price: session.amount_total / 100,
@ -112,14 +111,14 @@ export class SubscriptionService {
aSubscriptions: Subscription[]
): UserWithSettings['subscription'] {
if (aSubscriptions.length > 0) {
const latestSubscription = aSubscriptions.reduce((a, b) => {
const { expiresAt, price } = aSubscriptions.reduce((a, b) => {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
});
return {
expiresAt: latestSubscription.expiresAt,
offer: latestSubscription.price === 0 ? 'default' : 'renewal',
type: isBefore(new Date(), latestSubscription.expiresAt)
expiresAt,
offer: price ? 'renewal' : 'default',
type: isBefore(new Date(), expiresAt)
? SubscriptionType.Premium
: SubscriptionType.Basic
};

View File

@ -15,6 +15,7 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isDate, isEmpty } from 'lodash';
@ -93,7 +94,7 @@ export class SymbolController {
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString);
const date = parseISO(dateString);
if (!isDate(date)) {
throw new HttpException(

View File

@ -27,9 +27,9 @@ export class SymbolService {
dataGatheringItem: IDataGatheringItem;
includeHistoricalData?: number;
}): Promise<SymbolItem> {
const quotes = await this.dataProviderService.getQuotes([
dataGatheringItem
]);
const quotes = await this.dataProviderService.getQuotes({
items: [dataGatheringItem]
});
const { currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {};
if (dataGatheringItem.dataSource && marketPrice >= 0) {
@ -40,7 +40,12 @@ export class SymbolService {
const marketData = await this.marketDataService.getRange({
dateQuery: { gte: subDays(new Date(), days) },
symbols: [dataGatheringItem.symbol]
uniqueAssets: [
{
dataSource: dataGatheringItem.dataSource,
symbol: dataGatheringItem.symbol
}
]
});
historicalData = marketData.map(({ date, marketPrice: value }) => {

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class CreateTagDto {
@IsString()
name: string;
}

View File

@ -0,0 +1,104 @@
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
Delete,
Get,
HttpException,
Inject,
Param,
Post,
Put,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Tag } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateTagDto } from './create-tag.dto';
import { TagService } from './tag.service';
import { UpdateTagDto } from './update-tag.dto';
@Controller('tag')
export class TagController {
public constructor(
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly tagService: TagService
) {}
@Get()
@UseGuards(AuthGuard('jwt'))
public async getTags() {
return this.tagService.getTagsWithActivityCount();
}
@Post()
@UseGuards(AuthGuard('jwt'))
public async createTag(@Body() data: CreateTagDto): Promise<Tag> {
if (!hasPermission(this.request.user.permissions, permissions.createTag)) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.tagService.createTag(data);
}
@Put(':id')
@UseGuards(AuthGuard('jwt'))
public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) {
if (!hasPermission(this.request.user.permissions, permissions.updateTag)) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const originalTag = await this.tagService.getTag({
id
});
if (!originalTag) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.tagService.updateTag({
data: {
...data
},
where: {
id
}
});
}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
public async deleteTag(@Param('id') id: string) {
if (!hasPermission(this.request.user.permissions, permissions.deleteTag)) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const originalTag = await this.tagService.getTag({
id
});
if (!originalTag) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.tagService.deleteTag({ id });
}
}

View File

@ -0,0 +1,13 @@
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { TagController } from './tag.controller';
import { TagService } from './tag.service';
@Module({
controllers: [TagController],
exports: [TagService],
imports: [PrismaModule],
providers: [TagService]
})
export class TagModule {}

View File

@ -0,0 +1,79 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { Prisma, Tag } from '@prisma/client';
@Injectable()
export class TagService {
public constructor(private readonly prismaService: PrismaService) {}
public async createTag(data: Prisma.TagCreateInput) {
return this.prismaService.tag.create({
data
});
}
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> {
return this.prismaService.tag.delete({ where });
}
public async getTag(
tagWhereUniqueInput: Prisma.TagWhereUniqueInput
): Promise<Tag> {
return this.prismaService.tag.findUnique({
where: tagWhereUniqueInput
});
}
public async getTags({
cursor,
orderBy,
skip,
take,
where
}: {
cursor?: Prisma.TagWhereUniqueInput;
orderBy?: Prisma.TagOrderByWithRelationInput;
skip?: number;
take?: number;
where?: Prisma.TagWhereInput;
} = {}) {
return this.prismaService.tag.findMany({
cursor,
orderBy,
skip,
take,
where
});
}
public async getTagsWithActivityCount() {
const tagsWithOrderCount = await this.prismaService.tag.findMany({
include: {
_count: {
select: { orders: true }
}
}
});
return tagsWithOrderCount.map(({ _count, id, name }) => {
return {
id,
name,
activityCount: _count.orders
};
});
}
public async updateTag({
data,
where
}: {
data: Prisma.TagUpdateInput;
where: Prisma.TagWhereUniqueInput;
}): Promise<Tag> {
return this.prismaService.tag.update({
data,
where
});
}
}

View File

@ -0,0 +1,9 @@
import { IsString } from 'class-validator';
export class UpdateTagDto {
@IsString()
id: string;
@IsString()
name: string;
}

View File

@ -4,8 +4,17 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
import {
DEFAULT_CURRENCY,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SYSTEM_MESSAGE,
locale
} from '@ghostfolio/common/config';
import {
User as IUser,
SystemMessage,
UserSettings
} from '@ghostfolio/common/interfaces';
import {
getPermissions,
hasRole,
@ -14,24 +23,23 @@ import {
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { Prisma, Role, User } from '@prisma/client';
import { sortBy } from 'lodash';
import { differenceInDays } from 'date-fns';
import { sortBy, without } from 'lodash';
const crypto = require('crypto');
@Injectable()
export class UserService {
public static DEFAULT_CURRENCY = 'USD';
private baseCurrency: string;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService,
private readonly tagService: TagService
) {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
) {}
public async count(args?: Prisma.UserCountArgs) {
return this.prismaService.user.count(args);
}
public async getUser(
@ -45,6 +53,17 @@ export class UserService {
orderBy: { alias: 'asc' },
where: { GranteeUser: { id } }
});
let systemMessage: SystemMessage;
const systemMessageProperty = (await this.propertyService.getByKey(
PROPERTY_SYSTEM_MESSAGE
)) as SystemMessage;
if (systemMessageProperty?.targetGroups?.includes(subscription?.type)) {
systemMessage = systemMessageProperty;
}
let tags = await this.tagService.getByUser(id);
if (
@ -58,6 +77,7 @@ export class UserService {
id,
permissions,
subscription,
systemMessage,
tags,
access: access.map((accessItem) => {
return {
@ -107,7 +127,9 @@ export class UserService {
updatedAt
} = await this.prismaService.user.findUnique({
include: {
Account: true,
Account: {
include: { Platform: true }
},
Analytics: true,
Settings: true,
Subscription: true
@ -123,7 +145,7 @@ export class UserService {
id,
provider,
role,
Settings,
Settings: Settings as UserWithSettings['Settings'],
thirdPartyId,
updatedAt,
activityCount: Analytics?.activityCount
@ -144,8 +166,7 @@ export class UserService {
// Set default value for base currency
if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
(user.Settings.settings as UserSettings).baseCurrency =
UserService.DEFAULT_CURRENCY;
(user.Settings.settings as UserSettings).baseCurrency = DEFAULT_CURRENCY;
}
// Set default value for date range
@ -161,15 +182,47 @@ export class UserService {
let currentPermissions = getPermissions(user.role);
if (!(user.Settings.settings as UserSettings).isExperimentalFeatures) {
// currentPermissions = without(
// currentPermissions,
// permissions.xyz
// );
}
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
user.subscription =
this.subscriptionService.getSubscription(Subscription);
if (
Analytics?.activityCount % 10 === 0 &&
user.subscription?.type === 'Basic'
) {
currentPermissions.push(permissions.enableSubscriptionInterstitial);
if (user.subscription?.type === 'Basic') {
const daysSinceRegistration = differenceInDays(
new Date(),
user.createdAt
);
let frequency = 15;
if (daysSinceRegistration > 365) {
frequency = 2;
} else if (daysSinceRegistration > 180) {
frequency = 3;
} else if (daysSinceRegistration > 60) {
frequency = 5;
} else if (daysSinceRegistration > 30) {
frequency = 8;
} else if (daysSinceRegistration > 15) {
frequency = 12;
}
if (Analytics?.activityCount % frequency === 1) {
currentPermissions.push(permissions.enableSubscriptionInterstitial);
}
currentPermissions = without(
currentPermissions,
permissions.createAccess
);
// Reset benchmark
user.Settings.settings.benchmark = undefined;
}
if (user.subscription?.type === 'Premium') {
@ -201,8 +254,8 @@ export class UserService {
currentPermissions.push(permissions.impersonateAllUsers);
}
user.Account = sortBy(user.Account, (account) => {
return account.name;
user.Account = sortBy(user.Account, ({ name }) => {
return name.toLowerCase();
});
user.permissions = currentPermissions.sort();
@ -247,7 +300,7 @@ export class UserService {
...data,
Account: {
create: {
currency: this.baseCurrency,
currency: DEFAULT_CURRENCY,
isDefault: true,
name: 'Default Account'
}
@ -255,7 +308,7 @@ export class UserService {
Settings: {
create: {
settings: {
currency: this.baseCurrency
currency: DEFAULT_CURRENCY
}
}
}

View File

@ -0,0 +1 @@
["AU", "HK", "NZ", "SG"]

View File

@ -0,0 +1,19 @@
[
"AT",
"BE",
"CH",
"DE",
"DK",
"ES",
"FI",
"FR",
"GB",
"IE",
"IL",
"IT",
"LU",
"NL",
"NO",
"PT",
"SE"
]

View File

@ -51,7 +51,9 @@
"3FT": "ThreeFold Token",
"3ULL": "3ULL Coin",
"3XD": "3DChain",
"420CHAN": "420chan",
"4ART": "4ART Coin",
"4CHAN": "4Chan",
"4JNET": "4JNET",
"77G": "GraphenTech",
"7E": "7ELEVEN",
@ -60,6 +62,7 @@
"8BT": "8 Circuit Studios",
"8PAY": "8Pay",
"8X8": "8X8 Protocol",
"9GAG": "9GAG",
"A5T": "Alpha5",
"AAA": "Moon Rabbit",
"AAB": "AAX Token",
@ -101,6 +104,7 @@
"ACN": "AvonCoin",
"ACOIN": "ACoin",
"ACP": "Anarchists Prime",
"ACQ": "Acquire.Fi",
"ACS": "Access Protocol",
"ACT": "Achain",
"ACTIN": "Actinium",
@ -180,7 +184,7 @@
"AGX": "Agricoin",
"AHOO": "Ahoolee",
"AHT": "AhaToken",
"AI": "Multiverse",
"AI": "AiDoge",
"AIB": "AdvancedInternetBlock",
"AIBB": "AiBB",
"AIBK": "AIB Utility Token",
@ -213,6 +217,7 @@
"AKA": "Akroma",
"AKITA": "Akita Inu",
"AKN": "Akoin",
"AKNC": "Aave KNC v1",
"AKRO": "Akropolis",
"AKT": "Akash Network",
"AKTIO": "AKTIO Coin",
@ -237,12 +242,14 @@
"ALIC": "AliCoin",
"ALICE": "My Neighbor Alice",
"ALIEN": "AlienCoin",
"ALINK": "Aave LINK v1",
"ALIS": "ALISmedia",
"ALITA": "Alita Network",
"ALIX": "AlinX",
"ALKI": "Alkimi",
"ALLBI": "ALL BEST ICO",
"ALLEY": "NFT Alley",
"ALLIN": "All in",
"ALN": "Aluna",
"ALOHA": "Aloha",
"ALP": "Alphacon",
@ -410,12 +417,14 @@
"ARIX": "Arix",
"ARK": "ARK",
"ARKER": "Arker",
"ARKM": "Arkham",
"ARKN": "Ark Rivals",
"ARM": "Armory Coin",
"ARMOR": "ARMOR",
"ARMR": "ARMR",
"ARMS": "2Acoin",
"ARNA": "ARNA Panacea",
"ARNM": "Arenum",
"ARNO": "ARNO",
"ARNX": "Aeron",
"ARNXM": "Armor NXM",
@ -472,6 +481,7 @@
"ASTO": "Altered State Token",
"ASTON": "Aston",
"ASTR": "Astar",
"ASTRAFER": "Astrafer",
"ASTRAL": "Astral",
"ASTRO": "AstroSwap",
"ASTROC": "Astroport Classic",
@ -531,6 +541,7 @@
"AURY": "Aurory",
"AUSCM": "Auric Network",
"AUSD": "Appeal dollar",
"AUSDC": "Aave USDC v1",
"AUT": "Autoria",
"AUTHORSHIP": "Authorship",
"AUTO": "Auto",
@ -612,6 +623,7 @@
"BACK": "DollarBack",
"BACOIN": "BACoin",
"BACON": "BaconDAO (BACON)",
"BAD": "Bad Idea AI",
"BADGER": "Badger DAO",
"BAG": "BondAppetit",
"BAGS": "Basis Gold Share",
@ -662,6 +674,7 @@
"BBCT": "TraDove B2BCoin",
"BBDT": "BBD Token",
"BBF": "Bubblefong",
"BBFT": "Block Busters Tech Token",
"BBG": "BigBang",
"BBGC": "BigBang Game",
"BBI": "BelugaPay",
@ -725,6 +738,7 @@
"BDX": "Beldex",
"BDY": "Buddy DAO",
"BEACH": "BeachCoin",
"BEAI": "BeNFT Solutions",
"BEAM": "Beam",
"BEAN": "BeanCash",
"BEAST": "CryptoBeast",
@ -806,6 +820,7 @@
"BIDR": "Binance IDR Stable Coin",
"BIFI": "Beefy.Finance",
"BIFIF": "BiFi",
"BIG": "Big Eyes",
"BIGHAN": "BighanCoin",
"BIGSB": "BigShortBets",
"BIGUP": "BigUp",
@ -1090,6 +1105,7 @@
"BRNK": "Brank",
"BRNX": "Bronix",
"BRO": "Bitradio",
"BROCK": "Bitrock",
"BRONZ": "BitBronze",
"BRT": "Bikerush",
"BRTR": "Barter",
@ -1226,7 +1242,7 @@
"BULL": "Bullieverse",
"BULLC": "BuySell",
"BULLION": "BullionFX",
"BULLS": "BullshitCoin",
"BULLS": "Bull Coin",
"BULLSH": "Bullshit Inu",
"BUMN": "BUMooN",
"BUMP": "Bumper",
@ -1277,6 +1293,7 @@
"BZKY": "Bizkey",
"BZL": "BZLCoin",
"BZNT": "Bezant",
"BZR": "Bazaars",
"BZRX": "bZx Protocol",
"BZX": "Bitcoin Zero",
"BZZ": "Swarmv",
@ -1319,8 +1336,10 @@
"CAP": "BottleCaps",
"CAPD": "Capdax",
"CAPP": "Cappasity",
"CAPRICOIN": "CapriCoin",
"CAPS": "Ternoa",
"CAPT": "Bitcoin Captain",
"CAPTAINPLANET": "Captain Planet",
"CAR": "CarBlock",
"CARAT": "Carats Token",
"CARBON": "Carboncoin",
@ -1478,6 +1497,7 @@
"CHECKR": "CheckerChain",
"CHECOIN": "CheCoin",
"CHEDDA": "Chedda",
"CHEEL": "Cheelee",
"CHEESE": "CHEESE",
"CHEESUS": "Cheesus",
"CHEQ": "CHEQD Network",
@ -1520,7 +1540,8 @@
"CHX": "Own",
"CHY": "Concern Poverty Chain",
"CHZ": "Chiliz",
"CIC": "CIChain",
"CIC": "Crazy Internet Coin",
"CICHAIN": "CIChain",
"CIF": "Crypto Improvement Fund",
"CIM": "COINCOME",
"CIN": "CinderCoin",
@ -1630,7 +1651,6 @@
"COB": "Cobinhood",
"COC": "Coin of the champions",
"COCK": "Shibacock",
"COCOS": "COCOS BCX",
"CODEO": "Codeo Token",
"CODEX": "CODEX Finance",
"CODI": "Codi Finance",
@ -1659,7 +1679,7 @@
"COLX": "ColossusCoinXT",
"COM": "Coliseum",
"COMB": "Combo",
"COMBO": "Furucombo",
"COMBO": "COMBO",
"COMFI": "CompliFi",
"COMM": "Community Coin",
"COMMUNITYCOIN": "Community Coin",
@ -1672,7 +1692,6 @@
"CONI": "CoinBene",
"CONS": "ConSpiracy Coin",
"CONSENTIUM": "Consentium",
"CONT": "Contentos",
"CONUN": "CONUN",
"CONV": "Convergence",
"COOK": "Cook",
@ -1683,17 +1702,19 @@
"COPS": "Cops Finance",
"COR": "Corion",
"CORAL": "CoralPay",
"CORE": "Coreum",
"CORE": "Core",
"COREDAO": "coreDAO",
"COREG": "Core Group Asset",
"COREUM": "Coreum",
"CORGI": "Corgi Inu",
"CORN": "CORN",
"CORX": "CorionX",
"COS": "COS",
"COS": "Contentos",
"COSHI": "CoShi Inu",
"COSM": "CosmoChain",
"COSMIC": "CosmicSwap",
"COSP": "Cosplay Token",
"COSS": "COS",
"COSX": "Cosmecoin",
"COT": "CoTrader",
"COTI": "COTI",
@ -1729,7 +1750,7 @@
"CPOOL": "Clearpool",
"CPROP": "CPROP",
"CPRX": "Crypto Perx",
"CPS": "CapriCoin",
"CPS": "Cryptostone",
"CPT": "Cryptaur",
"CPU": "CPUcoin",
"CPX": "Apex Token",
@ -1796,6 +1817,7 @@
"CRTS": "Cratos",
"CRU": "Crust Network",
"CRV": "Curve DAO Token",
"CRVUSD": "crvUSD",
"CRW": "Crown Coin",
"CRWD": "CRWD Network",
"CRWNY": "Crowny Token",
@ -1843,7 +1865,7 @@
"CTLX": "Cash Telex",
"CTN": "Continuum Finance",
"CTO": "Crypto",
"CTP": "Captain Planet",
"CTP": "Ctomorrow Platform",
"CTPL": "Cultiplan",
"CTPT": "Contents Protocol",
"CTR": "Creator Platform",
@ -2007,6 +2029,7 @@
"DBC": "DeepBrain Chain",
"DBCCOIN": "Datablockchain",
"DBD": "Day By Day",
"DBEAR": "DBear Coin",
"DBET": "Decent.bet",
"DBIC": "DubaiCoin",
"DBIX": "DubaiCoin",
@ -2058,6 +2081,7 @@
"DEEP": "DeepCloud AI",
"DEEPG": "Deep Gold",
"DEEX": "DEEX",
"DEEZ": "DEEZ NUTS",
"DEFI": "Defi",
"DEFI5": "DEFI Top 5 Tokens Index",
"DEFIL": "DeFIL",
@ -2162,11 +2186,12 @@
"DIEM": "Facebook Diem",
"DIESEL": "Diesel",
"DIFX": "Digital Financial Exchange",
"DIG": "Dignity",
"DIG": "DIEGO",
"DIGG": "DIGG",
"DIGIC": "DigiCube",
"DIGIF": "DigiFel",
"DIGITAL": "Digital Reserve Currency",
"DIGNITY": "Dignity",
"DIGS": "Diggits",
"DIKO": "Arkadiko",
"DILI": "D Community",
@ -2246,6 +2271,7 @@
"DOGBOSS": "Dog Boss",
"DOGDEFI": "DogDeFiCoin",
"DOGE": "Dogecoin",
"DOGE20": "Doge 2.0",
"DOGEBNB": "DogeBNB",
"DOGEC": "DogeCash",
"DOGECEO": "Doge CEO",
@ -2539,7 +2565,7 @@
"ELONGT": "Elon GOAT",
"ELONONE": "AstroElon",
"ELP": "Ellerium",
"ELS": "Elysium",
"ELS": "Ethlas",
"ELT": "Element Black",
"ELTC2": "eLTC",
"ELTCOIN": "ELTCOIN",
@ -2548,6 +2574,7 @@
"ELVN": "11Minutes",
"ELX": "Energy Ledger",
"ELY": "Elysian",
"ELYSIUM": "Elysium",
"EM": "Eminer",
"EMANATE": "EMANATE",
"EMAR": "EmaratCoin",
@ -2559,6 +2586,7 @@
"EMC2": "Einsteinium",
"EMD": "Emerald",
"EMIGR": "EmiratesGoldCoin",
"EML": "EML Protocol",
"EMN.CUR": "Eastman Chemical",
"EMON": "Ethermon",
"EMOT": "Sentigraph.io",
@ -2692,6 +2720,7 @@
"ETHD": "Ethereum Dark",
"ETHER": "Etherparty",
"ETHERDELTA": "EtherDelta",
"ETHERKING": "Ether Kingdoms Token",
"ETHERNITY": "Ethernity Chain",
"ETHF": "EthereumFair",
"ETHIX": "EthicHub",
@ -2709,6 +2738,7 @@
"ETHSHIB": "Eth Shiba",
"ETHV": "Ethverse",
"ETHW": "Ethereum PoW",
"ETHX": "Stader ETHx",
"ETHY": "Ethereum Yield",
"ETI": "EtherInc",
"ETK": "Energi Token",
@ -2722,7 +2752,7 @@
"ETR": "Electric Token",
"ETRNT": "Eternal Trusts",
"ETS": "ETH Share",
"ETSC": "Ether star blockchain",
"ETSC": "Ether star blockchain",
"ETT": "EncryptoTel",
"ETY": "Ethereum Cloud",
"ETZ": "EtherZero",
@ -2773,6 +2803,7 @@
"EXB": "ExaByte (EXB)",
"EXC": "Eximchain",
"EXCC": "ExchangeCoin",
"EXCHANGEN": "ExchangeN",
"EXCL": "Exclusive Coin",
"EXE": "ExeCoin",
"EXFI": "Flare Finance",
@ -2781,7 +2812,7 @@
"EXLT": "ExtraLovers",
"EXM": "EXMO Coin",
"EXMR": "EXMR FDN",
"EXN": "ExchangeN",
"EXN": "Exeno",
"EXO": "Exosis",
"EXP": "Expanse",
"EXRD": "Radix",
@ -2814,6 +2845,7 @@
"FAIR": "FairCoin",
"FAIRC": "Faireum Token",
"FAIRG": "FairGame",
"FAKE": "FAKE COIN",
"FAKT": "Medifakt",
"FALCONS": "Falcon Swaps",
"FAME": "Fame MMA",
@ -2860,6 +2892,7 @@
"FDO": "Firdaos",
"FDR": "French Digital Reserve",
"FDT": "Frutti Dino",
"FDUSD": "First Digital USD",
"FDX": "fidentiaX",
"FDZ": "Friendz",
"FEAR": "Fear",
@ -2870,6 +2903,7 @@
"FEN": "First Ever NFT",
"FENOMY": "Fenomy",
"FER": "Ferro",
"FERC": "FairERC20",
"FERMA": "Ferma",
"FESS": "Fesschain",
"FET": "Fetch.AI",
@ -2931,7 +2965,7 @@
"FLASH": "Flashstake",
"FLASHC": "FLASH coin",
"FLC": "FlowChainCoin",
"FLD": "FLUID",
"FLD": "FluidAI",
"FLDC": "Folding Coin",
"FLDT": "FairyLand",
"FLETA": "FLETA",
@ -3091,6 +3125,7 @@
"FUEL": "Jetfuel Finance",
"FUJIN": "Fujinto",
"FUKU": "Furukuru",
"FUMO": "Alien Milady Fumo",
"FUN": "FUN Token",
"FUNC": "FunCoin",
"FUND": "Unification",
@ -3101,6 +3136,7 @@
"FUNDZ": "FundFantasy",
"FUNK": "Cypherfunks Coin",
"FUR": "Furio",
"FURU": "Furucombo",
"FURY": "Engines of Fury",
"FUS": "Fus",
"FUSE": "Fuse Network Token",
@ -3118,6 +3154,7 @@
"FXP": "FXPay",
"FXS": "Frax Share",
"FXT": "FuzeX",
"FXY": "Floxypay",
"FYN": "Affyn",
"FYP": "FlypMe",
"FYZ": "Fyooz",
@ -3172,6 +3209,7 @@
"GAT": "GATCOIN",
"GATE": "GATENet",
"GATEWAY": "Gateway Protocol",
"GAYPEPE": "Gay Pepe",
"GAZE": "GazeTV",
"GB": "GoldBlocks",
"GBA": "Geeba",
@ -3222,6 +3260,7 @@
"GEMZ": "Gemz Social",
"GEN": "DAOstack",
"GENE": "Genopets",
"GENIE": "The Genie",
"GENIX": "Genix",
"GENS": "Genshiro",
"GENSTAKE": "Genstake",
@ -3261,6 +3300,7 @@
"GHCOLD": "Galaxy Heroes Coin",
"GHD": "Giftedhands",
"GHNY": "Grizzly Honey",
"GHO": "GHO",
"GHOST": "GhostbyMcAfee",
"GHOSTCOIN": "GhostCoin",
"GHOSTM": "GhostMarket",
@ -3274,6 +3314,7 @@
"GIFT": "GiftNet",
"GIG": "GigaCoin",
"GIGA": "GigaSwap",
"GIGX": "GigXCoin",
"GIM": "Gimli",
"GIMMER": "Gimmer",
"GIN": "GINcoin",
@ -3385,6 +3426,7 @@
"GOVT": "The Government Network",
"GOZ": "Göztepe S.K. Fan Token",
"GP": "Wizards And Dragons",
"GPBP": "Genius Playboy Billionaire Philanthropist",
"GPKR": "Gold Poker",
"GPL": "Gold Pressed Latinum",
"GPPT": "Pluto Project Coin",
@ -3501,7 +3543,8 @@
"HALF": "0.5X Long Bitcoin Token",
"HALFSHIT": "0.5X Long Shitcoin Index Token",
"HALLO": "Halloween Coin",
"HALO": "Halo Platform",
"HALO": "Halo Coin",
"HALOPLATFORM": "Halo Platform",
"HAM": "Hamster",
"HAMS": "HamsterCoin",
"HANA": "Hanacoin",
@ -3598,6 +3641,7 @@
"HILL": "President Clinton",
"HINA": "Hina Inu",
"HINT": "Hintchain",
"HIPPO": "HIPPO",
"HIRE": "HireMatch",
"HIT": "HitChain",
"HITBTC": "HitBTC Token",
@ -3634,6 +3678,7 @@
"HNTR": "Hunter",
"HNY": "Honey",
"HNZO": "Hanzo Inu",
"HOBO": "HOBO THE BEAR",
"HOD": "HoDooi.com",
"HODL": "HOdlcoin",
"HOGE": "Hoge Finance",
@ -3839,7 +3884,7 @@
"IMPCN": "Brain Space",
"IMPER": "Impermax",
"IMPS": "Impulse Coin",
"IMPT": "Ether Kingdoms Token",
"IMPT": "IMPT",
"IMPULSE": "IMPULSE by FDR",
"IMS": "Independent Money System",
"IMST": "Imsmart",
@ -4001,6 +4046,7 @@
"JAM": "Tune.Fm",
"JANE": "JaneCoin",
"JAR": "Jarvis+",
"JARED": "Jared From Subway",
"JASMY": "JasmyCoin",
"JBS": "JumBucks Coin",
"JBX": "Juicebox",
@ -4163,9 +4209,10 @@
"KIN": "Kin",
"KIND": "Kind Ads",
"KINE": "Kine Protocol",
"KING": "King Finance",
"KING": "KING",
"KING93": "King93",
"KINGDOMQUEST": "Kingdom Quest",
"KINGF": "King Finance",
"KINGSHIB": "King Shiba",
"KINGSWAP": "KingSwap",
"KINT": "Kintsugi",
@ -4175,6 +4222,7 @@
"KISC": "Kaiser",
"KISHIMOTO": "Kishimoto Inu",
"KISHU": "Kishu Inu",
"KITA": "KITA INU",
"KITSU": "Kitsune Inu",
"KITTY": "Kitty Inu",
"KKO": "Kineko",
@ -4267,10 +4315,12 @@
"KUBO": "KUBO",
"KUBOS": "KubosCoin",
"KUE": "Kuende",
"KUJI": "Kujira",
"KUMA": "Kuma Inu",
"KUNCI": "Kunci Coin",
"KUR": "Kuro",
"KURT": "Kurrent",
"KUSA": "Kusa Inu",
"KUSD": "Kowala",
"KUSH": "KushCoin",
"KUV": "Kuverit",
@ -4280,6 +4330,7 @@
"KVT": "Kinesis Velocity Token",
"KWATT": "4New",
"KWD": "KIWI DEFI",
"KWENTA": "Kwenta",
"KWH": "KWHCoin",
"KWIK": "KwikSwap",
"KWS": "Knight War Spirits",
@ -4299,7 +4350,9 @@
"LABX": "Stakinglab",
"LACCOIN": "LocalAgro",
"LACE": "Lovelace World",
"LADYS": "Milady Meme Coin",
"LAEEB": "LaEeb",
"LAELAPS": "Laelaps",
"LAIKA": "Laika Protocol",
"LALA": "LaLa World",
"LAMB": "Lambda",
@ -4455,13 +4508,14 @@
"LLAND": "Lyfe Land",
"LLG": "Loligo",
"LLION": "Lydian Lion",
"LM": "LM Token",
"LM": "LeisureMeta",
"LMAO": "LMAO Finance",
"LMC": "LomoCoin",
"LMCH": "Latamcash",
"LMCSWAP": "LimoCoin SWAP",
"LMR": "Lumerin",
"LMT": "Lympo Market Token",
"LMTOKEN": "LM Token",
"LMXC": "LimonX",
"LMY": "Lunch Money",
"LN": "LINK",
@ -4530,6 +4584,7 @@
"LRG": "Largo Coin",
"LRN": "Loopring [NEO]",
"LSD": "LightSpeedCoin",
"LSETH": "Liquid Staked ETH",
"LSK": "Lisk",
"LSP": "Lumenswap",
"LSS": "Lossless",
@ -4626,6 +4681,7 @@
"MAEP": "Maester Protocol",
"MAG": "Magnet",
"MAGIC": "Magic",
"MAGICF": "MagicFox",
"MAHA": "MahaDAO",
"MAI": "Mindsync",
"MAID": "MaidSafe Coin",
@ -4639,6 +4695,7 @@
"MANDOX": "MandoX",
"MANGA": "Manga Token",
"MANNA": "Manna",
"MANTLE": "Mantle",
"MAP": "MAP Protocol",
"MAPC": "MapCoin",
"MAPE": "Mecha Morphing",
@ -4672,6 +4729,7 @@
"MATIC": "Polygon",
"MATPAD": "MaticPad",
"MATTER": "AntiMatter",
"MAV": "Maverick Protocol",
"MAX": "MaxCoin",
"MAXR": "Max Revive",
"MAY": "Theresa May Coin",
@ -4776,6 +4834,7 @@
"MESA": "MetaVisa",
"MESG": "MESG",
"MESH": "MeshBox",
"MESSI": "MESSI COIN",
"MET": "Metronome",
"META": "Metadium",
"METAC": "Metacoin",
@ -4881,6 +4940,7 @@
"MIODIO": "MIODIOCOIN",
"MIOTA": "IOTA",
"MIR": "Mirror Protocol",
"MIRACLE": "MIRACLE",
"MIRC": "MIR COIN",
"MIS": "Mithril Share",
"MISA": "Sangkara",
@ -4938,7 +4998,6 @@
"MNRB": "MoneyRebel",
"MNS": "Monnos",
"MNST": "MoonStarter",
"MNT": "microNFT",
"MNTC": "Manet Coin",
"MNTG": "Monetas",
"MNTL": "AssetMantle",
@ -4967,6 +5026,7 @@
"MOF": "Molecular Future (TRC20)",
"MOFI": "MobiFi",
"MOFOLD": "Molecular Future (ERC20)",
"MOG": "Mog Coin",
"MOGU": "Mogu",
"MOGX": "Mogu",
"MOI": "MyOwnItem",
@ -4989,9 +5049,11 @@
"MONEYIMT": "MoneyToken",
"MONF": "Monfter",
"MONG": "MongCoin",
"MONG20": "Mongoose 2.0",
"MONI": "Monsta Infinite",
"MONK": "Monkey Project",
"MONKEY": "Monkey",
"MONKEYS": "Monkeys Token",
"MONO": "MonoX",
"MONONOKEINU": "Mononoke Inu",
"MONS": "Monsters Clan",
@ -5011,11 +5073,13 @@
"MOONSHOT": "Moonshot",
"MOOO": "Hashtagger",
"MOOV": "dotmoovs",
"MOOX": "Moox Protocol",
"MOPS": "Mops",
"MORA": "Meliora",
"MORE": "More Coin",
"MOS": "MOS Coin",
"MOT": "Olympus Labs",
"MOTG": "MetaOctagon",
"MOTI": "Motion",
"MOTO": "Motocoin",
"MOV": "MovieCoin",
@ -5076,6 +5140,7 @@
"MSWAP": "MoneySwap",
"MT": "MyToken",
"MTA": "Meta",
"MTB": "MetaBridge",
"MTBC": "Metabolic",
"MTC": "MEDICAL TOKEN CURRENCY",
"MTCMN": "MTC Mesh",
@ -5108,6 +5173,7 @@
"MUE": "MonetaryUnit",
"MULTI": "Multichain",
"MULTIBOT": "Multibot",
"MULTIV": "Multiverse",
"MUN": "MUNcoin",
"MUNCH": "Munch Token",
"MUSD": "mStable USD",
@ -5648,6 +5714,7 @@
"OZP": "OZAPHYRE",
"P202": "Project 202",
"P2PS": "P2P Solutions Foundation",
"PAAL": "PAAL AI",
"PAC": "PAC Protocol",
"PACOCA": "Pacoca",
"PAD": "NearPad",
@ -5736,6 +5803,7 @@
"PEARL": "Pearl Finance",
"PEC": "PeaceCoin",
"PEEL": "Meta Apes",
"PEEPA": "Peepa",
"PEEPS": "The Peoples Coin",
"PEG": "PegNet",
"PEGS": "PegShares",
@ -5748,6 +5816,7 @@
"PEOPLE": "ConstitutionDAO",
"PEOS": "pEOS",
"PEPE": "Pepe",
"PEPE20": "Pepe 2.0",
"PEPECASH": "Pepe Cash",
"PEPPER": "Pepper Token",
"PEPS": "PEPS Coin",
@ -5822,6 +5891,7 @@
"PINK": "PinkCoin",
"PINKX": "PantherCoin",
"PINMO": "Pinmo",
"PINO": "Pinocchu",
"PINU": "Piccolo Inu",
"PIO": "Pioneershares",
"PIPI": "Pippi Finance",
@ -5885,6 +5955,7 @@
"PLS": "Pulsechain",
"PLSD": "PulseDogecoin",
"PLSPAD": "PulsePad",
"PLSX": "PulseX",
"PLT": "Poollotto.finance",
"PLTC": "PlatonCoin",
"PLTX": "PlutusX",
@ -5911,7 +5982,6 @@
"PNK": "Kleros",
"PNL": "True PNL",
"PNODE": "Pinknode",
"PNP": "LogisticsX",
"PNT": "pNetwork Token",
"PNX": "PhantomX",
"PNY": "Peony Coin",
@ -5927,6 +5997,7 @@
"POINTS": "Cryptsy Points",
"POK": "Pokmonsters",
"POKEM": "Pokemonio",
"POKEMON": "Pokemon",
"POKER": "PokerCoin",
"POKT": "Pocket Network",
"POL": "Pool-X",
@ -6010,6 +6081,7 @@
"PRIME": "Echelon Prime",
"PRIMECHAIN": "PrimeChain",
"PRINT": "Printer.Finance",
"PRINTERIUM": "Printerium",
"PRINTS": "FingerprintsDAO",
"PRISM": "Prism",
"PRIX": "Privatix",
@ -6033,7 +6105,7 @@
"PROTON": "Proton",
"PROUD": "PROUD Money",
"PROXI": "PROXI",
"PRP": "Papyrus",
"PRP": "Pepe Prime",
"PRPS": "Purpose",
"PRPT": "Purple Token",
"PRQ": "PARSIQ",
@ -6042,7 +6114,7 @@
"PRTG": "Pre-Retogeum",
"PRV": "PrivacySwap",
"PRVS": "Previse",
"PRX": "Printerium",
"PRX": "Parex",
"PRXY": "Proxy",
"PRY": "PRIMARY",
"PSB": "Planet Sandbox",
@ -6120,6 +6192,7 @@
"PYRAM": "Pyram Token",
"PYRK": "Pyrk",
"PYT": "Payther",
"PYUSD": "PayPal USD",
"PZM": "Prizm",
"Q1S": "Quantum1Net",
"Q2C": "QubitCoin",
@ -6178,6 +6251,7 @@
"QUA": "Quantum Tech",
"QUACK": "Rich Quack",
"QUAM": "Quam Network",
"QUANT": "Quant Finance",
"QUARASHI": "Quarashi Network",
"QUARTZ": "Sandclock",
"QUASA": "Quasacoin",
@ -6201,7 +6275,7 @@
"RAC": "RAcoin",
"RACA": "Radio Caca",
"RACEFI": "RaceFi",
"RAD": "Radicle",
"RAD": "Radworks",
"RADAR": "DappRadar",
"RADI": "RadicalCoin",
"RADIO": "RadioShack",
@ -6220,7 +6294,7 @@
"RAM": "Ramifi Protocol",
"RAMP": "RAMP",
"RANKER": "RankerDao",
"RAP": "Rapture",
"RAP": "Philosoraptor",
"RAPDOGE": "RapDoge",
"RARE": "SuperRare",
"RARI": "Rarible",
@ -6277,6 +6351,7 @@
"REA": "Realisto",
"REAL": "RealLink",
"REALM": "Realm",
"REALMS": "Realms of Ethernity",
"REALPLATFORM": "REAL",
"REALY": "Realy Metaverse",
"REAP": "ReapChain",
@ -6287,6 +6362,7 @@
"RED": "RED TOKEN",
"REDC": "RedCab",
"REDCO": "Redcoin",
"REDDIT": "Reddit",
"REDI": "REDi",
"REDLANG": "RED",
"REDLC": "Redlight Chain",
@ -6324,7 +6400,7 @@
"REST": "Restore",
"RET": "RealTract",
"RETAIL": "Retail.Global",
"RETH": "Realms of Ethernity",
"RETH": "Rocket Pool ETH",
"RETH2": "rETH2",
"RETIRE": "Retire Token",
"REU": "REUCOIN",
@ -6351,6 +6427,7 @@
"RGP": "Rigel Protocol",
"RGT": "Rari Governance Token",
"RHEA": "Rhea",
"RHINO": "RHINO",
"RHOC": "RChain",
"RHP": "Rhypton Club",
"RIC": "Riecoin",
@ -6490,6 +6567,7 @@
"RWE": "Real-World Evidence",
"RWN": "Rowan Token",
"RWS": "Robonomics Web Services",
"RXD": "Radiant",
"RXT": "RIMAUNANGIS",
"RYC": "RoyalCoin",
"RYCN": "RoyalCoin 2.0",
@ -6564,6 +6642,7 @@
"SBTC": "Super Bitcoin",
"SC": "Siacoin",
"SCA": "SiaClassic",
"SCAM": "Scam Coin",
"SCAP": "SafeCapital",
"SCAR": "Velhalla",
"SCASH": "SpaceCash",
@ -6624,6 +6703,7 @@
"SEER": "SEER",
"SEI": "Sei",
"SEL": "SelenCoin",
"SELF": "SELFCrypto",
"SEM": "Semux",
"SEN": "Sentaro",
"SENATE": "SENATE",
@ -6665,6 +6745,7 @@
"SGE": "Society of Galactic Exploration",
"SGLY": "Singularity",
"SGN": "Signals Network",
"SGO": "SafuuGO",
"SGOLD": "SpaceGold",
"SGP": "SGPay",
"SGR": "Sogur Currency",
@ -6684,6 +6765,7 @@
"SHEESH": "Sheesh it is bussin bussin",
"SHEESHA": "Sheesha Finance",
"SHELL": "Shell Token",
"SHERA": "Shera Tokens",
"SHFL": "SHUFFLE!",
"SHFT": "Shyft Network",
"SHI": "Shirtum",
@ -6719,6 +6801,8 @@
"SHR": "ShareToken",
"SHREK": "ShrekCoin",
"SHROOM": "Shroom.Finance",
"SHROOMFOX": "Magic Shroom",
"SHS": "SHEESH",
"SHX": "Stronghold Token",
"SI": "Siren",
"SIB": "SibCoin",
@ -7018,9 +7102,11 @@
"STEN": "Steneum Coin",
"STEP": "Step Finance",
"STEPH": "Step Hero",
"STEPR": "Step",
"STEPS": "Steps",
"STERLINGCOIN": "SterlingCoin",
"STETH": "Staked Ether",
"STEWIE": "Stewie Coin",
"STEX": "STEX",
"STF": "Structure Finance",
"STFX": "STFX",
@ -7055,7 +7141,7 @@
"STR": "Sourceless",
"STRAKS": "Straks",
"STRAX": "Stratis",
"STRAY": "Animal Token",
"STRAY": "Stray Dog",
"STREAM": "STREAMIT COIN",
"STRIP": "Stripto",
"STRK": "Strike",
@ -7361,6 +7447,7 @@
"TOM": "TOM Finance",
"TOMAHAWKCOIN": "Tomahawkcoin",
"TOMB": "Tomb",
"TOMI": "tomiNet",
"TOMO": "TomoChain",
"TOMOE": "TomoChain ERC20",
"TOMS": "TomTomCoin",
@ -7385,6 +7472,7 @@
"TOTM": "Totem",
"TOWER": "Tower",
"TOWN": "Town Star",
"TOX": "INTOverse",
"TOZ": "Tozex",
"TP": "Token Swap",
"TPAD": "TrustPad",
@ -7600,6 +7688,7 @@
"UNITY": "SuperNET",
"UNIVRS": "Universe",
"UNIX": "UniX",
"UNLEASH": "UnleashClub",
"UNN": "UNION Protocol Governance Token",
"UNO": "Unobtanium",
"UNORE": "UnoRe",
@ -7673,6 +7762,7 @@
"UTT": "United Traders Token",
"UTU": "UTU Protocol",
"UUU": "U Network",
"UWU": "uwu",
"UZUMAKI": "Uzumaki Inu",
"VAB": "Vabble",
"VADER": "Vader Protocol",
@ -7695,6 +7785,7 @@
"VCF": "Valencia CF Fan Token",
"VCG": "VCGamers",
"VCK": "28VCK",
"VCORE": "VCORE",
"VDG": "VeriDocGlobal",
"VDL": "Vidulum",
"VDO": "VidioCoin",
@ -7710,6 +7801,7 @@
"VEIL": "VEIL",
"VELA": "Vela Token",
"VELO": "Velo",
"VELOD": "Velodrome Finance",
"VELOX": "Velox",
"VELOXPROJECT": "Velox",
"VEMP": "vEmpire DDAO",
@ -7782,6 +7874,7 @@
"VNT": "VNT Chain",
"VNTW": "Value Network Token",
"VNX": "VisionX",
"VNXAU": "VNX Gold",
"VNXLU": "VNX Exchange",
"VOCO": "Provoco",
"VODKA": "Vodka Token",
@ -7902,7 +7995,8 @@
"WEC": "Whole Earth Coin",
"WEGEN": "WeGen Platform",
"WELD": "Weld",
"WELL": "Well",
"WELL": "Moonwell",
"WELLTOKEN": "Well",
"WELT": "Fabwelt",
"WELUPS": "Welups Blockchain",
"WEMIX": "WEMIX",
@ -7958,6 +8052,7 @@
"WIX": "Wixlar",
"WIZ": "WIZ Protocol",
"WKD": "Wakanda Inu",
"WLD": "Worldcoin",
"WLF": "Wolfs Group",
"WLITI": "wLITI",
"WLK": "Wolk",
@ -7983,6 +8078,7 @@
"WNZ": "Winerz",
"WOA": "Wrapped Origin Axie",
"WOD": "World of Defish",
"WOID": "WORLD ID",
"WOJ": "Wojak Finance",
"WOLF": "Insanity Coin",
"WOLFILAND": "Wolfiland",
@ -8000,6 +8096,7 @@
"WOOFY": "Woofy",
"WOOL": "Wolf Game Wool",
"WOONK": "Woonkly",
"WOOO": "wooonen",
"WOOP": "Woonkly Power",
"WOP": "WorldPay",
"WORLD": "World Token",
@ -8010,6 +8107,7 @@
"WOZX": "Efforce",
"WPC": "WePiggy Coin",
"WPE": "OPES (Wrapped PE)",
"WPLS": "Wrapped Pulse",
"WPP": "Green Energy Token",
"WPR": "WePower",
"WQT": "Work Quest",
@ -8049,6 +8147,7 @@
"WZEC": "Wrapped Zcash",
"WZENIQ": "Wrapped Zeniq (ETH)",
"WZRD": "Wizardia",
"X": "AI-X",
"X2": "X2Coin",
"X2Y2": "X2Y2",
"X42": "X42 Protocol",
@ -8096,7 +8195,7 @@
"XCI": "Cannabis Industry Coin",
"XCLR": "ClearCoin",
"XCM": "CoinMetro",
"XCN": "Chain",
"XCN": "Onyxcoin",
"XCO": "XCoin",
"XCONSOL": "X-Consoles",
"XCP": "CounterParty",
@ -8365,6 +8464,7 @@
"YUANG": "Yuang Coin",
"YUCJ": "Yu Coin",
"YUCT": "Yucreat",
"YUDI": "Yudi",
"YUM": "Yumerium",
"YUMMY": "Yummy",
"YUP": "Crowdholding",

View File

@ -1,4 +1,5 @@
{
"CYBER24781": "CyberConnect",
"LUNA1": "Terra",
"LUNA2": "Terra",
"SGB1": "Songbird",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
export const HAS_PERMISSION_KEY = 'has_permission';
export function HasPermission(permission: string) {
return SetMetadata(HAS_PERMISSION_KEY, permission);
}

View File

@ -0,0 +1,55 @@
import { HttpException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { Test, TestingModule } from '@nestjs/testing';
import { HasPermissionGuard } from './has-permission.guard';
describe('HasPermissionGuard', () => {
let guard: HasPermissionGuard;
let reflector: Reflector;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HasPermissionGuard, Reflector]
}).compile();
guard = module.get<HasPermissionGuard>(HasPermissionGuard);
reflector = module.get<Reflector>(Reflector);
});
function setupReflectorSpy(returnValue: string) {
jest.spyOn(reflector, 'get').mockReturnValue(returnValue);
}
function createMockExecutionContext(permissions: string[]) {
return new ExecutionContextHost([
{
user: {
permissions // Set user permissions based on the argument
}
}
]);
}
it('should deny access if the user does not have any permission', () => {
setupReflectorSpy('required-permission');
const noPermissions = createMockExecutionContext([]);
expect(() => guard.canActivate(noPermissions)).toThrow(HttpException);
});
it('should deny access if the user has the wrong permission', () => {
setupReflectorSpy('required-permission');
const wrongPermission = createMockExecutionContext(['wrong-permission']);
expect(() => guard.canActivate(wrongPermission)).toThrow(HttpException);
});
it('should allow access if the user has the required permission', () => {
setupReflectorSpy('required-permission');
const rightPermission = createMockExecutionContext(['required-permission']);
expect(guard.canActivate(rightPermission)).toBe(true);
});
});

View File

@ -0,0 +1,37 @@
import { HAS_PERMISSION_KEY } from '@ghostfolio/api/decorators/has-permission.decorator';
import { hasPermission } from '@ghostfolio/common/permissions';
import {
CanActivate,
ExecutionContext,
HttpException,
Injectable
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable()
export class HasPermissionGuard implements CanActivate {
public constructor(private reflector: Reflector) {}
public canActivate(context: ExecutionContext): boolean {
const requiredPermission = this.reflector.get<string>(
HAS_PERMISSION_KEY,
context.getHandler()
);
if (!requiredPermission) {
return true; // No specific permissions required
}
const { user } = context.switchToHttp().getRequest();
if (!user || !hasPermission(user.permissions, requiredPermission)) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return true;
}
}

View File

@ -3,7 +3,7 @@ import { cloneDeep, isArray, isObject } from 'lodash';
export function hasNotDefinedValuesInObject(aObject: Object): boolean {
for (const key in aObject) {
if (aObject[key] === null || aObject[key] === null) {
if (aObject[key] === null || aObject[key] === undefined) {
return true;
} else if (isObject(aObject[key])) {
return hasNotDefinedValuesInObject(aObject[key]);

View File

@ -7,6 +7,7 @@ import helmet from 'helmet';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware';
async function bootstrap() {
const configApp = await NestFactory.create(AppModule);
@ -23,7 +24,7 @@ async function bootstrap() {
defaultVersion: '1',
type: VersioningType.URI
});
app.setGlobalPrefix('api');
app.setGlobalPrefix('api', { exclude: ['sitemap.xml'] });
app.useGlobalPipes(
new ValidationPipe({
forbidNonWhitelisted: true,
@ -40,6 +41,7 @@ async function bootstrap() {
helmet({
contentSecurityPolicy: {
directives: {
connectSrc: ["'self'", 'https://js.stripe.com'], // Allow connections to Stripe
frameSrc: ["'self'", 'https://js.stripe.com'], // Allow loading frames from Stripe
scriptSrc: ["'self'", "'unsafe-inline'", 'https://js.stripe.com'], // Allow inline scripts and scripts from Stripe
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
@ -51,7 +53,8 @@ async function bootstrap() {
);
}
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
app.use(HtmlTemplateMiddleware);
const HOST = configService.get<string>('HOST') || '0.0.0.0';
const PORT = configService.get<number>('PORT') || 3333;
@ -59,15 +62,6 @@ async function bootstrap() {
logLogo();
Logger.log(`Listening at http://${HOST}:${PORT}`);
Logger.log('');
if (BASE_CURRENCY) {
Logger.warn(
`The environment variable "BASE_CURRENCY" is deprecated and will be removed in Ghostfolio 2.0.`
);
Logger.warn(
'Please use the currency converter in the activity dialog instead.'
);
}
});
}

View File

@ -0,0 +1,155 @@
import * as fs from 'fs';
import { join } from 'path';
import { environment } from '@ghostfolio/api/environments/environment';
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import {
DEFAULT_LANGUAGE_CODE,
DEFAULT_ROOT_URL,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
import { format } from 'date-fns';
import { NextFunction, Request, Response } from 'express';
const i18nService = new I18nService();
let indexHtmlMap: { [languageCode: string]: string } = {};
const title = 'Ghostfolio';
try {
indexHtmlMap = SUPPORTED_LANGUAGE_CODES.reduce(
(map, languageCode) => ({
...map,
[languageCode]: fs.readFileSync(
join(__dirname, '..', 'client', languageCode, 'index.html'),
'utf8'
)
}),
{}
);
} catch {}
const locales = {
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt': {
featureGraphicPath: 'assets/images/blog/ghostfolio-x-sackgeld.png',
title: `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`
},
'/en/blog/2022/08/500-stars-on-github': {
featureGraphicPath: 'assets/images/blog/500-stars-on-github.jpg',
title: `500 Stars - ${title}`
},
'/en/blog/2022/10/hacktoberfest-2022': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2022.png',
title: `Hacktoberfest 2022 - ${title}`
},
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances': {
featureGraphicPath: 'assets/images/blog/20221226.jpg',
title: `The importance of tracking your personal finances - ${title}`
},
'/en/blog/2023/02/ghostfolio-meets-umbrel': {
featureGraphicPath: 'assets/images/blog/ghostfolio-x-umbrel.png',
title: `Ghostfolio meets Umbrel - ${title}`
},
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github': {
featureGraphicPath: 'assets/images/blog/1000-stars-on-github.jpg',
title: `Ghostfolio reaches 1000 Stars on GitHub - ${title}`
},
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio': {
featureGraphicPath: 'assets/images/blog/20230520.jpg',
title: `Unlock your Financial Potential with Ghostfolio - ${title}`
},
'/en/blog/2023/07/exploring-the-path-to-fire': {
featureGraphicPath: 'assets/images/blog/20230701.jpg',
title: `Exploring the Path to FIRE - ${title}`
},
'/en/blog/2023/08/ghostfolio-joins-oss-friends': {
featureGraphicPath: 'assets/images/blog/ghostfolio-joins-oss-friends.png',
title: `Ghostfolio joins OSS Friends - ${title}`
},
'/en/blog/2023/09/ghostfolio-2': {
featureGraphicPath: 'assets/images/blog/ghostfolio-2.jpg',
title: `Announcing Ghostfolio 2.0 - ${title}`
},
'/en/blog/2023/09/hacktoberfest-2023': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
title: `Hacktoberfest 2023 - ${title}`
},
'/en/blog/2023/11/black-week-2023': {
featureGraphicPath: 'assets/images/blog/black-week-2023.jpg',
title: `Black Week 2023 - ${title}`
},
'/en/blog/2023/11/hacktoberfest-2023-debriefing': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
title: `Hacktoberfest 2023 Debriefing - ${title}`
}
};
const isFileRequest = (filename: string) => {
if (filename === '/assets/LICENSE') {
return true;
} else if (
filename.includes('auth/ey') ||
filename.includes(
'personal-finance-tools/open-source-alternative-to-de.fi'
) ||
filename.includes(
'personal-finance-tools/open-source-alternative-to-markets.sh'
)
) {
return false;
}
return filename.split('.').pop() !== filename;
};
export const HtmlTemplateMiddleware = async (
request: Request,
response: Response,
next: NextFunction
) => {
const path = request.originalUrl.replace(/\/$/, '');
let languageCode = path.substr(1, 2);
if (!SUPPORTED_LANGUAGE_CODES.includes(languageCode)) {
languageCode = DEFAULT_LANGUAGE_CODE;
}
const currentDate = format(new Date(), DATE_FORMAT);
const rootUrl = process.env.ROOT_URL || DEFAULT_ROOT_URL;
if (
path.startsWith('/api/') ||
isFileRequest(path) ||
!environment.production
) {
// Skip
next();
} else {
const indexHtml = interpolate(indexHtmlMap[languageCode], {
currentDate,
languageCode,
path,
rootUrl,
description: i18nService.getTranslation({
languageCode,
id: 'metaDescription'
}),
featureGraphicPath:
locales[path]?.featureGraphicPath ?? 'assets/cover.png',
keywords: i18nService.getTranslation({
languageCode,
id: 'metaKeywords'
}),
title:
locales[path]?.title ??
`${title} ${i18nService.getTranslation({
languageCode,
id: 'slogan'
})}`
});
return response.send(indexHtml);
}
};

View File

@ -1,4 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import {
PortfolioDetails,
@ -6,16 +7,18 @@ import {
UserSettings
} from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
private accounts: PortfolioDetails['accounts'];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private accounts: PortfolioDetails['accounts']
accounts: PortfolioDetails['accounts']
) {
super(exchangeRateDataService, {
name: 'Investment'
});
this.accounts = accounts;
}
public evaluate(ruleSettings: Settings) {

View File

@ -1,17 +1,20 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
private accounts: PortfolioDetails['accounts'];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private accounts: PortfolioDetails['accounts']
accounts: PortfolioDetails['accounts']
) {
super(exchangeRateDataService, {
name: 'Single Account'
});
this.accounts = accounts;
}
public evaluate() {

View File

@ -1,17 +1,20 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: TimelinePosition[]
positions: TimelinePosition[]
) {
super(exchangeRateDataService, {
name: 'Investment: Base Currency'
});
this.positions = positions;
}
public evaluate(ruleSettings: Settings) {

View File

@ -1,17 +1,20 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: TimelinePosition[]
positions: TimelinePosition[]
) {
super(exchangeRateDataService, {
name: 'Investment'
});
this.positions = positions;
}
public evaluate(ruleSettings: Settings) {

View File

@ -0,0 +1,46 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
export class EmergencyFundSetup extends Rule<Settings> {
private emergencyFund: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
emergencyFund: number
) {
super(exchangeRateDataService, {
name: 'Emergency Fund: Set up'
});
this.emergencyFund = emergencyFund;
}
public evaluate(ruleSettings: Settings) {
if (this.emergencyFund > ruleSettings.threshold) {
return {
evaluation: 'An emergency fund has been set up',
value: true
};
}
return {
evaluation: 'No emergency fund has been set up',
value: false
};
}
public getSettings(aUserSettings: UserSettings): Settings {
return {
baseCurrency: aUserSettings.baseCurrency,
isActive: true,
threshold: 0
};
}
}
interface Settings extends RuleSettings {
baseCurrency: string;
threshold: number;
}

View File

@ -1,22 +1,29 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class FeeRatioInitialInvestment extends Rule<Settings> {
private fees: number;
private totalInvestment: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private totalInvestment: number,
private fees: number
totalInvestment: number,
fees: number
) {
super(exchangeRateDataService, {
name: 'Investment'
name: 'Fee Ratio'
});
this.fees = fees;
this.totalInvestment = totalInvestment;
}
public evaluate(ruleSettings: Settings) {
const feeRatio = this.fees / this.totalInvestment;
const feeRatio = this.totalInvestment
? this.fees / this.totalInvestment
: 0;
if (feeRatio > ruleSettings.threshold) {
return {

View File

@ -8,14 +8,20 @@ export class ApiService {
public buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByAssetSubClasses,
filterBySearchQuery,
filterByTags
}: {
filterByAccounts?: string;
filterByAssetClasses?: string;
filterByAssetSubClasses?: string;
filterBySearchQuery?: string;
filterByTags?: string;
}): Filter[] {
const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? [];
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
const searchQuery = filterBySearchQuery?.toLowerCase();
const tagIds = filterByTags?.split(',') ?? [];
return [
@ -31,6 +37,16 @@ export class ApiService {
type: 'ASSET_CLASS'
};
}),
...assetSubClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_SUB_CLASS'
};
}),
{
id: searchQuery,
type: 'SEARCH_QUERY'
},
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,

View File

@ -1,4 +1,5 @@
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
import { DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
@ -11,10 +12,6 @@ export class ConfigurationService {
this.environmentConfiguration = cleanEnv(process.env, {
ACCESS_TOKEN_SALT: str(),
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
BASE_CURRENCY: str({
choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'],
default: 'USD'
}),
BETTER_UPTIME_API_KEY: str({ default: '' }),
CACHE_QUOTES_TTL: num({ default: 1 }),
CACHE_TTL: num({ default: 1 }),
@ -41,12 +38,13 @@ export class ConfigurationService {
JWT_SECRET_KEY: str({}),
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
OPEN_FIGI_API_KEY: str({ default: '' }),
PORT: port({ default: 3333 }),
RAPID_API_API_KEY: str({ default: '' }),
REDIS_HOST: str({ default: 'localhost' }),
REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port({ default: 6379 }),
ROOT_URL: str({ default: 'http://localhost:4200' }),
ROOT_URL: str({ default: DEFAULT_ROOT_URL }),
STRIPE_PUBLIC_KEY: str({ default: '' }),
STRIPE_SECRET_KEY: str({ default: '' }),
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),

View File

@ -2,6 +2,7 @@ import {
GATHER_ASSET_PROFILE_PROCESS,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@ -48,7 +49,7 @@ export class CronService {
name: GATHER_ASSET_PROFILE_PROCESS,
opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}`
jobId: getAssetProfileIdentifier({ dataSource, symbol })
}
};
})

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 { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull';
@ -28,6 +29,7 @@ import { DataGatheringProcessor } from './data-gathering.processor';
ExchangeRateDataModule,
MarketDataModule,
PrismaModule,
PropertyModule,
SymbolProfileModule
],
providers: [DataGatheringProcessor, DataGatheringService],

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