mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Task/improve changelog entry (#5738)
* Update changelog
* Feature/set up Storybook story for holdings table component (#5697)
* Set up Storybook story for holdings table component
* Update changelog
* Feature/improve accounts table Storybook story (#5739)
* Improve Storybook story
* Feature/use asset profile resolutions in getQuotes() of FMP service (#5743)
* Use asset profile resolutions in getQuotes()
* Update changelog
* Task/extract footer to component (#5702)
* Extract footer to component
* Update changelog
* Feature/improve currency validation in search functionality of data provider service (#5745)
* Improve currency validation
* Update changelog
* Feature/use asset profile resolutions in getQuotes() of FMP service (part 2) (#5750)
* Use asset profile resolutions in getQuotes()
* Bugfix/respect includeIndices flag in search functionality of FMP service (#5746)
* Respect includeIndices in search()
* Update changelog
* Feature/improve currency validation in getAssetProfiles() functionality of data provider service (#5747)
* Improve currency validation
* Update changelog
* Bugfix/reset scroll position on page change (#5753)
* Reset scroll position on page change
* Update changelog
* Feature/add Stealth Wealth to glossary (#5754)
* Add Stealth Wealth
* Update changelog
* Bugfix/dark mode in logo carousel component (#5758)
* Fix dark mode
* Update changelog
* Task/refactor interest to interestInBaseCurrency in portfolio summary interface (#5763)
* Refactor interest to interestInBaseCurrency
* Feature/extend pricing page (#5761)
* Extend pricing page
* Update changelog
* Bugfix/fix word wrap in menus of activities table (#5764)
* Fix word wrap
* Update changelog
* Feature/improve portfolio calculator unit tests by loading currency from user settings (#5765)
* Use currency from user settings
* Update changelog
* Task/migrate blog page component to standalone (#5742)
* Migrate blog page component to standalone
* Update changelog
* Task/refactor PortfolioDividends interface to PortfolioDividendsResponse interface (#5773)
* Refactor PortfolioDividends to PortfolioDividendsResponse
* Bugfix/total buy and sell calculation in summary (#5759)
* Fix total buy and sell calculation related to activities in custom currency
* Update changelog
* Bugfix/import of activity with MANUAL data source (CSV file) (#5749)
* Fix import of activity with MANUAL data source
* Update changelog
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Bugfix/fix penthouse-apartment.json (#5775)
* Fix penthouse-apartment.json
* Feature/update locales (#5714)
* Update locales
* Update translations
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/refactor PortfolioInvestments interface to PortfolioInvestmentsResponse interface (#5774)
* Refactor PortfolioInvestments to PortfolioInvestmentsResponse
* Task/refactor BenchmarkMarketDataDetails to BenchmarkMarketDataDetailsResponse (#5771)
* Refactor BenchmarkMarketDataDetails to BenchmarkMarketDataDetailsResponse
* Release 2.209.0 (#5780)
* Feature/allow data gathering by date range (#5762)
* Allow data gathering by date range
* Update changelog
* Feature/update locales (#5788)
* Update locales
* Update translations
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/auto-pad holdings table in AI prompt using tablemark (#5772)
* Auto-pad holdings table in AI prompt using tablemark
* Update changelog
* Task/improve changelog entry (#5790)
* Update changelog
* Task/harmonize wording in glossary (#5781)
* Harmonize wording
* Task/improve typings of getInfo() functionality (#5803)
* Improve typings
* Task/harmonize interfaces naming (#5796)
* Harmonize interfaces naming
* Task/improve typings of getAsset() functionality (#5804)
* Improve typings of getAsset() functionality
* Task/extract portfolio filter sub form of assistant to reusable component (#5618)
* Extract portfolio filter sub form of assistant to reusable component
* Update changelog
* Task/extend rule settings interface by locale (#5802)
* Extend rule settings interface by locale
* Task/refactor Export interface to ExportResponse interface (#5805)
* Refactor Export interface to ExportResponse interface
* Task/improve typings of getOrderById() functionality (#5810)
* Improve typings of getOrderById() functionality
* Bugfix/database seed (#5792)
* Fix database seed
* Update changelog
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/upgrade ioredis to forfeit overriding defaults (#5813)
* Upgrade ioredis to forfeit overriding defaults
* Update changelog
* Bugfix/submit form of login with access token dialog with enter key press (#5751)
* Fix form submit with enter key press
* Update changelog
* Task/introduce interface for create Stripe checkout session response (#5791)
* Introduce interface
* Task/clean up unused eslint-disable-next-line directives (#5782)
* Clean up unused eslint-disable-next-line directives
* Release 2.210.0 (#5814)
* Bugfix/include missing dotenv packages (#5817)
* Include missing dotenv packages
* Release 2.210.1 (#5818)
* Task/upgrade prisma to version 6.18.0 (#5823)
* Upgrade prisma to version 6.18.0
* Update changelog
* Task/format value in Buying Power rule (#5824)
* Format value in Buying Power rule
* Update changelog
* Task/move prisma.config.ts to .config/prisma.ts (#5821)
* Move prisma.config.ts to .config/prisma.ts
* Update changelog
* Task/extend export response by performanceCalculationType (#5816)
* Extend export response by performanceCalculationType
* Update changelog
* Bugfix/footer row style of accounts table component (#5826)
* Fix style of footer row
* Update changelog
* Task/migrate tablemark to v4 (#5809)
* Migrate tablemark to v4
* Update changelog
* Bugfix/custom asset name rendering in import activities dialog (#5787)
* Fix custom asset name rendering in import activities dialog
* Update changelog
* Release 2.211.0-beta.0 (#5829)
* Feature/set up user detail dialog in admin control panel (#5819)
* Set up user detail dialog
* Update changelog
* Feature/update locales (#5807)
* Update locales
* Update translations
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/refactor Activities interface to ActivitiesResponse interface (#5835)
* Refactor Activities interface to ActivitiesResponse interface
* Feature/integrate SelfhostedHub into logo carousel (#5786)
* Add SelfhostedHub
* Update changelog
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Bugfix/market price in base currency during the portfolio snapshot calculation (#5828)
* Add fallback for market price in base currency
* Update changelog
* Release 2.211.0 (#5837)
* Release 2.211.0 (#5838)
* Bugfix/provide missing locale to rule settings dialog (#5845)
* Provide locale to rule settings dialog
* Update changelog
* Feature/extend user detail dialog (#5844)
* Extend user detail dialog
* Update changelog
* Feature/add close holding button to holding detail dialog (#5832)
* Add close holding button to holding detail dialog
* Update changelog
* Feature/update locales (#5847)
* Update locales
* Update translations
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/improve typings of dialogs (#5846)
* Improve typings
* Task/extend Contributing section in README.md (#5864)
* Add GitHub Sponsors
* Task/add LambdaTest to Sponsors in README.md (#5861)
* Add LambdaTest
* Task/refactor column definitions in AI service (#5834)
* Refactor column definitions in AI service
* Update changelog
* Feature/set up sponsors section on about page (#5862)
* Set up sponsors section
* Update changelog
* Bugfix/fix typography hierarchy in resources pages (#5863)
* Fix hierarchy
* Feature/improve usability of user detail dialog (#5868)
* Do not reload on close
* Update changelog
* Release 2.212.0 (#5871)
* Feature/extend menu in activities table component (#5855)
* Extend menu in activities table component
* Update changelog
* Feature/improve icon of holdings tabs (#5842)
* Improve icon of holdings tabs
* Update changelog
* Feature/update locales (#5852)
* Update locales
* Update translation
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/add error logging to symbol lookup in Trackinsight data enhancer (#5872)
* Add error logging
* Update changelog
* Bugfix/LambdaTest logo (#5873)
* Fix LambdaTest logo
* Task/migrate tags selector component in holding detail dialog to form control (#5850)
* Migrate tags selector component to form control
* Update changelog
* Feature/upgrade NestJS to version 11.1.8 (#5874)
* Upgrade nestjs to version 11.1.8
* Update changelog
* Release 2.213.0 (#5876)
* Feature/refresh cryptocurrencies list 20251031 (#5880)
* Update cryptocurrencies.json
* Update changelog
* Task/conditionally show Sponsors section on about page (#5882)
* Conditionally show sponsors section
* Feature/update OSS friends 20251031 (#5879)
* Update OSS friends
* Feature/improve icon of View Holding menu item in activities table (#5881)
* Improve icon
* Update changelog
* Task/resolve @typescript-eslint/prefer-regexp-exec ESLint rule (#5885)
* fix(lint): remove @typescript-eslint/prefer-regexp-exec override
* fix(lint): resolve eslint errors
* Task/upgrade ng-extract-i18n-merge to version 3.1.0 (#5886)
* Upgrade ng-extract-i18n-merge to version 3.1.0
* Update changelog
* Task/resolve @typescript-eslint/no-unsafe-function-type ESLint rule (#5889)
* fix(lint): remove @typescript-eslint/no-unsafe-function-type override
* fix(lint): resolve eslint errors
* Task/resolve no-constant-binary-expression ESLint rule (#5890)
* fix(lint): remove no-constant-binary-expression override
* fix(lint): resolve eslint errors
* Task/extend user settings in test files (#5836)
* Extend user settings by performance calculation type
* Feature/set up unit test for BTCEUR in base currency EUR (#5778)
* Set up test
* Feature/atomic data replacement during historical market data gathering (#5858)
* Atomic data replacement during historical market data gathering
* Update changelog
* Task/remove Internet Identity as social login provider (#5891)
* Remove Internet Identity
* Update changelog
* Task/upgrade countries-list to version 3.2.0 (#5888)
* Upgrade countries-list to version 3.2.0
* Update changelog
* Task/upgrade twitter-api-v2 to version 1.27.0 (#5892)
* Upgrade twitter-api-v2 to version 1.27.0
* Update changelog
* Release 2.214.0 (#5893)
* Feature/migrate client build executor to @nx/angular:browser-esbuild (#5883)
* Migrate client build executor to @nx/angular:browser-esbuild
* Update changelog
* Bugfix/fix style of safe withdrawal rate selector (#5899)
* Fix style of selector
* Update changelog
* Task/introduce interface for get admin users response (#5903)
* Introduce interface for get admin users response
* Task/improve localization in lib components (#5907)
* Improve localization
* Task/improve localization of get started buttons (#5913)
* Improve localization
* Feature/update locales (#5875)
* Update locales
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Feature/improve usability for benchmark and markets management in asset profile dialog (#5911)
* Improve usability for benchmark and markets management
* Update changelog
* Feature/update locales (#5916)
* Update locales
* Update translation
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Bugfix/header alignment in admin platform and tag tables (#5908)
* Fix header alignment
* Update changelog
* Task/migrate app component to standalone (#5906)
* Migrate app component to standalone
* Update changelog
* Bugfix/assign admin role to first user signing up (#5914)
* Assign admin role to first user signing up
* Update changelog
* Release 2.215.0-beta.1 (#5918)
* Feature/add endpoint to get user by id (#5910)
* Add endpoint to get user by id
* Update changelog
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/upgrade @ionic/angular to version 8.7.8 (#5909)
* Upgrade @ionic/angular to version 8.7.8
* Update changelog
* Release 2.215.0 (#5922)
* Task/reorder lifecycle hooks in various components (#5919)
* Reorder lifecycle hooks
* Task/upgrade chart.js to version 4.5.1 (#5905)
* Upgrade chart.js to version 4.5.1
* Update changelog
* Task/upgrade svgmap to version 2.14.0 (#5904)
* Upgrade svgmap to version 2.14.0
* Update changelog
* Task/introduce interface for get account response (#5902)
* Introduce interface for get account response
* Feature/improve language localization for ZH 20251110 (#5928)
* Improve language localization for ZH
* Update changelog
* Task/improve localization of limited offer (#5929)
* Improve localization
* Task/improve promotion system (#5930)
* Add fallback to promotion logic
* Task/refactor primary text colors (#5900)
* Refactor primary text colors
* Release 2.216.0 (#5932)
* Task/improve localization of auto-renewal (#5933)
* Improve localization
* Task/refactor getHolding() in portfolio service (#5898)
* Refactor getHolding() if no holding has been found
* Update changelog
* Feature/update locales (#5931)
* Update locales
* Update translation
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/upgrade prisma to version 6.19.0 (#5937)
* Upgrade prisma to version 6.19.0
* Update changelog
* Task/fetch user data on demand in user detail dialog (#5923)
* Fetch user data on demand in user detail dialog
* Update changelog
* Task/enforce module boundaries for api and common modules (#5925)
* feat(lint): allow circular self deps
* feat(lint): enforce module boundaries
* feat(lib): move data provider response interface to common
* feat(lib): move symbol item interface to common
* feat(lib): move activity interface to common
* feat(lint): temporarily disable @nx/enforce-module-boundaries for ui files
* feat(lint): temporarily disable @nx/enforce-module-boundaries for client files
* feat(lint): ignore circular deps between client and ui
* feat(common): implement barrel export for data provider response interface
* feat(common): implement barrel export for activity interface
* feat(common): implement barrel export for symbol item interface
* Feature/separate Google OAuth and token authentication (#5915)
* Separate Google OAuth and token authentication
* Update changelog
* Feature/automatically gather required exchange rates (#5917)
* Automatically gather required exchange rates
* Update changelog
* Task/enforce module boundaries for client module (#5944)
* feat(lib): move SymbolPipe to common lib
* feat(lib): move CreateAccountBalanceDto to common lib
* feat(lib): move IsCurrencyCode validator to common lib
* feat(lib): move UpdateAssetProfileDto to common lib
* feat(lib): move UpdateUserSettingDto to common lib
* feat(lib): move CreateAccessDto to common lib
* feat(lib): move UpdateAccessDto to common lib
* feat(lib): move CreateTagDto to common lib
* feat(lib): move UpdateTagDto to common lib
* feat(lib): move CreatePlatformDto to common lib
* feat(lib): move UpdatePlatformDto to common lib
* feat(lib): move CreateOrderDto to common lib
* feat(lib): move UpdateOrderDto to common lib
* feat(lib): move RuleSettings interface to common lib
* feat(lib): move CreateAccountDto TransferBalanceDto UpdateAccountDto to common lib
* feat(lib): move CreateAccountWithBalancesDto to common lib
* feat(lib): move CreateAssetProfileDto and CreateAssetProfileWithMarketDataDto to common lib
* feat(lib): move AuthDeviceDto to common lib
* feat(lib): move simplewebauthn interfaces to common lib
This includes AssertionCredentialJSON, AttestationCredentialJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON.
* feat(lib): move UpdateMarketDataDto to common lib
* feat(lib): move UpdateBulkMarketDataDto to common lib
* feat(lib): move CreateWatchlistItemDto to common lib
* feat(lib): move DeleteOwnUserDto to common lib
* feat(lib): move UserItem interface to common lib
* feat(lib): move UpdateOwnAccessTokenDto to common lib
* feat(lib): move PropertyDto to common lib
* Bugfix/fix type error in CreateAccountWithBalancesDto (#5945)
* Refactor import
* Feature/add black weeks 2025 blog post (#5942)
* Add Black Weeks 2025 blog post
* Update changelog
* Release 2.217.0 (#5946)
* Bugfix/missing reflect-metadata polyfill in apps/client (#5952)
* Add reflect-metadata to polyfill
* Task/ignore forex in search results of FMP service (#5951)
* Ignore forex in search
* Update changelog
* Release 2.217.1 (#5954)
* Task/enforce module boundaries for ui module (#5947)
* feat(lib): move ConfirmationDialogType to common lib
* fix(lib): move SubscriptionType to enums
* feat(lib): move validateObjectForForm util to common lib
* feat(lib): move GfDialogFooterComponent to ui lib
* feat(lib): move GfDialogHeaderComponent to ui lib
* Task/integrate OSS Gallery into logo carousel (#5959)
* Integrate OSS Gallery
* Update changelog
* Task/upgrade yahoo-finance2 to version 3.10.1 (#5956)
* Upgrade yahoo-finance2 to version 3.10.1
* Update changelog
* Feature/extend menu in accounts table component (#5960)
* Extend menu
* Update changelog
* Feature/simplify portfolio summary on mobile (#5962)
* Simplify portfolio summary on mobile
* Improve numerical precision
* Hide hints
* Update changelog
* Feature/extend portfolio summary by percentage values (#5964)
* Extend summary by percentage values
* Update changelog
* Release 2.218.0 (#5965)
* Feature/add authentication method to user detail dialog (#5970)
* Extend user detail dialog
* Update changelog
* Feature/improve transform data source in request and response interceptor (#5972)
* Resolve data source of GHOSTFOLIO data provider
* Update changelog
* Feature/update locales (#5940)
* Update locales
* Update translation
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Feature/disable delete activities action if empty table (#5971)
* Disable delete activities action
* Update changelog
* Bugfix/edit of draft activities (#5974)
* Fix edit of draft activities
* Update changelog
* Task/refresh cryptocurrencies list 20251121 (#5967)
* Update cryptocurrencies.json
* Update changelog
* Task/improve validation of currency management in Admin Control panel (#5973)
* Improve validation (disallow GBp)
* Update changelog
* Feature/various improvements on pricing page (#5982)
* Various improvements
* Update changelog
* Task/update OSS friends (#5968)
* Update OSS friends
* Feature/update locales (#5977)
* Update locales
* Update translation
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/upgrade yahoo-finance2 to version 3.10.2 (#5978)
* Upgrade yahoo-finance2 to version 3.10.2
* Update changelog
* Release 2.219.0 (#5983)
* Bugfix/exchange rate calculation when converting derived currencies (#5961)
* Fix exchange rate calculation when converting derived currencies
* Update changelog
* Task/refactor from v4 as uuidv4 from uuid to crypto.randomUUID() (#5990)
* Refactor from v4 as uuidv4 from uuid to randomUUID() from node:crypto
* Update changelog
* Task/restructure user detail dialog (#5985)
* Restructure user detail dialog
* Task/upgrade color to version 5.0.3 (#5984)
* Upgrade color to version 5.0.3
* Update changelog
* Task/remove obsolete includeDrafts attribute in public controller (#5975)
* Remove obsolete includeDrafts attribute
* Feature/improve asset profile data gathering (#5997)
* Improve asset profile data gathering
* Update changelog
* Task/remove Cypress setup (#5995)
* Remove Cypress setup
* Update changelog
* Task/improve usability of actions in various tables (#5992)
* Improve usability of actions
* Task/upgrade prettier to version 3.7.2 (#5999)
* Upgrade prettier to version 3.7.2
* Update changelog
* Release 2.220.0 (#6001)
* Feature/extend Storybook story of portfolio proportion chart component by story in percentage (#6007)
* Extend Storybook story
* Update changelog
* Bugfix/countries in FMP service (#6005)
* Introduce countries mapping
* Update changelog
* Task/upgrade @internationalized/number to version 3.6.5 (#6000)
* Upgrade @internationalized/number to version 3.6.5
* Update changelog
* Task/refactor query parameters in data provider services (#6011)
* Refactor query parameters
* Update changelog
* Task/upgrade prettier to version 3.7.3 (#6014)
* Upgrade prettier to version 3.7.3
* Update changelog
* Bugfix/improve search by name in FMP service (#6012)
* Improve search by name
* Update changelog
* Task/extend subscription offer key type (#6022)
* Extend SubscriptionOfferKey
* Bugfix/user endpoint of admin control panel (#6021)
* Fix user endpoint
* Update changelog
* Release 2.221.0 (#6024)
* Task/upgrade envalid to version 8.1.1 (#6026)
* Upgrade envalid to version 8.1.1
* Update changelog
* Bugfix/clean up CHANGELOG.md (#5976)
* Clean up
* Task/remove return type in parseSector() of YahooFinanceService (#6006)
* Remove return type
* Task/upgrade prettier to version 3.7.4 (#6031)
* Upgrade prettier to version 3.7.4
* Update changelog
* Task/prettify files 20251204 (#6033)
* Prettify files
* Task/restructure pricing page (#6037)
* Restructure pricing page
* Task/increase numerical precision for cryptocurrency quantities in holding detail dialog (#6038)
* Increase numerical precision for cryptocurrency quantities
* Update changelog
* Feature/data source transformation in import for self-hosted environments (#6032)
* Introduce data source transformation support for self-hosted environments
* Update changelog
* Feature/add 3D hover effect to membership card component (#5966)
* Add 3D hover effect to membership card component
* Update changelog
* Feature/OIDC authentication (#5981)
* Set up OIDC authentication
* Update changelog
* Task/activate 3d hover effect in account membership overview (#6039)
* Activate 3d hover effect
* Release 2.222.0 (#6041)
* Task/improve OIDC login button label (#6043)
* Improve label
* Feature/update locales (#5993)
* Update locales
* Update translation
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/add OpenID Connect (OIDC) configuration details to README (#6044)
* Add OpenID Connect (OIDC) configuration details
* Task/move notification service to UI library (#6048)
* Move notification service to UI library
* Update changelog
* Task/update OSS Friends (#6047)
* Update OSS Friends
* Task/update VS Code extension of Prettier (#6010)
* Update VS Code extension of Prettier
* Bugfix/allocate remaining percentage to unknown data in portfolio proportion chart (#6054)
* Allocate remaining percentage to unknown data in portfolio proportion chart
* Update changelog
* Feature/extend FIRE page with projection information at retirement date (#6034)
* Extend FIRE page with projection information at retirement date
* Update changelog
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/update note in personal finance tools (#6053)
* Update note
* Feature/update locales (#6049)
* Update locales
* Update translations
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/improve language localization for de 20251214 (#6066)
* Improve language localization
* Feature/update locales (#6065)
* Update locales
* Update translations
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Release 2.223.0 (#6067)
* Task/refresh cryptocurrencies list 20251215 (#6073)
* Update cryptocurrencies.json
* Update changelog
* Task/remove deprecated Angular CLI decorator (#6071)
* Remove deprecated Angular CLI decorator
* Update changelog
* Task/include first and last date of each calendar year in getChartDateMap() (#6069)
* Include first and last date of each calendar year in getChartDateMap()
* Update changelog
* Task/improve localization of various components (#6074)
* Improve localization
* Bugfix/localize date formatting in FIRE calculator (#6077)
* Localize date formatting
* Update changelog
* Feature/add ISIN number to asset profile dialog (#6076)
* Add ISIN number
* Update changelog
* Task/refactor language in various components (#6078)
* Refactor language
* Feature/restore support for specific calendar year date ranges in assistant (#6079)
* Restore specific calendar year date ranges
* Update changelog
* Update locales (#6068)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Release 2.224.0 (#6080)
* Revert "Task/refactor language in various components (#6078)" (#6081)
This reverts commit 80c278555c.
* Release 2.224.1 (#6084)
* Release 2.224.2 (#6086)
* Feature/extend personal finance tools 20251220 (#6087)
* Add BlueBudget, Moneyspire and Pennies
* Task/refactor href in user account settings component (#6085)
* Refactoring
* Task/upgrade shx to version 0.4.0 (#6088)
* Upgrade shx to version 0.4.0
* Update changelog
* Bugfix/add missing currency suffix to cash balance field in create or update account dialog (#6092)
* Add missing currency suffix to cash balance field in create or update account dialog
* Update changelog
* Bugfix/fix asset profile deletion and allow editing asset profile identifiers with MANUAL data source (#6090)
* Fix asset profile deletion and allow editing asset profile identifiers with MANUAL data source
* Update changelog
* Task/refactor read platforms permission (#6095)
* Refactor read platforms permission
* Task/upgrade to Nx 22.0.4 and Angular 20.3 (#6098)
* feat(nx): migrate to v22.0.4
* fix(deps): install with Node.js 22
* Update changelog
* Task/upgrade to Nx 22.1.3 and Storybook 10.1.10 (#6105)
* chore(deps): bump nx from 22.0 to 22.1
* fix(ts): set moduleResolution to bundler
This is a requirement for Storybook 10 migration. See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#require-tsconfigjson-moduleresolution-set-to-value-that-supports-types-condition.
* feat(sb): change main.js to ESM
See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#the-storybookmain-file-and-other-presets-must-be-valid-esm.
* fix(ts): set target to es2022
This resolves the warning: TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and "false" respectively by the Angular CLI. To control ECMA version and features use the Browserslist configuration. For more information, see https://angular.dev/tools/cli/build#configuring-browser-compatibility.
* fix(ts): set esModuleInterop to true
* feat(docs): update changelog
* fix(ts): set moduleResolution to bundler
* fix(ts): revert target changes
* Update changelog
* Task/upgrade ng-extract-i18n-merge from 3.1.0 to 3.2.1 (#6106)
* chore(deps): bump ng-extract-i18n-merge from 3.1.0 to 3.2.1
* feat(docs): update changelog
* Task/revert adding styles to ui:storybook target (#6107)
* fix(sb): revert adding styles to ui:storybook target
* Task/upgrade jest to version 30.2.0 (#6110)
* chore(deps): bump jest to 30.2.0
* fix(test): update setup test scripts
* fix(deps): resolve vulns
* Bugfix/time in market for impersonation mode (#6103)
* Fix time in market for impersonation mode
* Update changelog
* Task/improve language localization for de 20251229 (#6108)
* Update translations
* Update changelog
* Feature/create endpoint to get all platforms (#6097)
* Create endpoint to get all platforms
* Update changelog
* Task/improve user detail dialog routing in Admin Control panel (#6104)
* Improve user detail dialog routing in Admin Control panel
* Update changelog
* Task/deprecate platforms in info item (#6114)
* Deprecate platforms
* Task/eliminate ngx-stripe (#6116)
* Eliminate ngx-stripe
* Update changelog
* Task/upgrade to marked 16.4.2 and ngx-markdown 20.1.0 (#6117)
* chore(deps): bump marked and ngx-markdown
* fix(client): change marked script file
* fix(client): change umd to esm
* fix(client): remove marked script file from project.json
* feat(docs): update changelog
* Task/upgrade to Nx 22.3.3 and Angular 21.0 (#6109)
* chore(deps): upgrade to Nx 22.3.3 and Angular 21.0
* feat(nx): run migrations
* fix(ts): change module and moduleResolution for api
* feat(ts): remove duplicates of bundler module resolution
* fix(ts): enable ESM interoperability
* fix(api): update imports
* fix(ts): set module to preserve in api spec
* fix(ts): update target
* resolve comments
* fix(api): set jest moduleNameMapper
* fix(ts): update tsconfig in api spec
* fix(api): revert import changes
* fix(apps): revert unnecessary changes
* fix(client): change marked js file
* fix(deps): remove duplicates
* Release 2.225.0 (#6120)
* Task/upgrade class-validator to version 0.14.3 (#6091)
* Upgrade class-validator to version 0.14.3
* Update changelog
* Task/update year to 2026 (#6119)
* Update year
* Task/extend data providers content in self-hosting FAQ (#6123)
* Extend data providers content
* Update changelog
* Task/upgrade yahoo-finance2 to version 3.11.2 (#6126)
* Upgrade yahoo-finance2 to version 3.11.2
* Update changelog
* Release 2.226.0 (#6127)
* Bugfix/jsonpath import in manual service (#6129)
* Fix jsonpath import
* Update changelog
* Bugfix/initialize input properties in fire calculator (#6122)
* Initialize input properties
* Update changelog
* Task/upgrade stripe to version 20.1.0 (#6132)
* Upgrade stripe to version 20.1.0
* Update changelog
* Task/remove deprecated public Stripe key (#6124)
* Remove deprecated public Stripe key
* Update changelog
* Release 2.227.0 (#6134)
* Task/move scraper configuration to tab in asset profile dialog (#6094)
* Move scraper configuration to tab in asset profile dialog
* Update changelog
* Task/lazy load platforms via API in create or update account dialog (#6130)
* Lazy load platforms via API
* Update changelog
* Task/remove deprecated public Stripe key (part 2) (#6138)
* Remove deprecated public Stripe key
* Feature/update locales (#6083)
* Update locales
* Update translations
* Update changelog
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
* Task/rename branch and title in extract locales GitHub action (#6142)
* Rename branch and title
* Feature/extend holdings endpoint to include performance with currency effects for cash positions (#5650)
* Extend holdings endpoint to include performance with currency effects for cash positions
* Update changelog
* Bugfix/header alignment in accounts table on mobile (#6143)
* Fix header alignment
* Update changelog
* fix(client): remove unused variables
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Co-authored-by: David Requeno <108202767+DavidReque@users.noreply.github.com>
Co-authored-by: Aditya Pawar <143347456+JustAdi10@users.noreply.github.com>
Co-authored-by: Mariam Saeed <69825646+Mariam-Saeed@users.noreply.github.com>
Co-authored-by: Dibyendu Sahoo <dibyendusahoo03@gmail.com>
Co-authored-by: H_S <81474463+HarjobandeepSingh@users.noreply.github.com>
Co-authored-by: Umesh Pal <127204670+72umesh@users.noreply.github.com>
Co-authored-by: Sven Günther <sven.guenther@it-agile.de>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Aman <142656811+Amanthisside@users.noreply.github.com>
Co-authored-by: Google <53668973+GooglyBlox@users.noreply.github.com>
Co-authored-by: s-vamshi <62489114+s-vamshi@users.noreply.github.com>
Co-authored-by: vitalymatyushik <vitaly_matyushik@outlook.com>
Co-authored-by: Vansh <140736931+Vansh-Parate@users.noreply.github.com>
Co-authored-by: Germán Martín <github@gmartin.net>
Co-authored-by: Ani07-05 <86768646+Ani07-05@users.noreply.github.com>
Co-authored-by: Harsh Santwani <96873014+HydrallHarsh@users.noreply.github.com>
Co-authored-by: Arshad Jamal <arshad.jamal@vlinkinfo.com>
Co-authored-by: Szymon Łągiewka <slagiewka@users.noreply.github.com>
Co-authored-by: jjs2099 <140512982+jjs2099@users.noreply.github.com>
Co-authored-by: danielochinasa <danielochinasa@gmail.com>
Co-authored-by: Vaishnavi Parabkar <parabkarvaishnavi24@gmail.com>
Co-authored-by: Abhishek Singla <singlaabhishek849@gmail.com>
Co-authored-by: Arghya Das <alfaarghya.dev@gmail.com>
Co-authored-by: TMs <tms@live.cn>
Co-authored-by: Johnson Towoju <johnsontodimu@gmail.com>
Co-authored-by: Eshaan Gupta <146680427+Eshaan-byte@users.noreply.github.com>
Co-authored-by: Karel De Smet <karel.de.smet@outlook.com>
Co-authored-by: Eduardo Almeida <edualm@users.noreply.github.com>
Co-authored-by: Vahant Sharma <vahantofficials12315@gmail.com>
Co-authored-by: Paul van der lei <54599584+0pilatos0@users.noreply.github.com>
Co-authored-by: Omkar Gujja <67428719+omkarg01@users.noreply.github.com>
pull/6148/head
committed by
GitHub
614 changed files with 39958 additions and 34409 deletions
@ -0,0 +1,14 @@ |
|||||
|
import { defineConfig } from '@prisma/config'; |
||||
|
import { config } from 'dotenv'; |
||||
|
import { expand } from 'dotenv-expand'; |
||||
|
import { join } from 'node:path'; |
||||
|
|
||||
|
expand(config({ quiet: true })); |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
migrations: { |
||||
|
path: join(__dirname, '..', 'prisma', 'migrations'), |
||||
|
seed: `node ${join(__dirname, '..', 'prisma', 'seed.mts')}` |
||||
|
}, |
||||
|
schema: join(__dirname, '..', 'prisma', 'schema.prisma') |
||||
|
}); |
||||
@ -1 +1,2 @@ |
|||||
custom: ['https://www.buymeacoffee.com/ghostfolio'] |
buy_me_a_coffee: ghostfolio |
||||
|
github: ghostfolio |
||||
|
|||||
@ -1,8 +1,8 @@ |
|||||
{ |
{ |
||||
"recommendations": [ |
"recommendations": [ |
||||
"angular.ng-template", |
"angular.ng-template", |
||||
"esbenp.prettier-vscode", |
|
||||
"firsttris.vscode-jest-runner", |
"firsttris.vscode-jest-runner", |
||||
"nrwl.angular-console" |
"nrwl.angular-console", |
||||
|
"prettier.prettier-vscode" |
||||
] |
] |
||||
} |
} |
||||
|
|||||
@ -1,4 +1,4 @@ |
|||||
{ |
{ |
||||
"editor.defaultFormatter": "esbenp.prettier-vscode", |
"editor.defaultFormatter": "prettier.prettier-vscode", |
||||
"editor.formatOnSave": true |
"editor.formatOnSave": true |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,114 @@ |
|||||
|
import ms from 'ms'; |
||||
|
|
||||
|
/** |
||||
|
* Custom state store for OIDC authentication that doesn't rely on express-session. |
||||
|
* This store manages OAuth2 state parameters in memory with automatic cleanup. |
||||
|
*/ |
||||
|
export class OidcStateStore { |
||||
|
private readonly STATE_EXPIRY_MS = ms('10 minutes'); |
||||
|
|
||||
|
private stateMap = new Map< |
||||
|
string, |
||||
|
{ |
||||
|
appState?: unknown; |
||||
|
ctx: { issued?: Date; maxAge?: number; nonce?: string }; |
||||
|
meta?: unknown; |
||||
|
timestamp: number; |
||||
|
} |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Store request state. |
||||
|
* Signature matches passport-openidconnect SessionStore |
||||
|
*/ |
||||
|
public store( |
||||
|
_req: unknown, |
||||
|
_meta: unknown, |
||||
|
appState: unknown, |
||||
|
ctx: { maxAge?: number; nonce?: string; issued?: Date }, |
||||
|
callback: (err: Error | null, handle?: string) => void |
||||
|
) { |
||||
|
try { |
||||
|
// Generate a unique handle for this state
|
||||
|
const handle = this.generateHandle(); |
||||
|
|
||||
|
this.stateMap.set(handle, { |
||||
|
appState, |
||||
|
ctx, |
||||
|
meta: _meta, |
||||
|
timestamp: Date.now() |
||||
|
}); |
||||
|
|
||||
|
// Clean up expired states
|
||||
|
this.cleanup(); |
||||
|
|
||||
|
callback(null, handle); |
||||
|
} catch (error) { |
||||
|
callback(error as Error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Verify request state. |
||||
|
* Signature matches passport-openidconnect SessionStore |
||||
|
*/ |
||||
|
public verify( |
||||
|
_req: unknown, |
||||
|
handle: string, |
||||
|
callback: ( |
||||
|
err: Error | null, |
||||
|
appState?: unknown, |
||||
|
ctx?: { maxAge?: number; nonce?: string; issued?: Date } |
||||
|
) => void |
||||
|
) { |
||||
|
try { |
||||
|
const data = this.stateMap.get(handle); |
||||
|
|
||||
|
if (!data) { |
||||
|
return callback(null, undefined, undefined); |
||||
|
} |
||||
|
|
||||
|
if (Date.now() - data.timestamp > this.STATE_EXPIRY_MS) { |
||||
|
// State has expired
|
||||
|
this.stateMap.delete(handle); |
||||
|
return callback(null, undefined, undefined); |
||||
|
} |
||||
|
|
||||
|
// Remove state after verification (one-time use)
|
||||
|
this.stateMap.delete(handle); |
||||
|
|
||||
|
callback(null, data.ctx, data.appState); |
||||
|
} catch (error) { |
||||
|
callback(error as Error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clean up expired states |
||||
|
*/ |
||||
|
private cleanup() { |
||||
|
const now = Date.now(); |
||||
|
const expiredKeys: string[] = []; |
||||
|
|
||||
|
for (const [key, value] of this.stateMap.entries()) { |
||||
|
if (now - value.timestamp > this.STATE_EXPIRY_MS) { |
||||
|
expiredKeys.push(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (const key of expiredKeys) { |
||||
|
this.stateMap.delete(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Generate a cryptographically secure random handle |
||||
|
*/ |
||||
|
private generateHandle() { |
||||
|
return ( |
||||
|
Math.random().toString(36).substring(2, 15) + |
||||
|
Math.random().toString(36).substring(2, 15) + |
||||
|
Date.now().toString(36) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
import { Injectable, Logger } from '@nestjs/common'; |
||||
|
import { PassportStrategy } from '@nestjs/passport'; |
||||
|
import { Provider } from '@prisma/client'; |
||||
|
import { Request } from 'express'; |
||||
|
import { Strategy, type StrategyOptions } from 'passport-openidconnect'; |
||||
|
|
||||
|
import { AuthService } from './auth.service'; |
||||
|
import { |
||||
|
OidcContext, |
||||
|
OidcIdToken, |
||||
|
OidcParams, |
||||
|
OidcProfile |
||||
|
} from './interfaces/interfaces'; |
||||
|
import { OidcStateStore } from './oidc-state.store'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { |
||||
|
private static readonly stateStore = new OidcStateStore(); |
||||
|
|
||||
|
public constructor( |
||||
|
private readonly authService: AuthService, |
||||
|
options: StrategyOptions |
||||
|
) { |
||||
|
super({ |
||||
|
...options, |
||||
|
passReqToCallback: true, |
||||
|
store: OidcStrategy.stateStore |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async validate( |
||||
|
_request: Request, |
||||
|
issuer: string, |
||||
|
profile: OidcProfile, |
||||
|
context: OidcContext, |
||||
|
idToken: OidcIdToken, |
||||
|
_accessToken: string, |
||||
|
_refreshToken: string, |
||||
|
params: OidcParams |
||||
|
) { |
||||
|
try { |
||||
|
const thirdPartyId = |
||||
|
profile?.id ?? |
||||
|
profile?.sub ?? |
||||
|
idToken?.sub ?? |
||||
|
params?.sub ?? |
||||
|
context?.claims?.sub; |
||||
|
|
||||
|
const jwt = await this.authService.validateOAuthLogin({ |
||||
|
thirdPartyId, |
||||
|
provider: Provider.OIDC |
||||
|
}); |
||||
|
|
||||
|
if (!thirdPartyId) { |
||||
|
Logger.error( |
||||
|
`Missing subject identifier in OIDC response from ${issuer}`, |
||||
|
'OidcStrategy' |
||||
|
); |
||||
|
|
||||
|
throw new Error('Missing subject identifier in OIDC response'); |
||||
|
} |
||||
|
|
||||
|
return { jwt }; |
||||
|
} catch (error) { |
||||
|
Logger.error(error, 'OidcStrategy'); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,24 +0,0 @@ |
|||||
import { Type } from 'class-transformer'; |
|
||||
import { |
|
||||
ArrayNotEmpty, |
|
||||
IsArray, |
|
||||
IsISO8601, |
|
||||
IsNumber, |
|
||||
IsOptional |
|
||||
} from 'class-validator'; |
|
||||
|
|
||||
export class UpdateBulkMarketDataDto { |
|
||||
@ArrayNotEmpty() |
|
||||
@IsArray() |
|
||||
@Type(() => UpdateMarketDataDto) |
|
||||
marketData: UpdateMarketDataDto[]; |
|
||||
} |
|
||||
|
|
||||
class UpdateMarketDataDto { |
|
||||
@IsISO8601() |
|
||||
@IsOptional() |
|
||||
date?: string; |
|
||||
|
|
||||
@IsNumber() |
|
||||
marketPrice: number; |
|
||||
} |
|
||||
@ -0,0 +1,22 @@ |
|||||
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; |
||||
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; |
||||
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; |
||||
|
import { PlatformsResponse } from '@ghostfolio/common/interfaces'; |
||||
|
import { permissions } from '@ghostfolio/common/permissions'; |
||||
|
|
||||
|
import { Controller, Get, UseGuards } from '@nestjs/common'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
|
||||
|
@Controller('platforms') |
||||
|
export class PlatformsController { |
||||
|
public constructor(private readonly platformService: PlatformService) {} |
||||
|
|
||||
|
@Get() |
||||
|
@HasPermission(permissions.readPlatforms) |
||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) |
||||
|
public async getPlatforms(): Promise<PlatformsResponse> { |
||||
|
const platforms = await this.platformService.getPlatforms(); |
||||
|
|
||||
|
return { platforms }; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { PlatformsController } from './platforms.controller'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [PlatformsController], |
||||
|
imports: [PlatformModule] |
||||
|
}) |
||||
|
export class PlatformsModule {} |
||||
@ -0,0 +1,205 @@ |
|||||
|
import { |
||||
|
activityDummyData, |
||||
|
symbolProfileDummyData, |
||||
|
userDummyData |
||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
||||
|
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
import { Activity } from '@ghostfolio/common/interfaces'; |
||||
|
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { Big } from 'big.js'; |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
||||
|
return { |
||||
|
CurrentRateService: jest.fn().mockImplementation(() => { |
||||
|
return CurrentRateServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
||||
|
() => { |
||||
|
return { |
||||
|
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
||||
|
return PortfolioSnapshotServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
||||
|
return { |
||||
|
RedisCacheService: jest.fn().mockImplementation(() => { |
||||
|
return RedisCacheServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
describe('PortfolioCalculator', () => { |
||||
|
let configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
configurationService = new ConfigurationService(); |
||||
|
|
||||
|
currentRateService = new CurrentRateService(null, null, null, null); |
||||
|
|
||||
|
exchangeRateDataService = new ExchangeRateDataService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
portfolioSnapshotService = new PortfolioSnapshotService(null); |
||||
|
|
||||
|
redisCacheService = new RedisCacheService(null, null); |
||||
|
|
||||
|
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
||||
|
configurationService, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with BALN.SW buy and buy', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = [ |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2021-11-22'), |
||||
|
feeInAssetProfileCurrency: 1.55, |
||||
|
quantity: 2, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'CHF', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Bâloise Holding AG', |
||||
|
symbol: 'BALN.SW' |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPriceInAssetProfileCurrency: 142.9 |
||||
|
}, |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2021-11-30'), |
||||
|
feeInAssetProfileCurrency: 1.65, |
||||
|
quantity: 2, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'CHF', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Bâloise Holding AG', |
||||
|
symbol: 'BALN.SW' |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPriceInAssetProfileCurrency: 136.6 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROAI, |
||||
|
currency: 'CHF', |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
const investments = portfolioCalculator.getInvestments(); |
||||
|
|
||||
|
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ |
||||
|
data: portfolioSnapshot.historicalData, |
||||
|
groupBy: 'month' |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot).toMatchObject({ |
||||
|
currentValueInBaseCurrency: new Big('595.6'), |
||||
|
errors: [], |
||||
|
hasErrors: false, |
||||
|
positions: [ |
||||
|
{ |
||||
|
averagePrice: new Big('139.75'), |
||||
|
currency: 'CHF', |
||||
|
dataSource: 'YAHOO', |
||||
|
dividend: new Big('0'), |
||||
|
dividendInBaseCurrency: new Big('0'), |
||||
|
fee: new Big('3.2'), |
||||
|
feeInBaseCurrency: new Big('3.2'), |
||||
|
firstBuyDate: '2021-11-22', |
||||
|
grossPerformance: new Big('36.6'), |
||||
|
grossPerformancePercentage: new Big('0.07706261539956593567'), |
||||
|
grossPerformancePercentageWithCurrencyEffect: new Big( |
||||
|
'0.07706261539956593567' |
||||
|
), |
||||
|
grossPerformanceWithCurrencyEffect: new Big('36.6'), |
||||
|
investment: new Big('559'), |
||||
|
investmentWithCurrencyEffect: new Big('559'), |
||||
|
netPerformance: new Big('33.4'), |
||||
|
netPerformancePercentage: new Big('0.07032490039195361342'), |
||||
|
netPerformancePercentageWithCurrencyEffectMap: { |
||||
|
max: new Big('0.06986689805847808234') |
||||
|
}, |
||||
|
netPerformanceWithCurrencyEffectMap: { |
||||
|
max: new Big('33.4') |
||||
|
}, |
||||
|
marketPrice: 148.9, |
||||
|
marketPriceInBaseCurrency: 148.9, |
||||
|
quantity: new Big('4'), |
||||
|
symbol: 'BALN.SW', |
||||
|
tags: [], |
||||
|
timeWeightedInvestment: new Big('474.93846153846153846154'), |
||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big( |
||||
|
'474.93846153846153846154' |
||||
|
), |
||||
|
transactionCount: 2, |
||||
|
valueInBaseCurrency: new Big('595.6') |
||||
|
} |
||||
|
], |
||||
|
totalFeesWithCurrencyEffect: new Big('3.2'), |
||||
|
totalInterestWithCurrencyEffect: new Big('0'), |
||||
|
totalInvestment: new Big('559'), |
||||
|
totalInvestmentWithCurrencyEffect: new Big('559'), |
||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0') |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( |
||||
|
expect.objectContaining({ |
||||
|
netPerformance: 33.4, |
||||
|
netPerformanceInPercentage: 0.07032490039195362, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0.07032490039195362, |
||||
|
netPerformanceWithCurrencyEffect: 33.4, |
||||
|
totalInvestmentValueWithCurrencyEffect: 559 |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
expect(investments).toEqual([ |
||||
|
{ date: '2021-11-22', investment: new Big('285.8') }, |
||||
|
{ date: '2021-11-30', investment: new Big('559') } |
||||
|
]); |
||||
|
|
||||
|
expect(investmentsByMonth).toEqual([ |
||||
|
{ date: '2021-11-01', investment: 559 }, |
||||
|
{ date: '2021-12-01', investment: 0 } |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,150 @@ |
|||||
|
import { |
||||
|
activityDummyData, |
||||
|
loadExportFile, |
||||
|
symbolProfileDummyData, |
||||
|
userDummyData |
||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; |
||||
|
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
||||
|
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; |
||||
|
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { Big } from 'big.js'; |
||||
|
import { join } from 'node:path'; |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
||||
|
return { |
||||
|
CurrentRateService: jest.fn().mockImplementation(() => { |
||||
|
return CurrentRateServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', |
||||
|
() => { |
||||
|
return { |
||||
|
ExchangeRateDataService: jest.fn().mockImplementation(() => { |
||||
|
return ExchangeRateDataServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
||||
|
() => { |
||||
|
return { |
||||
|
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
||||
|
return PortfolioSnapshotServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
||||
|
return { |
||||
|
RedisCacheService: jest.fn().mockImplementation(() => { |
||||
|
return RedisCacheServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
describe('PortfolioCalculator', () => { |
||||
|
let exportResponse: ExportResponse; |
||||
|
|
||||
|
let configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
beforeAll(() => { |
||||
|
exportResponse = loadExportFile( |
||||
|
join(__dirname, '../../../../../../../test/import/ok/btceur.json') |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
configurationService = new ConfigurationService(); |
||||
|
|
||||
|
currentRateService = new CurrentRateService(null, null, null, null); |
||||
|
|
||||
|
exchangeRateDataService = new ExchangeRateDataService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
portfolioSnapshotService = new PortfolioSnapshotService(null); |
||||
|
|
||||
|
redisCacheService = new RedisCacheService(null, null); |
||||
|
|
||||
|
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
||||
|
configurationService, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with BTCUSD buy (in EUR)', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = exportResponse.activities.map( |
||||
|
(activity) => ({ |
||||
|
...activityDummyData, |
||||
|
...activity, |
||||
|
date: parseDate(activity.date), |
||||
|
feeInAssetProfileCurrency: 4.46, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: activity.dataSource, |
||||
|
name: 'Bitcoin', |
||||
|
symbol: activity.symbol |
||||
|
}, |
||||
|
unitPriceInAssetProfileCurrency: 44558.42 |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROAI, |
||||
|
currency: 'EUR', |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
const historicalDataDates = portfolioSnapshot.historicalData.map( |
||||
|
({ date }) => { |
||||
|
return date; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
expect(historicalDataDates).not.toContain('2021-01-01'); |
||||
|
expect(historicalDataDates).toContain('2021-12-31'); |
||||
|
expect(historicalDataDates).toContain('2022-01-01'); |
||||
|
expect(historicalDataDates).not.toContain('2022-12-31'); |
||||
|
|
||||
|
expect(portfolioSnapshot.positions[0].fee).toEqual(new Big(4.46)); |
||||
|
expect( |
||||
|
portfolioSnapshot.positions[0].feeInBaseCurrency.toNumber() |
||||
|
).toBeCloseTo(3.94, 1); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,126 @@ |
|||||
|
import { |
||||
|
activityDummyData, |
||||
|
loadExportFile, |
||||
|
symbolProfileDummyData, |
||||
|
userDummyData |
||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
||||
|
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; |
||||
|
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { Big } from 'big.js'; |
||||
|
import { join } from 'node:path'; |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
||||
|
return { |
||||
|
CurrentRateService: jest.fn().mockImplementation(() => { |
||||
|
return CurrentRateServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
||||
|
() => { |
||||
|
return { |
||||
|
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
||||
|
return PortfolioSnapshotServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
||||
|
return { |
||||
|
RedisCacheService: jest.fn().mockImplementation(() => { |
||||
|
return RedisCacheServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
describe('PortfolioCalculator', () => { |
||||
|
let exportResponse: ExportResponse; |
||||
|
|
||||
|
let configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
beforeAll(() => { |
||||
|
exportResponse = loadExportFile( |
||||
|
join(__dirname, '../../../../../../../test/import/ok/btcusd-short.json') |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
configurationService = new ConfigurationService(); |
||||
|
|
||||
|
currentRateService = new CurrentRateService(null, null, null, null); |
||||
|
|
||||
|
exchangeRateDataService = new ExchangeRateDataService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
portfolioSnapshotService = new PortfolioSnapshotService(null); |
||||
|
|
||||
|
redisCacheService = new RedisCacheService(null, null); |
||||
|
|
||||
|
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
||||
|
configurationService, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with BTCUSD short sell (in USD)', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = exportResponse.activities.map( |
||||
|
(activity) => ({ |
||||
|
...activityDummyData, |
||||
|
...activity, |
||||
|
date: parseDate(activity.date), |
||||
|
feeInAssetProfileCurrency: activity.fee, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: activity.dataSource, |
||||
|
name: 'Bitcoin', |
||||
|
symbol: activity.symbol |
||||
|
}, |
||||
|
unitPriceInAssetProfileCurrency: activity.unitPrice |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROAI, |
||||
|
currency: exportResponse.user.settings.currency, |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
expect(portfolioSnapshot.positions[0].averagePrice).toEqual( |
||||
|
Big(45647.95) |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,290 @@ |
|||||
|
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; |
||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service'; |
||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service'; |
||||
|
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; |
||||
|
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
||||
|
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; |
||||
|
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { DataSource } from '@prisma/client'; |
||||
|
import { randomUUID } from 'node:crypto'; |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
||||
|
return { |
||||
|
CurrentRateService: jest.fn().mockImplementation(() => { |
||||
|
return CurrentRateServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', |
||||
|
() => { |
||||
|
return { |
||||
|
ExchangeRateDataService: jest.fn().mockImplementation(() => { |
||||
|
return ExchangeRateDataServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
||||
|
() => { |
||||
|
return { |
||||
|
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
||||
|
return PortfolioSnapshotServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
||||
|
return { |
||||
|
RedisCacheService: jest.fn().mockImplementation(() => { |
||||
|
return RedisCacheServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
describe('PortfolioCalculator', () => { |
||||
|
let accountBalanceService: AccountBalanceService; |
||||
|
let accountService: AccountService; |
||||
|
let configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let dataProviderService: DataProviderService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let orderService: OrderService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
configurationService = new ConfigurationService(); |
||||
|
|
||||
|
exchangeRateDataService = new ExchangeRateDataService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
accountBalanceService = new AccountBalanceService( |
||||
|
null, |
||||
|
exchangeRateDataService, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
accountService = new AccountService( |
||||
|
accountBalanceService, |
||||
|
null, |
||||
|
exchangeRateDataService, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
redisCacheService = new RedisCacheService(null, configurationService); |
||||
|
|
||||
|
dataProviderService = new DataProviderService( |
||||
|
configurationService, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
redisCacheService |
||||
|
); |
||||
|
|
||||
|
currentRateService = new CurrentRateService( |
||||
|
dataProviderService, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
orderService = new OrderService( |
||||
|
accountBalanceService, |
||||
|
accountService, |
||||
|
null, |
||||
|
dataProviderService, |
||||
|
null, |
||||
|
exchangeRateDataService, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
portfolioSnapshotService = new PortfolioSnapshotService(null); |
||||
|
|
||||
|
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
||||
|
configurationService, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('Cash Performance', () => { |
||||
|
it('should calculate performance for cash assets in CHF default currency', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2025-01-01').getTime()); |
||||
|
|
||||
|
const accountId = randomUUID(); |
||||
|
|
||||
|
jest |
||||
|
.spyOn(accountBalanceService, 'getAccountBalances') |
||||
|
.mockResolvedValue({ |
||||
|
balances: [ |
||||
|
{ |
||||
|
accountId, |
||||
|
id: randomUUID(), |
||||
|
date: parseDate('2023-12-31'), |
||||
|
value: 1000, |
||||
|
valueInBaseCurrency: 850 |
||||
|
}, |
||||
|
{ |
||||
|
accountId, |
||||
|
id: randomUUID(), |
||||
|
date: parseDate('2024-12-31'), |
||||
|
value: 2000, |
||||
|
valueInBaseCurrency: 1800 |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
jest.spyOn(accountService, 'getCashDetails').mockResolvedValue({ |
||||
|
accounts: [ |
||||
|
{ |
||||
|
balance: 2000, |
||||
|
comment: null, |
||||
|
createdAt: parseDate('2023-12-31'), |
||||
|
currency: 'USD', |
||||
|
id: accountId, |
||||
|
isExcluded: false, |
||||
|
name: 'USD', |
||||
|
platformId: null, |
||||
|
updatedAt: parseDate('2023-12-31'), |
||||
|
userId: userDummyData.id |
||||
|
} |
||||
|
], |
||||
|
balanceInBaseCurrency: 1820 |
||||
|
}); |
||||
|
|
||||
|
jest |
||||
|
.spyOn(dataProviderService, 'getDataSourceForExchangeRates') |
||||
|
.mockReturnValue(DataSource.YAHOO); |
||||
|
|
||||
|
jest.spyOn(orderService, 'getOrders').mockResolvedValue({ |
||||
|
activities: [], |
||||
|
count: 0 |
||||
|
}); |
||||
|
|
||||
|
const { activities } = await orderService.getOrdersForPortfolioCalculator( |
||||
|
{ |
||||
|
userCurrency: 'CHF', |
||||
|
userId: userDummyData.id |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.spyOn(currentRateService, 'getValues').mockResolvedValue({ |
||||
|
dataProviderInfos: [], |
||||
|
errors: [], |
||||
|
values: [] |
||||
|
}); |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROAI, |
||||
|
currency: 'CHF', |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const { historicalData } = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
const historicalData20231231 = historicalData.find(({ date }) => { |
||||
|
return date === '2023-12-31'; |
||||
|
}); |
||||
|
const historicalData20240101 = historicalData.find(({ date }) => { |
||||
|
return date === '2024-01-01'; |
||||
|
}); |
||||
|
const historicalData20241231 = historicalData.find(({ date }) => { |
||||
|
return date === '2024-12-31'; |
||||
|
}); |
||||
|
|
||||
|
/** |
||||
|
* Investment value with currency effect: 1000 USD * 0.85 = 850 CHF |
||||
|
* Total investment: 1000 USD * 0.91 = 910 CHF |
||||
|
* Value (current): 1000 USD * 0.91 = 910 CHF |
||||
|
* Value with currency effect: 1000 USD * 0.85 = 850 CHF |
||||
|
*/ |
||||
|
expect(historicalData20231231).toMatchObject({ |
||||
|
date: '2023-12-31', |
||||
|
investmentValueWithCurrencyEffect: 850, |
||||
|
netPerformance: 0, |
||||
|
netPerformanceInPercentage: 0, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0, |
||||
|
netPerformanceWithCurrencyEffect: 0, |
||||
|
netWorth: 850, |
||||
|
totalAccountBalance: 0, |
||||
|
totalInvestment: 910, |
||||
|
totalInvestmentValueWithCurrencyEffect: 850, |
||||
|
value: 910, |
||||
|
valueWithCurrencyEffect: 850 |
||||
|
}); |
||||
|
|
||||
|
/** |
||||
|
* Net performance with currency effect: (1000 * 0.86) - (1000 * 0.85) = 10 CHF |
||||
|
* Total investment: 1000 USD * 0.91 = 910 CHF |
||||
|
* Total investment value with currency effect: 1000 USD * 0.85 = 850 CHF |
||||
|
* Value (current): 1000 USD * 0.91 = 910 CHF |
||||
|
* Value with currency effect: 1000 USD * 0.86 = 860 CHF |
||||
|
*/ |
||||
|
expect(historicalData20240101).toMatchObject({ |
||||
|
date: '2024-01-01', |
||||
|
investmentValueWithCurrencyEffect: 0, |
||||
|
netPerformance: 0, |
||||
|
netPerformanceInPercentage: 0, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0.011764705882352941, |
||||
|
netPerformanceWithCurrencyEffect: 10, |
||||
|
netWorth: 860, |
||||
|
totalAccountBalance: 0, |
||||
|
totalInvestment: 910, |
||||
|
totalInvestmentValueWithCurrencyEffect: 850, |
||||
|
value: 910, |
||||
|
valueWithCurrencyEffect: 860 |
||||
|
}); |
||||
|
|
||||
|
/** |
||||
|
* Investment value with currency effect: 1000 USD * 0.90 = 900 CHF |
||||
|
* Net performance: (1000 USD * 1.0) - (1000 USD * 1.0) = 0 CHF |
||||
|
* Net performance with currency effect: (1000 USD * 0.9) - (1000 USD * 0.85) = 50 CHF |
||||
|
* Total investment: 2000 USD * 0.91 = 1820 CHF |
||||
|
* Total investment value with currency effect: (1000 USD * 0.85) + (1000 USD * 0.90) = 1750 CHF |
||||
|
* Value (current): 2000 USD * 0.91 = 1820 CHF |
||||
|
* Value with currency effect: 2000 USD * 0.9 = 1800 CHF |
||||
|
*/ |
||||
|
expect(historicalData20241231).toMatchObject<HistoricalDataItem>({ |
||||
|
date: '2024-12-31', |
||||
|
investmentValueWithCurrencyEffect: 900, |
||||
|
netPerformance: 0, |
||||
|
netPerformanceInPercentage: 0, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0.058823529411764705, |
||||
|
netPerformanceWithCurrencyEffect: 50, |
||||
|
netWorth: 1800, |
||||
|
totalAccountBalance: 0, |
||||
|
totalInvestment: 1820, |
||||
|
totalInvestmentValueWithCurrencyEffect: 1750, |
||||
|
value: 1820, |
||||
|
valueWithCurrencyEffect: 1800 |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,144 @@ |
|||||
|
import { |
||||
|
activityDummyData, |
||||
|
symbolProfileDummyData, |
||||
|
userDummyData |
||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
||||
|
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
import { Activity } from '@ghostfolio/common/interfaces'; |
||||
|
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
||||
|
return { |
||||
|
CurrentRateService: jest.fn().mockImplementation(() => { |
||||
|
return CurrentRateServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
||||
|
() => { |
||||
|
return { |
||||
|
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
||||
|
return PortfolioSnapshotServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
||||
|
return { |
||||
|
RedisCacheService: jest.fn().mockImplementation(() => { |
||||
|
return RedisCacheServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
describe('PortfolioCalculator', () => { |
||||
|
let configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
configurationService = new ConfigurationService(); |
||||
|
currentRateService = new CurrentRateService(null, null, null, null); |
||||
|
exchangeRateDataService = new ExchangeRateDataService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
portfolioSnapshotService = new PortfolioSnapshotService(null); |
||||
|
redisCacheService = new RedisCacheService(null, null); |
||||
|
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
||||
|
configurationService, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get transaction point', () => { |
||||
|
it('with MSFT buy and sell with fractional quantities (multiples of 1/3)', () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2024-04-01').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = [ |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2024-03-08'), |
||||
|
feeInAssetProfileCurrency: 0, |
||||
|
quantity: 0.3333333333333333, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Microsoft Inc.', |
||||
|
symbol: 'MSFT' |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPriceInAssetProfileCurrency: 408 |
||||
|
}, |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2024-03-13'), |
||||
|
quantity: 0.6666666666666666, |
||||
|
feeInAssetProfileCurrency: 0, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Microsoft Inc.', |
||||
|
symbol: 'MSFT' |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPriceInAssetProfileCurrency: 400 |
||||
|
}, |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2024-03-14'), |
||||
|
quantity: 1, |
||||
|
feeInAssetProfileCurrency: 0, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Microsoft Inc.', |
||||
|
symbol: 'MSFT' |
||||
|
}, |
||||
|
type: 'SELL', |
||||
|
unitPriceInAssetProfileCurrency: 411 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROAI, |
||||
|
currency: 'USD', |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const transactionPoints = portfolioCalculator.getTransactionPoints(); |
||||
|
const lastTransactionPoint = |
||||
|
transactionPoints[transactionPoints.length - 1]; |
||||
|
const position = lastTransactionPoint.items.find( |
||||
|
(item) => item.symbol === 'MSFT' |
||||
|
); |
||||
|
|
||||
|
expect(position.investment.toNumber()).toBe(0); |
||||
|
expect(position.quantity.toNumber()).toBe(0); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -1,8 +1,8 @@ |
|||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
||||
|
|
||||
import { DateQuery } from './date-query.interface'; |
import { DateQuery } from './date-query.interface'; |
||||
|
|
||||
export interface GetValuesParams { |
export interface GetValuesParams { |
||||
dataGatheringItems: IDataGatheringItem[]; |
dataGatheringItems: DataGatheringItem[]; |
||||
dateQuery: DateQuery; |
dateQuery: DateQuery; |
||||
} |
} |
||||
|
|||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,3 @@ |
|||||
|
// Dependencies required by .config/prisma.ts in Docker container
|
||||
|
import 'dotenv'; |
||||
|
import 'dotenv-expand'; |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
export class AssetProfileChangedEvent { |
||||
|
public constructor( |
||||
|
public readonly data: AssetProfileIdentifier & { currency: string } |
||||
|
) {} |
||||
|
|
||||
|
public static getName(): string { |
||||
|
return 'assetProfile.changed'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service'; |
||||
|
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'; |
||||
|
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; |
||||
|
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; |
||||
|
|
||||
|
import { Injectable, Logger } from '@nestjs/common'; |
||||
|
import { OnEvent } from '@nestjs/event-emitter'; |
||||
|
|
||||
|
import { AssetProfileChangedEvent } from './asset-profile-changed.event'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AssetProfileChangedListener { |
||||
|
public constructor( |
||||
|
private readonly configurationService: ConfigurationService, |
||||
|
private readonly dataGatheringService: DataGatheringService, |
||||
|
private readonly dataProviderService: DataProviderService, |
||||
|
private readonly exchangeRateDataService: ExchangeRateDataService, |
||||
|
private readonly orderService: OrderService |
||||
|
) {} |
||||
|
|
||||
|
@OnEvent(AssetProfileChangedEvent.getName()) |
||||
|
public async handleAssetProfileChanged(event: AssetProfileChangedEvent) { |
||||
|
Logger.log( |
||||
|
`Asset profile of ${event.data.symbol} (${event.data.dataSource}) has changed`, |
||||
|
'AssetProfileChangedListener' |
||||
|
); |
||||
|
|
||||
|
if ( |
||||
|
this.configurationService.get( |
||||
|
'ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES' |
||||
|
) === false || |
||||
|
event.data.currency === DEFAULT_CURRENCY |
||||
|
) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const existingCurrencies = this.exchangeRateDataService.getCurrencies(); |
||||
|
|
||||
|
if (!existingCurrencies.includes(event.data.currency)) { |
||||
|
Logger.log( |
||||
|
`New currency ${event.data.currency} has been detected`, |
||||
|
'AssetProfileChangedListener' |
||||
|
); |
||||
|
|
||||
|
await this.exchangeRateDataService.initialize(); |
||||
|
} |
||||
|
|
||||
|
const { dateOfFirstActivity } = |
||||
|
await this.orderService.getStatisticsByCurrency(event.data.currency); |
||||
|
|
||||
|
if (dateOfFirstActivity) { |
||||
|
await this.dataGatheringService.gatherSymbol({ |
||||
|
dataSource: this.dataProviderService.getDataSourceForExchangeRates(), |
||||
|
date: dateOfFirstActivity, |
||||
|
symbol: `${DEFAULT_CURRENCY}${event.data.currency}` |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,11 +1,24 @@ |
|||||
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; |
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; |
||||
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
||||
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; |
||||
|
import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; |
||||
|
|
||||
import { Module } from '@nestjs/common'; |
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { AssetProfileChangedListener } from './asset-profile-changed.listener'; |
||||
import { PortfolioChangedListener } from './portfolio-changed.listener'; |
import { PortfolioChangedListener } from './portfolio-changed.listener'; |
||||
|
|
||||
@Module({ |
@Module({ |
||||
imports: [RedisCacheModule], |
imports: [ |
||||
providers: [PortfolioChangedListener] |
ConfigurationModule, |
||||
|
DataGatheringModule, |
||||
|
DataProviderModule, |
||||
|
ExchangeRateDataModule, |
||||
|
OrderModule, |
||||
|
RedisCacheModule |
||||
|
], |
||||
|
providers: [AssetProfileChangedListener, PortfolioChangedListener] |
||||
}) |
}) |
||||
export class EventsModule {} |
export class EventsModule {} |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue