diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 75e362465..000000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,151 +0,0 @@
-{
- "root": true,
- "ignorePatterns": ["**/*"],
- "plugins": ["@nx"],
- "overrides": [
- {
- "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
- "rules": {
- "@nx/enforce-module-boundaries": [
- "warn",
- {
- "enforceBuildableLibDependency": true,
- "allow": [],
- "depConstraints": [
- {
- "sourceTag": "*",
- "onlyDependOnLibsWithTags": ["*"]
- }
- ]
- }
- ],
- "@typescript-eslint/no-extra-semi": "error",
- "no-extra-semi": "off"
- }
- },
- {
- "files": ["*.ts", "*.tsx"],
- "extends": ["plugin:@nx/typescript"]
- },
- {
- "files": ["*.js", "*.jsx"],
- "extends": ["plugin:@nx/javascript"]
- },
- {
- "files": ["*.ts"],
- "plugins": ["eslint-plugin-import", "@typescript-eslint"],
- "extends": [
- "plugin:@typescript-eslint/recommended-type-checked",
- "plugin:@typescript-eslint/stylistic-type-checked"
- ],
- "rules": {
- "@typescript-eslint/consistent-indexed-object-style": "off",
- "@typescript-eslint/dot-notation": "off",
- "@typescript-eslint/explicit-member-accessibility": [
- "off",
- {
- "accessibility": "explicit"
- }
- ],
- "@typescript-eslint/member-ordering": "warn",
- "@typescript-eslint/naming-convention": [
- "off",
- {
- "selector": "default",
- "format": ["camelCase"],
- "leadingUnderscore": "allow",
- "trailingUnderscore": "allow"
- },
- {
- "selector": ["variable", "classProperty", "typeProperty"],
- "format": ["camelCase", "UPPER_CASE"],
- "leadingUnderscore": "allow",
- "trailingUnderscore": "allow"
- },
- {
- "selector": "objectLiteralProperty",
- "format": null
- },
- {
- "selector": "enumMember",
- "format": ["camelCase", "UPPER_CASE", "PascalCase"]
- },
- {
- "selector": "typeLike",
- "format": ["PascalCase"]
- }
- ],
- "@typescript-eslint/no-empty-interface": "warn",
- "@typescript-eslint/no-inferrable-types": [
- "warn",
- {
- "ignoreParameters": true
- }
- ],
- "@typescript-eslint/no-non-null-assertion": "warn",
- "@typescript-eslint/no-shadow": [
- "warn",
- {
- "hoist": "all"
- }
- ],
- "@typescript-eslint/unified-signatures": "error",
- "@typescript-eslint/no-loss-of-precision": "warn",
- "@typescript-eslint/no-var-requires": "warn",
- "@typescript-eslint/ban-types": "warn",
- "arrow-body-style": "off",
- "constructor-super": "error",
- "eqeqeq": ["error", "smart"],
- "guard-for-in": "warn",
- "id-blacklist": "off",
- "id-match": "off",
- "import/no-deprecated": "warn",
- "no-bitwise": "error",
- "no-caller": "error",
- "no-debugger": "error",
- "no-empty": "off",
- "no-eval": "error",
- "no-fallthrough": "error",
- "no-new-wrappers": "error",
- "no-restricted-imports": ["error", "rxjs/Rx"],
- "no-undef-init": "error",
- "no-underscore-dangle": "off",
- "no-var": "error",
- "radix": "error",
- "no-unsafe-optional-chaining": "warn",
- "no-extra-boolean-cast": "warn",
- "no-empty-pattern": "warn",
- "no-useless-catch": "warn",
- "no-unsafe-finally": "warn",
- "no-prototype-builtins": "warn",
- "no-async-promise-executor": "warn",
- "no-constant-condition": "warn",
-
- // The following rules are part of @typescript-eslint/recommended-type-checked
- // and can be remove once solved
- "@typescript-eslint/await-thenable": "warn",
- "@typescript-eslint/ban-ts-comment": "warn",
- "@typescript-eslint/no-base-to-string": "warn",
- "@typescript-eslint/no-explicit-any": "warn",
- "@typescript-eslint/no-floating-promises": "warn",
- "@typescript-eslint/no-misused-promises": "warn",
- "@typescript-eslint/no-redundant-type-constituents": "warn",
- "@typescript-eslint/no-unnecessary-type-assertion": "warn",
- "@typescript-eslint/no-unsafe-argument": "warn",
- "@typescript-eslint/no-unsafe-assignment": "warn",
- "@typescript-eslint/no-unsafe-enum-comparison": "warn",
- "@typescript-eslint/no-unsafe-member-access": "warn",
- "@typescript-eslint/no-unsafe-return": "warn",
- "@typescript-eslint/no-unsafe-call": "warn",
- "@typescript-eslint/require-await": "warn",
- "@typescript-eslint/restrict-template-expressions": "warn",
- "@typescript-eslint/unbound-method": "warn",
-
- // The following rules are part of @typescript-eslint/stylistic-type-checked
- // and can be remove once solved
- "@typescript-eslint/prefer-nullish-coalescing": "warn" // TODO: Requires strictNullChecks: true
- }
- }
- ],
- "extends": ["plugin:storybook/recommended"]
-}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 1fabe2f48..329a9a5a3 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -26,7 +26,7 @@ Thank you for your understanding and cooperation!
2.
3.
-**Expected behavior**
+**Expected Behavior**
@@ -48,6 +48,6 @@ Thank you for your understanding and cooperation!
- Browser
- OS
-**Additional context**
+**Additional Context**
diff --git a/.github/workflows/extract-locales.yml b/.github/workflows/extract-locales.yml
new file mode 100644
index 000000000..c17eac5b6
--- /dev/null
+++ b/.github/workflows/extract-locales.yml
@@ -0,0 +1,40 @@
+name: Extract locales
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ extract_locales:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Extract locales
+ run: npm run extract-locales
+
+ - name: Check changes
+ id: verify-changed-files
+ uses: tj-actions/verify-changed-files@v20
+
+ - name: Create pull request
+ if: steps.verify-changed-files.outputs.files_changed == 'true'
+ uses: peter-evans/create-pull-request@v7
+ with:
+ author: 'github-actions[bot] '
+ branch: 'feature/update-locales'
+ commit-message: 'Update locales'
+ delete-branch: true
+ title: 'Feature/update locales'
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63aef801b..80f831119 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,14 +7,252 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Added
+
+- Added support for filtering in the _Copy AI prompt to clipboard_ actions on the analysis page (experimental)
+- Added the _Storybook_ path to the `sitemap.xml` file
+
+### Changed
+
+- Improved the export functionality by applying filters on accounts and tags
+- Improved the symbol validation in the _Yahoo Finance_ service (get asset profiles)
+- Refactored `lodash.uniq` with `Array.from(new Set(...))`
+- Refreshed the cryptocurrencies list
+
+### Fixed
+
+- Fixed an issue in the activities import functionality related to the account balances
+- Changed client-side dates to be sent in UTC format to ensure date consistency
+ - Benchmark endpoint
+ - Exchange rate endpoint
+
+## 2.146.0 - 2025-03-15
+
+### Changed
+
+- Improved the usability of the user account registration
+- Improved the usability of the _Copy AI prompt to clipboard_ actions on the analysis page (experimental)
+- Formatted the name in the _Financial Modeling Prep_ service
+- Removed the exchange rates from the overview of the admin control panel
+- Improved the language localization for German (`de`)
+- Upgraded `angular` from version `19.0.5` to `19.2.1`
+- Upgraded `Nx` from version `20.3.2` to `20.5.0`
+- Upgraded `prettier` from version `3.5.1` to `3.5.3`
+- Upgraded `prisma` from version `6.4.1` to `6.5.0`
+
+### Fixed
+
+- Fixed an issue with serving _Storybook_ related to the `contentSecurityPolicy`
+
+## 2.145.1 - 2025-03-10
+
+### Added
+
+- Extended the export functionality by the account balances
+- Added a _Copy portfolio data to clipboard for AI prompt_ action to the analysis page (experimental)
+
+### Changed
+
+- Improved the style of the summary on the _X-ray_ page
+- Improved the language localization for German (`de`)
+- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `9.0` to `13.1`
+
+### Fixed
+
+- Fixed an issue to get dividends in the _Financial Modeling Prep_ service
+- Fixed an issue to get historical market data in the _Financial Modeling Prep_ service
+- Fixed an issue with serving _Storybook_
+
+## 2.144.0 - 2025-03-06
+
+### Fixed
+
+- Fixed the missing import functionality on the non-empty activities page
+- Fixed the functionality to delete an asset profile of a custom currency in the admin control panel
+
+## 2.143.0 - 2025-03-02
+
+### Added
+
+- Added the Ghostfolio _LinkedIn_ page to the about page
+- Added the Ghostfolio _LinkedIn_ page to the footer
+
+### Changed
+
+- Optimized the asynchronous operations using `Promise.all()` in the portfolio service (`getPerformance`)
+- Improved the symbol lookup in the _Trackinsight_ data enhancer for asset profile data
+- Removed the no transactions info component from the holdings table on the home page
+- Refactored the show condition of the step by step introduction for new users using the activities count
+- Upgraded `color` from version `4.2.3` to `5.0.0`
+- Upgraded `prisma` from version `6.3.0` to `6.4.1`
+
+### Fixed
+
+- Handled an exception in the export functionality related to platforms
+- Handled an exception in the benchmark service related to unnamed asset profiles
+
+## 2.142.0 - 2025-02-28
+
+### Added
+
+- Extended the export functionality by the platforms
+- Extended the portfolio snapshot in the portfolio calculator by the `createdAt` timestamp
+- Extended the _Trackinsight_ data enhancer for asset profile data by `cusip`
+- Added _Storybook_ to the build process
+
+### Changed
+
+- Upgraded `eslint` dependencies
+
+## 2.141.0 - 2025-02-25
+
+### Added
+
+- Extended the export functionality by the tags
+- Extended the portfolio snapshot in the portfolio calculator by the activities count
+- Extended the user endpoint `GET api/v1/user` by the activities count
+- Added `cusip` to the asset profile model
+
+### Changed
+
+- Upgraded `prettier` from version `3.4.2` to `3.5.1`
+
+### Fixed
+
+- Improved the numeric comparison of strings in the value component
+
+## 2.140.0 - 2025-02-20
+
### Changed
+- Reloaded the available tags after creating a custom tag in the holding detail dialog (experimental)
+- Improved the validation of the currency management in the admin control panel
+- Migrated the `@ghostfolio/client` components to control flow
+- Migrated the `@ghostfolio/ui` components to control flow
+- Improved the language localization for German (`de`)
+
+### Fixed
+
+- Improved the error handling in the `HttpResponseInterceptor`
+- Fixed an issue while using symbol profile overrides in the historical market data table of the admin control panel
+- Added missing assets in _Storybook_ setup
+
+## 2.139.1 - 2025-02-15
+
+### Added
+
+- Extended the tooltip in the chart of the holdings tab on the home page by the allocation, change and performance
+- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Asia-Pacific Markets)
+- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan)
+- Added support to create custom tags in the holding detail dialog (experimental)
+- Extended the tags selector component by a `readonly` attribute
+- Extended the tags selector component to support creating custom tags
+- Extended the holding detail dialog by the historical market data editor (experimental)
+- Added global styles to the _Storybook_ setup
+
+### Changed
+
+- Improved the symbol lookup in the _Trackinsight_ data enhancer for asset profile data
+- Improved the language localization for German (`de`)
+- Upgraded `@trivago/prettier-plugin-sort-imports` from version `5.2.1` to `5.2.2`
+
+### Fixed
+
+- Fixed the gaps in the chart of the benchmark comparator
+
+## 2.138.0 - 2025-02-08
+
+### Added
+
+- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Emerging Markets)
+- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Europe)
+- Added a link to _Duck.ai_ to the _Copy AI prompt to clipboard_ action on the analysis page (experimental)
+- Extracted the tags selector to a reusable component used in the create or update activity dialog and holding detail dialog
+- Added stories for the tags selector component
+
+### Changed
+
+- Improved the caching of the portfolio snapshot in the portfolio calculator by expiring cache entries when a user changes tags in the holding detail dialog
+- Improved the error handling in the _CoinGecko_ service
+- Improved the language localization for German (`de`)
+- Upgraded `svgmap` from version `2.6.0` to `2.12.2`
+
+## 2.137.1 - 2025-02-01
+
+### Added
+
+- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (North America)
+- Added support for ETF sector data in the _Yahoo Finance_ data enhancer
+
+### Changed
+
+- Extracted the scraper configuration to a sub form in the asset profile details dialog of the admin control
+- Migrated the database seeding to _TypeScript_
+- Improved the language localization for German (`de`)
+- Upgraded `@trivago/prettier-plugin-sort-imports` from version `4.3.0` to `5.2.1`
+- Upgraded `bull` from version `4.16.4` to `4.16.5`
+- Upgraded `ng-extract-i18n-merge` from version `2.13.1` to `2.14.1`
+- Upgraded `prisma` from version `6.2.1` to `6.3.0`
+
+### Fixed
+
+- Fixed the dynamic numerical precision for cryptocurrencies in the holding detail dialog
+
+## 2.136.0 - 2025-01-24
+
+### Added
+
+- Set up a _GitHub Action_ to automatically extract locales when the `main` branch changes
+
+### Changed
+
+- Extended the _Financial Modeling Prep_ service
+- Improved the language localization for Ukrainian (`uk`)
+- Refreshed the cryptocurrencies list
+- Upgraded `date-fns` from version `3.6.0` to `4.1.0`
+- Upgraded `rxjs` from version `7.5.6` to `7.8.1`
+
+### Fixed
+
+- Fixed an issue with the detection of the thousand separator by locale
+- Fixed an issue with holdings and sectors while using symbol profile overrides
+- Fixed an issue with the MIME type detection in the scraper configuration
+
+## 2.135.0 - 2025-01-19
+
+### Changed
+
+- Moved the language localization for Polski (`pl`) from experimental to general availability
+- Extended the _Financial Modeling Prep_ service
+- Switched to _ESLint_’s flat config format
+- Upgraded `bull` from version `4.16.2` to `4.16.4`
+- Upgraded `chart.js` from version `4.2.0` to `4.4.7`
+- Upgraded `chartjs-chart-treemap` from version `2.3.1` to `3.1.0`
+- Upgraded `chartjs-plugin-annotation` from version `2.1.2` to `3.1.0`
+- Upgraded `eslint` dependencies
+- Upgraded `nestjs` from version `10.1.3` to `10.4.15`
+- Upgraded `Nx` from version `20.3.0` to `20.3.2`
+- Upgraded `reflect-metadata` from version `0.1.13` to `0.2.2`
+- Upgraded `uuid` from version `11.0.2` to `11.0.5`
+
+## 2.134.0 - 2025-01-15
+
+### Added
+
+- Set up the language localization for Українська (`uk`)
+
+### Changed
+
+- Extended the health check endpoint to include database and cache operations (experimental)
- Refactored various `lodash` functions with native JavaScript equivalents
+- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.1.0` to `6.2.1`
### Fixed
+- Fixed an issue with the import of activities with type `FEE` (where unit price is `0`)
- Fixed an issue with the renaming of activities with type `FEE`, `INTEREST`, `ITEM` or `LIABILITY`
+- Handled an exception in the scraper configuration introduced by the migration from `got` to `fetch`
## 2.133.1 - 2025-01-09
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 9e32b56eb..1c45aeca1 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -60,6 +60,10 @@ Remove permission in `UserService` using `without()`
Use `@if (user?.settings?.isExperimentalFeatures) {}` in HTML template
+## Component Library (_Storybook_)
+
+https://ghostfol.io/development/storybook
+
## Git
### Rebase
@@ -101,3 +105,12 @@ https://www.prisma.io/docs/concepts/components/prisma-migrate/db-push
Run `npm run prisma migrate dev --name added_job_title`
https://www.prisma.io/docs/concepts/components/prisma-migrate#getting-started-with-prisma-migrate
+
+## SSL
+
+Generate `localhost.cert` and `localhost.pem` files.
+
+```
+openssl req -x509 -newkey rsa:2048 -nodes -keyout apps/client/localhost.pem -out apps/client/localhost.cert -days 365 \
+ -subj "/C=CH/ST=State/L=City/O=Organization/OU=Unit/CN=localhost"
+```
diff --git a/README.md b/README.md
index 14477ea9a..ed82ac723 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ Ghostfolio is for you if you are...
- ✅ Create, update and delete transactions
- ✅ Multi account management
-- ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `WTD`, `MTD`, `YTD`, `1Y`, `5Y`, `Max`
+- ✅ Portfolio performance: Return on Average Investment (ROAI) for `Today`, `WTD`, `MTD`, `YTD`, `1Y`, `5Y`, `Max`
- ✅ Various charts
- ✅ Static analysis to identify potential risks in your portfolio
- ✅ Import and export transactions
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..528fecd44
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Reporting Security Issues
+
+If you discover a security vulnerability in this repository, please report it to security[at]ghostfol.io. We will acknowledge your report and provide guidance on the next steps.
+
+To help us resolve the issue, please include the following details:
+
+- A description of the vulnerability
+- Steps to reproduce the vulnerability
+- Affected versions of the software
+
+We appreciate your responsible disclosure and will work to address the issue promptly.
diff --git a/apps/api/.eslintrc.json b/apps/api/.eslintrc.json
deleted file mode 100644
index 9263d0065..000000000
--- a/apps/api/.eslintrc.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "extends": "../../.eslintrc.json",
- "ignorePatterns": ["!**/*"],
- "rules": {},
- "overrides": [
- {
- "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
- "parserOptions": {
- "project": ["apps/api/tsconfig.*?.json"]
- },
- "rules": {}
- },
- {
- "files": ["*.ts", "*.tsx"],
- "rules": {}
- },
- {
- "files": ["*.js", "*.jsx"],
- "rules": {}
- }
- ]
-}
diff --git a/apps/api/eslint.config.cjs b/apps/api/eslint.config.cjs
new file mode 100644
index 000000000..043802a0d
--- /dev/null
+++ b/apps/api/eslint.config.cjs
@@ -0,0 +1,31 @@
+const baseConfig = require('../../eslint.config.cjs');
+
+module.exports = [
+ {
+ ignores: ['**/dist']
+ },
+ ...baseConfig,
+ {
+ rules: {}
+ },
+ {
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
+ // Override or add rules here
+ rules: {},
+ languageOptions: {
+ parserOptions: {
+ project: ['apps/api/tsconfig.*?.json']
+ }
+ }
+ },
+ {
+ files: ['**/*.ts', '**/*.tsx'],
+ // Override or add rules here
+ rules: {}
+ },
+ {
+ files: ['**/*.js', '**/*.jsx'],
+ // Override or add rules here
+ rules: {}
+ }
+];
diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts
index df369859b..aab4c0766 100644
--- a/apps/api/src/app/account/account.service.ts
+++ b/apps/api/src/app/account/account.service.ts
@@ -7,7 +7,13 @@ import { Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
-import { Account, Order, Platform, Prisma } from '@prisma/client';
+import {
+ Account,
+ AccountBalance,
+ Order,
+ Platform,
+ Prisma
+} from '@prisma/client';
import { Big } from 'big.js';
import { format } from 'date-fns';
import { groupBy } from 'lodash';
@@ -56,13 +62,19 @@ export class AccountService {
orderBy?: Prisma.AccountOrderByWithRelationInput;
}): Promise<
(Account & {
+ balances?: AccountBalance[];
Order?: Order[];
Platform?: Platform;
})[]
> {
const { include = {}, skip, take, cursor, where, orderBy } = params;
- include.balances = { orderBy: { date: 'desc' }, take: 1 };
+ const isBalancesIncluded = !!include.balances;
+
+ include.balances = {
+ orderBy: { date: 'desc' },
+ ...(isBalancesIncluded ? {} : { take: 1 })
+ };
const accounts = await this.prismaService.account.findMany({
cursor,
@@ -76,7 +88,9 @@ export class AccountService {
return accounts.map((account) => {
account = { ...account, balance: account.balances[0]?.value ?? 0 };
- delete account.balances;
+ if (!isBalancesIncluded) {
+ delete account.balances;
+ }
return account;
});
diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts
index 81c58ff03..d94cdd963 100644
--- a/apps/api/src/app/admin/admin.module.ts
+++ b/apps/api/src/app/admin/admin.module.ts
@@ -1,8 +1,8 @@
-import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
+import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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';
diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts
index fb6e90f5d..edb96a28e 100644
--- a/apps/api/src/app/admin/admin.service.ts
+++ b/apps/api/src/app/admin/admin.service.ts
@@ -1,7 +1,7 @@
-import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { environment } from '@ghostfolio/api/environments/environment';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.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';
@@ -10,7 +10,6 @@ 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_CURRENCY,
PROPERTY_CURRENCIES,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED
@@ -30,13 +29,13 @@ import {
EnhancedSymbolProfile,
Filter
} from '@ghostfolio/common/interfaces';
+import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { MarketDataPreset } from '@ghostfolio/common/types';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import {
AssetClass,
AssetSubClass,
- DataSource,
Prisma,
PrismaClient,
Property,
@@ -108,35 +107,29 @@ export class AdminService {
symbol
}: AssetProfileIdentifier) {
await this.marketDataService.deleteMany({ dataSource, symbol });
- await this.symbolProfileService.delete({ dataSource, symbol });
- }
- public async get(): Promise {
- const exchangeRates = this.exchangeRateDataService
- .getCurrencies()
- .filter((currency) => {
- return currency !== DEFAULT_CURRENCY;
- })
- .map((currency) => {
- const label1 = DEFAULT_CURRENCY;
- const label2 = currency;
+ const currency = getCurrencyFromSymbol(symbol);
+ const customCurrencies = (await this.propertyService.getByKey(
+ PROPERTY_CURRENCIES
+ )) as string[];
- return {
- label1,
- label2,
- dataSource:
- DataSource[
- this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
- ],
- symbol: `${label1}${label2}`,
- value: this.exchangeRateDataService.toCurrency(
- 1,
- DEFAULT_CURRENCY,
- currency
- )
- };
- });
+ if (customCurrencies.includes(currency)) {
+ const updatedCustomCurrencies = customCurrencies.filter(
+ (customCurrency) => {
+ return customCurrency !== currency;
+ }
+ );
+ await this.putSetting(
+ PROPERTY_CURRENCIES,
+ JSON.stringify(updatedCustomCurrencies)
+ );
+ } else {
+ await this.symbolProfileService.delete({ dataSource, symbol });
+ }
+ }
+
+ public async get(): Promise {
const [settings, transactionCount, userCount] = await Promise.all([
this.propertyService.get(),
this.prismaService.order.count(),
@@ -144,7 +137,6 @@ export class AdminService {
]);
return {
- exchangeRates,
settings,
transactionCount,
userCount,
@@ -259,7 +251,8 @@ export class AdminService {
},
scraperConfiguration: true,
sectors: true,
- symbol: true
+ symbol: true,
+ SymbolProfileOverrides: true
}
}),
this.prismaService.symbolProfile.count({ where })
@@ -313,11 +306,10 @@ export class AdminService {
name,
Order,
sectors,
- symbol
+ symbol,
+ SymbolProfileOverrides
}) => {
- const countriesCount = countries
- ? Object.keys(countries).length
- : 0;
+ let countriesCount = countries ? Object.keys(countries).length : 0;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
@@ -331,7 +323,34 @@ export class AdminService {
);
})?._count ?? 0;
- const sectorsCount = sectors ? Object.keys(sectors).length : 0;
+ let sectorsCount = sectors ? Object.keys(sectors).length : 0;
+
+ if (SymbolProfileOverrides) {
+ assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
+ assetSubClass =
+ SymbolProfileOverrides.assetSubClass ?? assetSubClass;
+
+ if (
+ (
+ SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
+ )?.length > 0
+ ) {
+ countriesCount = (
+ SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
+ ).length;
+ }
+
+ name = SymbolProfileOverrides.name ?? name;
+
+ if (
+ (SymbolProfileOverrides.sectors as unknown as Sector[])
+ ?.length > 0
+ ) {
+ sectorsCount = (
+ SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
+ ).length;
+ }
+ }
return {
assetClass,
diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts
index 6d097aeff..2a515bf43 100644
--- a/apps/api/src/app/app.module.ts
+++ b/apps/api/src/app/app.module.ts
@@ -29,13 +29,14 @@ import { AppController } from './app.controller';
import { AssetModule } from './asset/asset.module';
import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module';
-import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module';
import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
+import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module';
import { PublicModule } from './endpoints/public/public.module';
+import { TagsModule } from './endpoints/tags/tags.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module';
import { HealthModule } from './health/health.module';
@@ -49,7 +50,6 @@ 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({
@@ -63,7 +63,7 @@ import { UserModule } from './user/user.module';
AssetModule,
AuthDeviceModule,
AuthModule,
- BenchmarkModule,
+ BenchmarksModule,
BullModule.forRoot({
redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),
@@ -124,7 +124,7 @@ import { UserModule } from './user/user.module';
SitemapModule,
SubscriptionModule,
SymbolModule,
- TagModule,
+ TagsModule,
TwitterBotModule,
UserModule
],
diff --git a/apps/api/src/app/auth/web-auth.service.ts b/apps/api/src/app/auth/web-auth.service.ts
index 2f8dd1018..5678ef7fe 100644
--- a/apps/api/src/app/auth/web-auth.service.ts
+++ b/apps/api/src/app/auth/web-auth.service.ts
@@ -24,6 +24,7 @@ import {
verifyRegistrationResponse,
VerifyRegistrationResponseOpts
} from '@simplewebauthn/server';
+import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
import {
AssertionCredentialJSON,
@@ -54,10 +55,9 @@ export class WebAuthService {
const opts: GenerateRegistrationOptionsOpts = {
rpName: 'Ghostfolio',
rpID: this.rpID,
- userID: user.id,
+ userID: isoUint8Array.fromUTF8String(user.id),
userName: '',
timeout: 60000,
- attestationType: 'indirect',
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: false,
@@ -111,11 +111,17 @@ export class WebAuthService {
where: { userId: user.id }
});
if (registrationInfo && verified) {
- const { counter, credentialID, credentialPublicKey } = registrationInfo;
+ const {
+ credential: {
+ counter,
+ id: credentialId,
+ publicKey: credentialPublicKey
+ }
+ } = registrationInfo;
- let existingDevice = devices.find(
- (device) => device.credentialId === credentialID
- );
+ let existingDevice = devices.find((device) => {
+ return isoBase64URL.fromBuffer(device.credentialId) === credentialId;
+ });
if (!existingDevice) {
/**
@@ -123,7 +129,7 @@ export class WebAuthService {
*/
existingDevice = await this.deviceService.createAuthDevice({
counter,
- credentialId: Buffer.from(credentialID),
+ credentialId: Buffer.from(credentialId),
credentialPublicKey: Buffer.from(credentialPublicKey),
User: { connect: { id: user.id } }
});
@@ -148,9 +154,8 @@ export class WebAuthService {
const opts: GenerateAuthenticationOptionsOpts = {
allowCredentials: [
{
- id: device.credentialId,
- transports: ['internal'],
- type: 'public-key'
+ id: isoBase64URL.fromBuffer(device.credentialId),
+ transports: ['internal']
}
],
rpID: this.rpID,
@@ -187,10 +192,10 @@ export class WebAuthService {
let verification: VerifiedAuthenticationResponse;
try {
const opts: VerifyAuthenticationResponseOpts = {
- authenticator: {
- credentialID: device.credentialId,
- credentialPublicKey: device.credentialPublicKey,
- counter: device.counter
+ credential: {
+ counter: device.counter,
+ id: isoBase64URL.fromBuffer(device.credentialId),
+ publicKey: device.credentialPublicKey
},
expectedChallenge: `${user.authChallenge}`,
expectedOrigin: this.expectedOrigin,
diff --git a/apps/api/src/app/benchmark/benchmark.module.ts b/apps/api/src/app/benchmark/benchmark.module.ts
deleted file mode 100644
index 4c5f4d58e..000000000
--- a/apps/api/src/app/benchmark/benchmark.module.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
-import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
-import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
-import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
-import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
-import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
-import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
-import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
-import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
-import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
-import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
-
-import { Module } from '@nestjs/common';
-
-import { BenchmarkController } from './benchmark.controller';
-import { BenchmarkService } from './benchmark.service';
-
-@Module({
- controllers: [BenchmarkController],
- exports: [BenchmarkService],
- imports: [
- ConfigurationModule,
- DataProviderModule,
- ExchangeRateDataModule,
- MarketDataModule,
- PrismaModule,
- PropertyModule,
- RedisCacheModule,
- SymbolModule,
- SymbolProfileModule,
- TransformDataSourceInRequestModule,
- TransformDataSourceInResponseModule
- ],
- providers: [BenchmarkService]
-})
-export class BenchmarkModule {}
diff --git a/apps/api/src/app/endpoints/ai/ai.controller.ts b/apps/api/src/app/endpoints/ai/ai.controller.ts
index 981b26aa2..980d5607c 100644
--- a/apps/api/src/app/endpoints/ai/ai.controller.ts
+++ b/apps/api/src/app/endpoints/ai/ai.controller.ts
@@ -1,14 +1,22 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
+import { ApiService } from '@ghostfolio/api/services/api/api.service';
import {
DEFAULT_CURRENCY,
DEFAULT_LANGUAGE_CODE
} from '@ghostfolio/common/config';
import { AiPromptResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
-import type { RequestWithUser } from '@ghostfolio/common/types';
+import type { AiPromptMode, RequestWithUser } from '@ghostfolio/common/types';
-import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
+import {
+ Controller,
+ Get,
+ Inject,
+ Param,
+ Query,
+ UseGuards
+} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
@@ -18,14 +26,32 @@ import { AiService } from './ai.service';
export class AiController {
public constructor(
private readonly aiService: AiService,
+ private readonly apiService: ApiService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
- @Get('prompt')
+ @Get('prompt/:mode')
@HasPermission(permissions.readAiPrompt)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
- public async getPrompt(): Promise {
+ public async getPrompt(
+ @Param('mode') mode: AiPromptMode,
+ @Query('accounts') filterByAccounts?: string,
+ @Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
+ @Query('symbol') filterBySymbol?: string,
+ @Query('tags') filterByTags?: string
+ ): Promise {
+ const filters = this.apiService.buildFiltersFromQueryParams({
+ filterByAccounts,
+ filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
+ filterByTags
+ });
+
const prompt = await this.aiService.getPrompt({
+ filters,
+ mode,
impersonationId: undefined,
languageCode:
this.request.user.Settings.settings.language ?? DEFAULT_LANGUAGE_CODE,
diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts
index 5a30f3264..584f29956 100644
--- a/apps/api/src/app/endpoints/ai/ai.module.ts
+++ b/apps/api/src/app/endpoints/ai/ai.module.ts
@@ -7,6 +7,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
+import { ApiModule } from '@ghostfolio/api/services/api/api.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';
@@ -25,6 +26,7 @@ import { AiService } from './ai.service';
@Module({
controllers: [AiController],
imports: [
+ ApiModule,
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts
index 59dec6add..8807e67bf 100644
--- a/apps/api/src/app/endpoints/ai/ai.service.ts
+++ b/apps/api/src/app/endpoints/ai/ai.service.ts
@@ -1,4 +1,6 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
+import { Filter } from '@ghostfolio/common/interfaces';
+import type { AiPromptMode } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
@@ -7,17 +9,22 @@ export class AiService {
public constructor(private readonly portfolioService: PortfolioService) {}
public async getPrompt({
+ filters,
impersonationId,
languageCode,
+ mode,
userCurrency,
userId
}: {
+ filters?: Filter[];
impersonationId: string;
languageCode: string;
+ mode: AiPromptMode;
userCurrency: string;
userId: string;
}) {
const { holdings } = await this.portfolioService.getDetails({
+ filters,
impersonationId,
userId
});
@@ -43,6 +50,10 @@ export class AiService {
)
];
+ if (mode === 'portfolio') {
+ return holdingsTable.join('\n');
+ }
+
return [
`You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`,
...holdingsTable,
diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
similarity index 72%
rename from apps/api/src/app/benchmark/benchmark.controller.ts
rename to apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
index 66c268b9b..69383a30d 100644
--- a/apps/api/src/app/benchmark/benchmark.controller.ts
+++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
@@ -2,7 +2,10 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
+import { ApiService } from '@ghostfolio/api/services/api/api.service';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
+import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import type {
AssetProfileIdentifier,
BenchmarkMarketDataDetails,
@@ -16,6 +19,7 @@ import {
Controller,
Delete,
Get,
+ Headers,
HttpException,
Inject,
Param,
@@ -29,12 +33,14 @@ import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
-import { BenchmarkService } from './benchmark.service';
+import { BenchmarksService } from './benchmarks.service';
-@Controller('benchmark')
-export class BenchmarkController {
+@Controller('benchmarks')
+export class BenchmarksController {
public constructor(
+ private readonly apiService: ApiService,
private readonly benchmarkService: BenchmarkService,
+ private readonly benchmarksService: BenchmarksService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@@ -108,23 +114,43 @@ export class BenchmarkController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataForUser(
+ @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string,
- @Query('range') dateRange: DateRange = 'max'
+ @Query('range') dateRange: DateRange = 'max',
+ @Query('accounts') filterByAccounts?: string,
+ @Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
+ @Query('symbol') filterBySymbol?: string,
+ @Query('tags') filterByTags?: string,
+ @Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
): Promise {
const { endDate, startDate } = getIntervalFromDateRange(
dateRange,
new Date(startDateString)
);
- const userCurrency = this.request.user.Settings.settings.baseCurrency;
- return this.benchmarkService.getMarketDataForUser({
+ const filters = this.apiService.buildFiltersFromQueryParams({
+ filterByAccounts,
+ filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
+ filterByTags
+ });
+
+ const withExcludedAccounts = withExcludedAccountsParam === 'true';
+
+ return this.benchmarksService.getMarketDataForUser({
dataSource,
+ dateRange,
endDate,
+ filters,
+ impersonationId,
startDate,
symbol,
- userCurrency
+ withExcludedAccounts,
+ user: this.request.user
});
}
}
diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
new file mode 100644
index 000000000..a0f443621
--- /dev/null
+++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
@@ -0,0 +1,63 @@
+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 { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
+import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
+import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
+import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
+import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
+import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
+import { UserModule } from '@ghostfolio/api/app/user/user.module';
+import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
+import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
+import { ApiModule } from '@ghostfolio/api/services/api/api.module';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
+import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
+import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
+import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
+import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
+import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
+import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
+import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
+import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
+import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
+import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
+
+import { Module } from '@nestjs/common';
+
+import { BenchmarksController } from './benchmarks.controller';
+import { BenchmarksService } from './benchmarks.service';
+
+@Module({
+ controllers: [BenchmarksController],
+ imports: [
+ ApiModule,
+ ConfigurationModule,
+ DataProviderModule,
+ ExchangeRateDataModule,
+ ImpersonationModule,
+ MarketDataModule,
+ OrderModule,
+ PortfolioSnapshotQueueModule,
+ PrismaModule,
+ PropertyModule,
+ RedisCacheModule,
+ SymbolModule,
+ SymbolProfileModule,
+ TransformDataSourceInRequestModule,
+ TransformDataSourceInResponseModule,
+ UserModule
+ ],
+ providers: [
+ AccountBalanceService,
+ AccountService,
+ BenchmarkService,
+ BenchmarksService,
+ CurrentRateService,
+ MarketDataService,
+ PortfolioCalculatorFactory,
+ PortfolioService,
+ RulesService
+ ]
+})
+export class BenchmarksModule {}
diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
new file mode 100644
index 000000000..237f0d153
--- /dev/null
+++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
@@ -0,0 +1,163 @@
+import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
+import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
+import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
+import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
+import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
+import {
+ AssetProfileIdentifier,
+ BenchmarkMarketDataDetails,
+ Filter
+} from '@ghostfolio/common/interfaces';
+import { DateRange, UserWithSettings } from '@ghostfolio/common/types';
+
+import { Injectable, Logger } from '@nestjs/common';
+import { format, isSameDay } from 'date-fns';
+import { isNumber } from 'lodash';
+
+@Injectable()
+export class BenchmarksService {
+ public constructor(
+ private readonly benchmarkService: BenchmarkService,
+ private readonly exchangeRateDataService: ExchangeRateDataService,
+ private readonly marketDataService: MarketDataService,
+ private readonly portfolioService: PortfolioService,
+ private readonly symbolService: SymbolService
+ ) {}
+
+ public async getMarketDataForUser({
+ dataSource,
+ dateRange,
+ endDate = new Date(),
+ filters,
+ impersonationId,
+ startDate,
+ symbol,
+ user,
+ withExcludedAccounts
+ }: {
+ dateRange: DateRange;
+ endDate?: Date;
+ filters?: Filter[];
+ impersonationId: string;
+ startDate: Date;
+ user: UserWithSettings;
+ withExcludedAccounts?: boolean;
+ } & AssetProfileIdentifier): Promise {
+ const marketData: { date: string; value: number }[] = [];
+ const userCurrency = user.Settings.settings.baseCurrency;
+ const userId = user.id;
+
+ const { chart } = await this.portfolioService.getPerformance({
+ dateRange,
+ filters,
+ impersonationId,
+ userId,
+ withExcludedAccounts
+ });
+
+ const [currentSymbolItem, marketDataItems] = await Promise.all([
+ this.symbolService.get({
+ dataGatheringItem: {
+ dataSource,
+ symbol
+ }
+ }),
+ this.marketDataService.marketDataItems({
+ orderBy: {
+ date: 'asc'
+ },
+ where: {
+ dataSource,
+ symbol,
+ date: {
+ in: chart.map(({ date }) => {
+ return resetHours(parseDate(date));
+ })
+ }
+ }
+ })
+ ]);
+
+ const exchangeRates =
+ await this.exchangeRateDataService.getExchangeRatesByCurrency({
+ startDate,
+ currencies: [currentSymbolItem.currency],
+ targetCurrency: userCurrency
+ });
+
+ const exchangeRateAtStartDate =
+ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
+ format(startDate, DATE_FORMAT)
+ ];
+
+ const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
+ return isSameDay(date, startDate);
+ })?.marketPrice;
+
+ if (!marketPriceAtStartDate) {
+ Logger.error(
+ `No historical market data has been found for ${symbol} (${dataSource}) at ${format(
+ startDate,
+ DATE_FORMAT
+ )}`,
+ 'BenchmarkService'
+ );
+
+ return { marketData };
+ }
+
+ for (const marketDataItem of marketDataItems) {
+ const exchangeRate =
+ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
+ format(marketDataItem.date, DATE_FORMAT)
+ ];
+
+ const exchangeRateFactor =
+ isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
+ ? exchangeRate / exchangeRateAtStartDate
+ : 1;
+
+ marketData.push({
+ date: format(marketDataItem.date, DATE_FORMAT),
+ value:
+ marketPriceAtStartDate === 0
+ ? 0
+ : this.benchmarkService.calculateChangeInPercentage(
+ marketPriceAtStartDate,
+ marketDataItem.marketPrice * exchangeRateFactor
+ ) * 100
+ });
+ }
+
+ const includesEndDate = isSameDay(
+ parseDate(marketData.at(-1).date),
+ endDate
+ );
+
+ if (currentSymbolItem?.marketPrice && !includesEndDate) {
+ const exchangeRate =
+ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
+ format(endDate, DATE_FORMAT)
+ ];
+
+ const exchangeRateFactor =
+ isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
+ ? exchangeRate / exchangeRateAtStartDate
+ : 1;
+
+ marketData.push({
+ date: format(endDate, DATE_FORMAT),
+ value:
+ this.benchmarkService.calculateChangeInPercentage(
+ marketPriceAtStartDate,
+ currentSymbolItem.marketPrice * exchangeRateFactor
+ ) * 100
+ });
+ }
+
+ return {
+ marketData
+ };
+ }
+}
diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
index f3386f8a7..83c7317f0 100644
--- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
+++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
@@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { parseDate } from '@ghostfolio/common/helper';
import {
+ DataProviderGhostfolioAssetProfileResponse,
DataProviderGhostfolioStatusResponse,
DividendsResponse,
HistoricalResponse,
@@ -37,6 +38,41 @@ export class GhostfolioController {
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
+ @Get('asset-profile/:symbol')
+ @HasPermission(permissions.enableDataProviderGhostfolio)
+ @UseGuards(AuthGuard('api-key'), HasPermissionGuard)
+ public async getAssetProfile(
+ @Param('symbol') symbol: string
+ ): Promise {
+ const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
+
+ if (
+ this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
+ ) {
+ throw new HttpException(
+ getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
+ StatusCodes.TOO_MANY_REQUESTS
+ );
+ }
+
+ try {
+ const assetProfile = await this.ghostfolioService.getAssetProfile({
+ symbol
+ });
+
+ await this.ghostfolioService.incrementDailyRequests({
+ userId: this.request.user.id
+ });
+
+ return assetProfile;
+ } catch {
+ throw new HttpException(
+ getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
+ StatusCodes.INTERNAL_SERVER_ERROR
+ );
+ }
+ }
+
/**
* @deprecated
*/
diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
index 78685a61b..7281697bd 100644
--- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
+++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
@@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import {
+ GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@@ -15,6 +16,7 @@ import {
} from '@ghostfolio/common/config';
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
import {
+ DataProviderGhostfolioAssetProfileResponse,
DataProviderInfo,
DividendsResponse,
HistoricalResponse,
@@ -25,7 +27,7 @@ import {
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
-import { DataSource } from '@prisma/client';
+import { DataSource, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
@Injectable()
@@ -37,6 +39,44 @@ export class GhostfolioService {
private readonly propertyService: PropertyService
) {}
+ public async getAssetProfile({
+ requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
+ symbol
+ }: GetAssetProfileParams) {
+ let result: DataProviderGhostfolioAssetProfileResponse = {};
+
+ try {
+ const promises: Promise>[] = [];
+
+ for (const dataProviderService of this.getDataProviderServices()) {
+ promises.push(
+ dataProviderService
+ .getAssetProfile({
+ requestTimeout,
+ symbol
+ })
+ .then((assetProfile) => {
+ result = {
+ ...result,
+ ...assetProfile,
+ dataSource: DataSource.GHOSTFOLIO
+ };
+
+ return assetProfile;
+ })
+ );
+ }
+
+ await Promise.all(promises);
+
+ return result;
+ } catch (error) {
+ Logger.error(error, 'GhostfolioService');
+
+ throw error;
+ }
+ }
+
public async getDividends({
from,
granularity,
@@ -277,6 +317,7 @@ export class GhostfolioService {
});
results.items = filteredItems;
+
return results;
} catch (error) {
Logger.error(error, 'GhostfolioService');
diff --git a/apps/api/src/app/endpoints/market-data/market-data.controller.ts b/apps/api/src/app/endpoints/market-data/market-data.controller.ts
index b4aef807a..933e70e9d 100644
--- a/apps/api/src/app/endpoints/market-data/market-data.controller.ts
+++ b/apps/api/src/app/endpoints/market-data/market-data.controller.ts
@@ -1,6 +1,7 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
+import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { MarketDataDetailsResponse } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types';
@@ -42,7 +43,7 @@ export class MarketDataController {
{ dataSource, symbol }
]);
- if (!assetProfile) {
+ if (!assetProfile && !isCurrency(getCurrencyFromSymbol(symbol))) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
@@ -55,7 +56,7 @@ export class MarketDataController {
);
const canReadOwnAssetProfile =
- assetProfile.userId === this.request.user.id &&
+ assetProfile?.userId === this.request.user.id &&
hasPermission(
this.request.user.permissions,
permissions.readMarketDataOfOwnAssetProfile
diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts
index 7488e4201..bdcd86afb 100644
--- a/apps/api/src/app/endpoints/public/public.controller.ts
+++ b/apps/api/src/app/endpoints/public/public.controller.ts
@@ -57,7 +57,7 @@ export class PublicController {
}
const [
- { holdings, markets },
+ { createdAt, holdings, markets },
{ performance: performance1d },
{ performance: performanceMax },
{ performance: performanceYtd }
@@ -81,6 +81,7 @@ export class PublicController {
});
const publicPortfolioResponse: PublicPortfolioResponse = {
+ createdAt,
hasDetails,
markets,
alias: access.alias,
diff --git a/apps/api/src/app/endpoints/tags/create-tag.dto.ts b/apps/api/src/app/endpoints/tags/create-tag.dto.ts
new file mode 100644
index 000000000..09c29d25a
--- /dev/null
+++ b/apps/api/src/app/endpoints/tags/create-tag.dto.ts
@@ -0,0 +1,10 @@
+import { IsOptional, IsString } from 'class-validator';
+
+export class CreateTagDto {
+ @IsString()
+ name: string;
+
+ @IsOptional()
+ @IsString()
+ userId?: string;
+}
diff --git a/apps/api/src/app/tag/tag.controller.ts b/apps/api/src/app/endpoints/tags/tags.controller.ts
similarity index 62%
rename from apps/api/src/app/tag/tag.controller.ts
rename to apps/api/src/app/endpoints/tags/tags.controller.ts
index 6198a0bf5..bf216bb21 100644
--- a/apps/api/src/app/tag/tag.controller.ts
+++ b/apps/api/src/app/endpoints/tags/tags.controller.ts
@@ -1,6 +1,8 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
-import { permissions } from '@ghostfolio/common/permissions';
+import { TagService } from '@ghostfolio/api/services/tag/tag.service';
+import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+import { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@@ -8,41 +10,63 @@ import {
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(private readonly tagService: TagService) {}
-
- @Get()
- @HasPermission(permissions.readTags)
- @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
- public async getTags() {
- return this.tagService.getTagsWithActivityCount();
- }
+@Controller('tags')
+export class TagsController {
+ public constructor(
+ @Inject(REQUEST) private readonly request: RequestWithUser,
+ private readonly tagService: TagService
+ ) {}
@Post()
- @HasPermission(permissions.createTag)
- @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ @UseGuards(AuthGuard('jwt'))
public async createTag(@Body() data: CreateTagDto): Promise {
+ const canCreateOwnTag = hasPermission(
+ this.request.user.permissions,
+ permissions.createOwnTag
+ );
+
+ const canCreateTag = hasPermission(
+ this.request.user.permissions,
+ permissions.createTag
+ );
+
+ if (!canCreateOwnTag && !canCreateTag) {
+ throw new HttpException(
+ getReasonPhrase(StatusCodes.FORBIDDEN),
+ StatusCodes.FORBIDDEN
+ );
+ }
+
+ if (canCreateOwnTag && !canCreateTag) {
+ if (data.userId !== this.request.user.id) {
+ throw new HttpException(
+ getReasonPhrase(StatusCodes.BAD_REQUEST),
+ StatusCodes.BAD_REQUEST
+ );
+ }
+ }
+
return this.tagService.createTag(data);
}
- @HasPermission(permissions.updateTag)
- @Put(':id')
+ @Delete(':id')
+ @HasPermission(permissions.deleteTag)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
- public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) {
+ public async deleteTag(@Param('id') id: string) {
const originalTag = await this.tagService.getTag({
id
});
@@ -54,20 +78,20 @@ export class TagController {
);
}
- return this.tagService.updateTag({
- data: {
- ...data
- },
- where: {
- id
- }
- });
+ return this.tagService.deleteTag({ id });
}
- @Delete(':id')
- @HasPermission(permissions.deleteTag)
+ @Get()
+ @HasPermission(permissions.readTags)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
- public async deleteTag(@Param('id') id: string) {
+ public async getTags() {
+ return this.tagService.getTagsWithActivityCount();
+ }
+
+ @HasPermission(permissions.updateTag)
+ @Put(':id')
+ @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) {
const originalTag = await this.tagService.getTag({
id
});
@@ -79,6 +103,13 @@ export class TagController {
);
}
- return this.tagService.deleteTag({ id });
+ return this.tagService.updateTag({
+ data: {
+ ...data
+ },
+ where: {
+ id
+ }
+ });
}
}
diff --git a/apps/api/src/app/endpoints/tags/tags.module.ts b/apps/api/src/app/endpoints/tags/tags.module.ts
new file mode 100644
index 000000000..a8a2f1c51
--- /dev/null
+++ b/apps/api/src/app/endpoints/tags/tags.module.ts
@@ -0,0 +1,12 @@
+import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
+
+import { Module } from '@nestjs/common';
+
+import { TagsController } from './tags.controller';
+
+@Module({
+ controllers: [TagsController],
+ imports: [PrismaModule, TagModule]
+})
+export class TagsModule {}
diff --git a/apps/api/src/app/endpoints/tags/update-tag.dto.ts b/apps/api/src/app/endpoints/tags/update-tag.dto.ts
new file mode 100644
index 000000000..5ae42dcc6
--- /dev/null
+++ b/apps/api/src/app/endpoints/tags/update-tag.dto.ts
@@ -0,0 +1,13 @@
+import { IsOptional, IsString } from 'class-validator';
+
+export class UpdateTagDto {
+ @IsString()
+ id: string;
+
+ @IsString()
+ name: string;
+
+ @IsOptional()
+ @IsString()
+ userId?: string;
+}
diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts
index 551b3e489..d807132c9 100644
--- a/apps/api/src/app/export/export.controller.ts
+++ b/apps/api/src/app/export/export.controller.ts
@@ -21,10 +21,11 @@ export class ExportController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async export(
@Query('accounts') filterByAccounts?: string,
- @Query('activityIds') activityIds?: string[],
+ @Query('activityIds') filterByActivityIds?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise {
+ const activityIds = filterByActivityIds?.split(',') ?? [];
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
diff --git a/apps/api/src/app/export/export.module.ts b/apps/api/src/app/export/export.module.ts
index 048c60359..892a761cc 100644
--- a/apps/api/src/app/export/export.module.ts
+++ b/apps/api/src/app/export/export.module.ts
@@ -1,6 +1,7 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
+import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@@ -8,7 +9,7 @@ import { ExportController } from './export.controller';
import { ExportService } from './export.service';
@Module({
- imports: [AccountModule, ApiModule, OrderModule],
+ imports: [AccountModule, ApiModule, OrderModule, TagModule],
controllers: [ExportController],
providers: [ExportService]
})
diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts
index 1ff18ce9c..f0449dc14 100644
--- a/apps/api/src/app/export/export.service.ts
+++ b/apps/api/src/app/export/export.service.ts
@@ -1,15 +1,18 @@
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 { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { Filter, Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
+import { Platform } from '@prisma/client';
@Injectable()
export class ExportService {
public constructor(
private readonly accountService: AccountService,
- private readonly orderService: OrderService
+ private readonly orderService: OrderService,
+ private readonly tagService: TagService
) {}
public async export({
@@ -23,46 +26,96 @@ export class ExportService {
userCurrency: string;
userId: string;
}): Promise {
+ const platformsMap: { [platformId: string]: Platform } = {};
+
+ let { activities } = await this.orderService.getOrders({
+ filters,
+ userCurrency,
+ userId,
+ includeDrafts: true,
+ sortColumn: 'date',
+ sortDirection: 'asc',
+ withExcludedAccounts: true
+ });
+
+ if (activityIds?.length > 0) {
+ activities = activities.filter(({ id }) => {
+ return activityIds.includes(id);
+ });
+ }
+
const accounts = (
await this.accountService.accounts({
+ include: {
+ balances: true,
+ Platform: true
+ },
orderBy: {
name: 'asc'
},
where: { userId }
})
- ).map(
- ({ balance, comment, currency, id, isExcluded, name, platformId }) => {
- return {
+ )
+ .filter(({ id }) => {
+ return activities.length > 0
+ ? activities.some(({ accountId }) => {
+ return accountId === id;
+ })
+ : true;
+ })
+ .map(
+ ({
balance,
+ balances,
comment,
currency,
id,
isExcluded,
name,
+ Platform: platform,
platformId
- };
- }
- );
+ }) => {
+ if (platformId) {
+ platformsMap[platformId] = platform;
+ }
- let { activities } = await this.orderService.getOrders({
- filters,
- userCurrency,
- userId,
- includeDrafts: true,
- sortColumn: 'date',
- sortDirection: 'asc',
- withExcludedAccounts: true
- });
+ return {
+ balance,
+ balances: balances.map(({ date, value }) => {
+ return { date: date.toISOString(), value };
+ }),
+ comment,
+ currency,
+ id,
+ isExcluded,
+ name,
+ platformId
+ };
+ }
+ );
- if (activityIds) {
- activities = activities.filter((activity) => {
- return activityIds.includes(activity.id);
+ const tags = (await this.tagService.getTagsForUser(userId))
+ .filter(
+ ({ id, isUsed }) =>
+ isUsed &&
+ activities.some((activity) => {
+ return activity.tags.some(({ id: tagId }) => {
+ return tagId === id;
+ });
+ })
+ )
+ .map(({ id, name }) => {
+ return {
+ id,
+ name
+ };
});
- }
return {
meta: { date: new Date().toISOString(), version: environment.version },
accounts,
+ platforms: Object.values(platformsMap),
+ tags,
activities: activities.map(
({
accountId,
@@ -72,6 +125,7 @@ export class ExportService {
id,
quantity,
SymbolProfile,
+ tags: currentTags,
type,
unitPrice
}) => {
@@ -86,13 +140,12 @@ export class ExportService {
currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource,
date: date.toISOString(),
- symbol:
- type === 'FEE' ||
- type === 'INTEREST' ||
- type === 'ITEM' ||
- type === 'LIABILITY'
- ? SymbolProfile.name
- : SymbolProfile.symbol
+ symbol: ['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(type)
+ ? SymbolProfile.name
+ : SymbolProfile.symbol,
+ tags: currentTags.map(({ id: tagId }) => {
+ return tagId;
+ })
};
}
),
diff --git a/apps/api/src/app/health/health.controller.ts b/apps/api/src/app/health/health.controller.ts
index 62ee20419..6ff09825b 100644
--- a/apps/api/src/app/health/health.controller.ts
+++ b/apps/api/src/app/health/health.controller.ts
@@ -3,13 +3,14 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import {
Controller,
Get,
- HttpCode,
HttpException,
HttpStatus,
Param,
+ Res,
UseInterceptors
} from '@nestjs/common';
import { DataSource } from '@prisma/client';
+import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { HealthService } from './health.service';
@@ -19,9 +20,20 @@ export class HealthController {
public constructor(private readonly healthService: HealthService) {}
@Get()
- @HttpCode(HttpStatus.OK)
- public getHealth() {
- return { status: getReasonPhrase(StatusCodes.OK) };
+ public async getHealth(@Res() response: Response) {
+ const databaseServiceHealthy = await this.healthService.isDatabaseHealthy();
+ const redisCacheServiceHealthy =
+ await this.healthService.isRedisCacheHealthy();
+
+ if (databaseServiceHealthy && redisCacheServiceHealthy) {
+ return response
+ .status(HttpStatus.OK)
+ .json({ status: getReasonPhrase(StatusCodes.OK) });
+ } else {
+ return response
+ .status(HttpStatus.SERVICE_UNAVAILABLE)
+ .json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) });
+ }
}
@Get('data-enhancer/:name')
diff --git a/apps/api/src/app/health/health.module.ts b/apps/api/src/app/health/health.module.ts
index 6ed464401..b8c4d5810 100644
--- a/apps/api/src/app/health/health.module.ts
+++ b/apps/api/src/app/health/health.module.ts
@@ -1,6 +1,8 @@
+import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
+import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common';
@@ -12,6 +14,8 @@ import { HealthService } from './health.service';
imports: [
DataEnhancerModule,
DataProviderModule,
+ PropertyModule,
+ RedisCacheModule,
TransformDataSourceInRequestModule
],
providers: [HealthService]
diff --git a/apps/api/src/app/health/health.service.ts b/apps/api/src/app/health/health.service.ts
index b0c811392..f08f33a1e 100644
--- a/apps/api/src/app/health/health.service.ts
+++ b/apps/api/src/app/health/health.service.ts
@@ -1,5 +1,8 @@
+import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
+import { PropertyService } from '@ghostfolio/api/services/property/property.service';
+import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@@ -8,7 +11,9 @@ import { DataSource } from '@prisma/client';
export class HealthService {
public constructor(
private readonly dataEnhancerService: DataEnhancerService,
- private readonly dataProviderService: DataProviderService
+ private readonly dataProviderService: DataProviderService,
+ private readonly propertyService: PropertyService,
+ private readonly redisCacheService: RedisCacheService
) {}
public async hasResponseFromDataEnhancer(aName: string) {
@@ -18,4 +23,24 @@ export class HealthService {
public async hasResponseFromDataProvider(aDataSource: DataSource) {
return this.dataProviderService.checkQuote(aDataSource);
}
+
+ public async isDatabaseHealthy() {
+ try {
+ await this.propertyService.getByKey(PROPERTY_CURRENCIES);
+
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ public async isRedisCacheHealthy() {
+ try {
+ const isHealthy = await this.redisCacheService.isHealthy();
+
+ return isHealthy;
+ } catch {
+ return false;
+ }
+ }
}
diff --git a/apps/api/src/app/import/create-account-with-balances.dto.ts b/apps/api/src/app/import/create-account-with-balances.dto.ts
new file mode 100644
index 000000000..fd4b8df48
--- /dev/null
+++ b/apps/api/src/app/import/create-account-with-balances.dto.ts
@@ -0,0 +1,10 @@
+import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
+import { AccountBalance } from '@ghostfolio/common/interfaces';
+
+import { IsArray, IsOptional } from 'class-validator';
+
+export class CreateAccountWithBalancesDto extends CreateAccountDto {
+ @IsArray()
+ @IsOptional()
+ balances?: AccountBalance;
+}
diff --git a/apps/api/src/app/import/import-data.dto.ts b/apps/api/src/app/import/import-data.dto.ts
index 715766821..207c8152b 100644
--- a/apps/api/src/app/import/import-data.dto.ts
+++ b/apps/api/src/app/import/import-data.dto.ts
@@ -1,15 +1,16 @@
-import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Type } from 'class-transformer';
import { IsArray, IsOptional, ValidateNested } from 'class-validator';
+import { CreateAccountWithBalancesDto } from './create-account-with-balances.dto';
+
export class ImportDataDto {
@IsOptional()
@IsArray()
- @Type(() => CreateAccountDto)
+ @Type(() => CreateAccountWithBalancesDto)
@ValidateNested({ each: true })
- accounts: CreateAccountDto[];
+ accounts: CreateAccountWithBalancesDto[];
@IsArray()
@Type(() => CreateOrderDto)
diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts
index 3b7290b43..698d13e2b 100644
--- a/apps/api/src/app/import/import.service.ts
+++ b/apps/api/src/app/import/import.service.ts
@@ -30,7 +30,7 @@ import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns';
-import { uniqBy } from 'lodash';
+import { isNumber, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
@@ -293,6 +293,7 @@ export class ImportService {
assetSubClass,
countries,
createdAt,
+ cusip,
dataSource,
figi,
figiComposite,
@@ -328,7 +329,7 @@ export class ImportService {
date
);
- if (!unitPrice) {
+ if (!isNumber(unitPrice)) {
throw new Error(
`activities.${index} historical exchange rate at ${format(
date,
@@ -367,6 +368,7 @@ export class ImportService {
assetSubClass,
countries,
createdAt,
+ cusip,
dataSource,
figi,
figiComposite,
diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts
index 7903ac397..d7a5ed641 100644
--- a/apps/api/src/app/info/info.module.ts
+++ b/apps/api/src/app/info/info.module.ts
@@ -1,8 +1,8 @@
-import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
+import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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';
diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts
index 1af41520d..780860232 100644
--- a/apps/api/src/app/info/info.service.ts
+++ b/apps/api/src/app/info/info.service.ts
@@ -1,7 +1,7 @@
-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 { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts
index e80811351..a26099e9d 100644
--- a/apps/api/src/app/order/order.service.ts
+++ b/apps/api/src/app/order/order.service.ts
@@ -63,14 +63,14 @@ export class OrderService {
}
});
- return Promise.all(
+ await Promise.all(
orders.map(({ id }) =>
this.prismaService.order.update({
data: {
tags: {
// The set operation replaces all existing connections with the provided ones
- set: tags.map(({ id }) => {
- return { id };
+ set: tags.map((tag) => {
+ return { id: tag.id };
})
}
},
@@ -78,6 +78,13 @@ export class OrderService {
})
)
);
+
+ this.eventEmitter.emit(
+ PortfolioChangedEvent.getName(),
+ new PortfolioChangedEvent({
+ userId
+ })
+ );
}
public async createOrder(
diff --git a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
index e54f63422..fab15e6e7 100644
--- a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
@@ -5,7 +5,7 @@ import {
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
-export class MWRPortfolioCalculator extends PortfolioCalculator {
+export class MwrPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.');
}
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
index 18738373e..6cc5edeaf 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
@@ -8,12 +8,14 @@ import { Filter, HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
-import { MWRPortfolioCalculator } from './mwr/portfolio-calculator';
+import { MwrPortfolioCalculator } from './mwr/portfolio-calculator';
import { PortfolioCalculator } from './portfolio-calculator';
-import { TWRPortfolioCalculator } from './twr/portfolio-calculator';
+import { RoaiPortfolioCalculator } from './roai/portfolio-calculator';
+import { TwrPortfolioCalculator } from './twr/portfolio-calculator';
export enum PerformanceCalculationType {
MWR = 'MWR', // Money-Weighted Rate of Return
+ ROAI = 'ROAI', // Return on Average Investment
TWR = 'TWR' // Time-Weighted Rate of Return
}
@@ -44,7 +46,7 @@ export class PortfolioCalculatorFactory {
}): PortfolioCalculator {
switch (calculationType) {
case PerformanceCalculationType.MWR:
- return new MWRPortfolioCalculator({
+ return new MwrPortfolioCalculator({
accountBalanceItems,
activities,
currency,
@@ -56,15 +58,28 @@ export class PortfolioCalculatorFactory {
portfolioSnapshotService: this.portfolioSnapshotService,
redisCacheService: this.redisCacheService
});
- case PerformanceCalculationType.TWR:
- return new TWRPortfolioCalculator({
+ case PerformanceCalculationType.ROAI:
+ return new RoaiPortfolioCalculator({
accountBalanceItems,
activities,
currency,
+ filters,
+ userId,
+ configurationService: this.configurationService,
currentRateService: this.currentRateService,
+ exchangeRateDataService: this.exchangeRateDataService,
+ portfolioSnapshotService: this.portfolioSnapshotService,
+ redisCacheService: this.redisCacheService
+ });
+ case PerformanceCalculationType.TWR:
+ return new TwrPortfolioCalculator({
+ accountBalanceItems,
+ activities,
+ currency,
filters,
userId,
configurationService: this.configurationService,
+ currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
portfolioSnapshotService: this.portfolioSnapshotService,
redisCacheService: this.redisCacheService
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
index 2f8a9f0c9..52d57230b 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
@@ -49,7 +49,7 @@ import {
min,
subDays
} from 'date-fns';
-import { isNumber, sortBy, sum, uniq, uniqBy } from 'lodash';
+import { isNumber, sortBy, sum, uniqBy } from 'lodash';
export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false;
@@ -175,6 +175,8 @@ export abstract class PortfolioCalculator {
if (!transactionPoints.length) {
return {
+ activitiesCount: 0,
+ createdAt: new Date(),
currentValueInBaseCurrency: new Big(0),
errors: [],
hasErrors: false,
@@ -220,7 +222,7 @@ export abstract class PortfolioCalculator {
const exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
- currencies: uniq(Object.values(currencies)),
+ currencies: Array.from(new Set(Object.values(currencies))),
endDate: endOfDay(this.endDate),
startDate: this.startDate,
targetCurrency: this.currency
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
similarity index 96%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
index deb3cd72f..e157e2d26 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
@@ -137,7 +137,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
index 7b4d53b2f..a1650ea82 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
@@ -122,7 +122,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
index 002cbd5e9..63a4d77b4 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
@@ -107,7 +107,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
index 640de3985..2853e3d87 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
@@ -136,7 +136,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
index 6f030a73d..b96e4f540 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
@@ -107,7 +107,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
index 4e25c17f7..b3793a5b4 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
@@ -120,7 +120,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-item.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-item.spec.ts
index 7fc5c526d..d226fe6f8 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-item.spec.ts
@@ -107,7 +107,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
similarity index 98%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
index 5fa90e94c..569212b9a 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
@@ -107,7 +107,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
index 543985424..4c54ba7aa 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
@@ -135,7 +135,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts
similarity index 98%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts
index 84898490f..77e3f6157 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts
@@ -84,7 +84,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities: [],
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
similarity index 99%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
index 37f22e2f6..84bcc5bc1 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
@@ -116,7 +116,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
similarity index 96%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
index caf196f54..937fd8b48 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
@@ -116,7 +116,7 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
similarity index 100%
rename from apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts
rename to apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
new file mode 100644
index 000000000..5b918fa03
--- /dev/null
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
@@ -0,0 +1,969 @@
+import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
+import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
+import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
+import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
+import { DATE_FORMAT } from '@ghostfolio/common/helper';
+import {
+ AssetProfileIdentifier,
+ SymbolMetrics
+} from '@ghostfolio/common/interfaces';
+import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
+import { DateRange } from '@ghostfolio/common/types';
+
+import { Logger } from '@nestjs/common';
+import { Big } from 'big.js';
+import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns';
+import { cloneDeep, sortBy } from 'lodash';
+
+export class RoaiPortfolioCalculator extends PortfolioCalculator {
+ private chartDates: string[];
+
+ protected calculateOverallPerformance(
+ positions: TimelinePosition[]
+ ): PortfolioSnapshot {
+ let currentValueInBaseCurrency = new Big(0);
+ let grossPerformance = new Big(0);
+ let grossPerformanceWithCurrencyEffect = new Big(0);
+ let hasErrors = false;
+ let netPerformance = new Big(0);
+ let totalFeesWithCurrencyEffect = new Big(0);
+ const totalInterestWithCurrencyEffect = new Big(0);
+ let totalInvestment = new Big(0);
+ let totalInvestmentWithCurrencyEffect = new Big(0);
+ let totalTimeWeightedInvestment = new Big(0);
+ let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
+
+ for (const currentPosition of positions) {
+ if (currentPosition.feeInBaseCurrency) {
+ totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
+ currentPosition.feeInBaseCurrency
+ );
+ }
+
+ if (currentPosition.valueInBaseCurrency) {
+ currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
+ currentPosition.valueInBaseCurrency
+ );
+ } else {
+ hasErrors = true;
+ }
+
+ if (currentPosition.investment) {
+ totalInvestment = totalInvestment.plus(currentPosition.investment);
+
+ totalInvestmentWithCurrencyEffect =
+ totalInvestmentWithCurrencyEffect.plus(
+ currentPosition.investmentWithCurrencyEffect
+ );
+ } else {
+ hasErrors = true;
+ }
+
+ if (currentPosition.grossPerformance) {
+ grossPerformance = grossPerformance.plus(
+ currentPosition.grossPerformance
+ );
+
+ grossPerformanceWithCurrencyEffect =
+ grossPerformanceWithCurrencyEffect.plus(
+ currentPosition.grossPerformanceWithCurrencyEffect
+ );
+
+ netPerformance = netPerformance.plus(currentPosition.netPerformance);
+ } else if (!currentPosition.quantity.eq(0)) {
+ hasErrors = true;
+ }
+
+ if (currentPosition.timeWeightedInvestment) {
+ totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus(
+ currentPosition.timeWeightedInvestment
+ );
+
+ totalTimeWeightedInvestmentWithCurrencyEffect =
+ totalTimeWeightedInvestmentWithCurrencyEffect.plus(
+ currentPosition.timeWeightedInvestmentWithCurrencyEffect
+ );
+ } else if (!currentPosition.quantity.eq(0)) {
+ Logger.warn(
+ `Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
+ 'PortfolioCalculator'
+ );
+
+ hasErrors = true;
+ }
+ }
+
+ return {
+ currentValueInBaseCurrency,
+ hasErrors,
+ positions,
+ totalFeesWithCurrencyEffect,
+ totalInterestWithCurrencyEffect,
+ totalInvestment,
+ totalInvestmentWithCurrencyEffect,
+ activitiesCount: this.activities.filter(({ type }) => {
+ return ['BUY', 'SELL'].includes(type);
+ }).length,
+ createdAt: new Date(),
+ errors: [],
+ historicalData: [],
+ totalLiabilitiesWithCurrencyEffect: new Big(0),
+ totalValuablesWithCurrencyEffect: new Big(0)
+ };
+ }
+
+ protected getSymbolMetrics({
+ chartDateMap,
+ dataSource,
+ end,
+ exchangeRates,
+ marketSymbolMap,
+ start,
+ symbol
+ }: {
+ chartDateMap?: { [date: string]: boolean };
+ end: Date;
+ exchangeRates: { [dateString: string]: number };
+ marketSymbolMap: {
+ [date: string]: { [symbol: string]: Big };
+ };
+ start: Date;
+ } & AssetProfileIdentifier): SymbolMetrics {
+ const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
+ const currentValues: { [date: string]: Big } = {};
+ const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
+ let fees = new Big(0);
+ let feesAtStartDate = new Big(0);
+ let feesAtStartDateWithCurrencyEffect = new Big(0);
+ let feesWithCurrencyEffect = new Big(0);
+ let grossPerformance = new Big(0);
+ let grossPerformanceWithCurrencyEffect = new Big(0);
+ let grossPerformanceAtStartDate = new Big(0);
+ let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0);
+ let grossPerformanceFromSells = new Big(0);
+ let grossPerformanceFromSellsWithCurrencyEffect = new Big(0);
+ let initialValue: Big;
+ let initialValueWithCurrencyEffect: Big;
+ let investmentAtStartDate: Big;
+ let investmentAtStartDateWithCurrencyEffect: Big;
+ const investmentValuesAccumulated: { [date: string]: Big } = {};
+ const investmentValuesAccumulatedWithCurrencyEffect: {
+ [date: string]: Big;
+ } = {};
+ const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {};
+ let lastAveragePrice = new Big(0);
+ let lastAveragePriceWithCurrencyEffect = new Big(0);
+ const netPerformanceValues: { [date: string]: Big } = {};
+ const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {};
+ const timeWeightedInvestmentValues: { [date: string]: Big } = {};
+
+ const timeWeightedInvestmentValuesWithCurrencyEffect: {
+ [date: string]: Big;
+ } = {};
+
+ const totalAccountBalanceInBaseCurrency = new Big(0);
+ let totalDividend = new Big(0);
+ let totalDividendInBaseCurrency = new Big(0);
+ let totalInterest = new Big(0);
+ let totalInterestInBaseCurrency = new Big(0);
+ let totalInvestment = new Big(0);
+ let totalInvestmentFromBuyTransactions = new Big(0);
+ let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
+ let totalInvestmentWithCurrencyEffect = new Big(0);
+ let totalLiabilities = new Big(0);
+ let totalLiabilitiesInBaseCurrency = new Big(0);
+ let totalQuantityFromBuyTransactions = new Big(0);
+ let totalUnits = new Big(0);
+ let totalValuables = new Big(0);
+ let totalValuablesInBaseCurrency = new Big(0);
+ let valueAtStartDate: Big;
+ let valueAtStartDateWithCurrencyEffect: Big;
+
+ // Clone orders to keep the original values in this.orders
+ let orders: PortfolioOrderItem[] = cloneDeep(
+ this.activities.filter(({ SymbolProfile }) => {
+ return SymbolProfile.symbol === symbol;
+ })
+ );
+
+ if (orders.length <= 0) {
+ return {
+ currentValues: {},
+ currentValuesWithCurrencyEffect: {},
+ feesWithCurrencyEffect: new Big(0),
+ grossPerformance: new Big(0),
+ grossPerformancePercentage: new Big(0),
+ grossPerformancePercentageWithCurrencyEffect: new Big(0),
+ grossPerformanceWithCurrencyEffect: new Big(0),
+ hasErrors: false,
+ initialValue: new Big(0),
+ initialValueWithCurrencyEffect: new Big(0),
+ investmentValuesAccumulated: {},
+ investmentValuesAccumulatedWithCurrencyEffect: {},
+ investmentValuesWithCurrencyEffect: {},
+ netPerformance: new Big(0),
+ netPerformancePercentage: new Big(0),
+ netPerformancePercentageWithCurrencyEffectMap: {},
+ netPerformanceValues: {},
+ netPerformanceValuesWithCurrencyEffect: {},
+ netPerformanceWithCurrencyEffectMap: {},
+ timeWeightedInvestment: new Big(0),
+ timeWeightedInvestmentValues: {},
+ timeWeightedInvestmentValuesWithCurrencyEffect: {},
+ timeWeightedInvestmentWithCurrencyEffect: new Big(0),
+ totalAccountBalanceInBaseCurrency: new Big(0),
+ totalDividend: new Big(0),
+ totalDividendInBaseCurrency: new Big(0),
+ totalInterest: new Big(0),
+ totalInterestInBaseCurrency: new Big(0),
+ totalInvestment: new Big(0),
+ totalInvestmentWithCurrencyEffect: new Big(0),
+ totalLiabilities: new Big(0),
+ totalLiabilitiesInBaseCurrency: new Big(0),
+ totalValuables: new Big(0),
+ totalValuablesInBaseCurrency: new Big(0)
+ };
+ }
+
+ const dateOfFirstTransaction = new Date(orders[0].date);
+
+ const endDateString = format(end, DATE_FORMAT);
+ const startDateString = format(start, DATE_FORMAT);
+
+ const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol];
+ const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol];
+
+ if (
+ !unitPriceAtEndDate ||
+ (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start))
+ ) {
+ return {
+ currentValues: {},
+ currentValuesWithCurrencyEffect: {},
+ feesWithCurrencyEffect: new Big(0),
+ grossPerformance: new Big(0),
+ grossPerformancePercentage: new Big(0),
+ grossPerformancePercentageWithCurrencyEffect: new Big(0),
+ grossPerformanceWithCurrencyEffect: new Big(0),
+ hasErrors: true,
+ initialValue: new Big(0),
+ initialValueWithCurrencyEffect: new Big(0),
+ investmentValuesAccumulated: {},
+ investmentValuesAccumulatedWithCurrencyEffect: {},
+ investmentValuesWithCurrencyEffect: {},
+ netPerformance: new Big(0),
+ netPerformancePercentage: new Big(0),
+ netPerformancePercentageWithCurrencyEffectMap: {},
+ netPerformanceWithCurrencyEffectMap: {},
+ netPerformanceValues: {},
+ netPerformanceValuesWithCurrencyEffect: {},
+ timeWeightedInvestment: new Big(0),
+ timeWeightedInvestmentValues: {},
+ timeWeightedInvestmentValuesWithCurrencyEffect: {},
+ timeWeightedInvestmentWithCurrencyEffect: new Big(0),
+ totalAccountBalanceInBaseCurrency: new Big(0),
+ totalDividend: new Big(0),
+ totalDividendInBaseCurrency: new Big(0),
+ totalInterest: new Big(0),
+ totalInterestInBaseCurrency: new Big(0),
+ totalInvestment: new Big(0),
+ totalInvestmentWithCurrencyEffect: new Big(0),
+ totalLiabilities: new Big(0),
+ totalLiabilitiesInBaseCurrency: new Big(0),
+ totalValuables: new Big(0),
+ totalValuablesInBaseCurrency: new Big(0)
+ };
+ }
+
+ // Add a synthetic order at the start and the end date
+ orders.push({
+ date: startDateString,
+ fee: new Big(0),
+ feeInBaseCurrency: new Big(0),
+ itemType: 'start',
+ quantity: new Big(0),
+ SymbolProfile: {
+ dataSource,
+ symbol
+ },
+ type: 'BUY',
+ unitPrice: unitPriceAtStartDate
+ });
+
+ orders.push({
+ date: endDateString,
+ fee: new Big(0),
+ feeInBaseCurrency: new Big(0),
+ itemType: 'end',
+ SymbolProfile: {
+ dataSource,
+ symbol
+ },
+ quantity: new Big(0),
+ type: 'BUY',
+ unitPrice: unitPriceAtEndDate
+ });
+
+ let lastUnitPrice: Big;
+
+ const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {};
+
+ for (const order of orders) {
+ ordersByDate[order.date] = ordersByDate[order.date] ?? [];
+ ordersByDate[order.date].push(order);
+ }
+
+ if (!this.chartDates) {
+ this.chartDates = Object.keys(chartDateMap).sort();
+ }
+
+ for (const dateString of this.chartDates) {
+ if (dateString < startDateString) {
+ continue;
+ } else if (dateString > endDateString) {
+ break;
+ }
+
+ if (ordersByDate[dateString]?.length > 0) {
+ for (const order of ordersByDate[dateString]) {
+ order.unitPriceFromMarketData =
+ marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice;
+ }
+ } else {
+ orders.push({
+ date: dateString,
+ fee: new Big(0),
+ feeInBaseCurrency: new Big(0),
+ quantity: new Big(0),
+ SymbolProfile: {
+ dataSource,
+ symbol
+ },
+ type: 'BUY',
+ unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice,
+ unitPriceFromMarketData:
+ marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice
+ });
+ }
+
+ const lastOrder = orders.at(-1);
+
+ lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice;
+ }
+
+ // Sort orders so that the start and end placeholder order are at the correct
+ // position
+ orders = sortBy(orders, ({ date, itemType }) => {
+ let sortIndex = new Date(date);
+
+ if (itemType === 'end') {
+ sortIndex = addMilliseconds(sortIndex, 1);
+ } else if (itemType === 'start') {
+ sortIndex = addMilliseconds(sortIndex, -1);
+ }
+
+ return sortIndex.getTime();
+ });
+
+ const indexOfStartOrder = orders.findIndex(({ itemType }) => {
+ return itemType === 'start';
+ });
+
+ const indexOfEndOrder = orders.findIndex(({ itemType }) => {
+ return itemType === 'end';
+ });
+
+ let totalInvestmentDays = 0;
+ let sumOfTimeWeightedInvestments = new Big(0);
+ let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0);
+
+ for (let i = 0; i < orders.length; i += 1) {
+ const order = orders[i];
+
+ if (PortfolioCalculator.ENABLE_LOGGING) {
+ console.log();
+ console.log();
+ console.log(
+ i + 1,
+ order.date,
+ order.type,
+ order.itemType ? `(${order.itemType})` : ''
+ );
+ }
+
+ const exchangeRateAtOrderDate = exchangeRates[order.date];
+
+ if (order.type === 'DIVIDEND') {
+ const dividend = order.quantity.mul(order.unitPrice);
+
+ totalDividend = totalDividend.plus(dividend);
+ totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
+ dividend.mul(exchangeRateAtOrderDate ?? 1)
+ );
+ } else if (order.type === 'INTEREST') {
+ const interest = order.quantity.mul(order.unitPrice);
+
+ totalInterest = totalInterest.plus(interest);
+ totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
+ interest.mul(exchangeRateAtOrderDate ?? 1)
+ );
+ } else if (order.type === 'ITEM') {
+ const valuables = order.quantity.mul(order.unitPrice);
+
+ totalValuables = totalValuables.plus(valuables);
+ totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus(
+ valuables.mul(exchangeRateAtOrderDate ?? 1)
+ );
+ } else if (order.type === 'LIABILITY') {
+ const liabilities = order.quantity.mul(order.unitPrice);
+
+ totalLiabilities = totalLiabilities.plus(liabilities);
+ totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
+ liabilities.mul(exchangeRateAtOrderDate ?? 1)
+ );
+ }
+
+ if (order.itemType === 'start') {
+ // Take the unit price of the order as the market price if there are no
+ // orders of this symbol before the start date
+ order.unitPrice =
+ indexOfStartOrder === 0
+ ? orders[i + 1]?.unitPrice
+ : unitPriceAtStartDate;
+ }
+
+ if (order.fee) {
+ order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1);
+ order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul(
+ exchangeRateAtOrderDate ?? 1
+ );
+ }
+
+ const unitPrice = ['BUY', 'SELL'].includes(order.type)
+ ? order.unitPrice
+ : order.unitPriceFromMarketData;
+
+ if (unitPrice) {
+ order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1);
+
+ order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul(
+ exchangeRateAtOrderDate ?? 1
+ );
+ }
+
+ const valueOfInvestmentBeforeTransaction = totalUnits.mul(
+ order.unitPriceInBaseCurrency
+ );
+
+ const valueOfInvestmentBeforeTransactionWithCurrencyEffect =
+ totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect);
+
+ if (!investmentAtStartDate && i >= indexOfStartOrder) {
+ investmentAtStartDate = totalInvestment ?? new Big(0);
+
+ investmentAtStartDateWithCurrencyEffect =
+ totalInvestmentWithCurrencyEffect ?? new Big(0);
+
+ valueAtStartDate = valueOfInvestmentBeforeTransaction;
+
+ valueAtStartDateWithCurrencyEffect =
+ valueOfInvestmentBeforeTransactionWithCurrencyEffect;
+ }
+
+ let transactionInvestment = new Big(0);
+ let transactionInvestmentWithCurrencyEffect = new Big(0);
+
+ if (order.type === 'BUY') {
+ transactionInvestment = order.quantity
+ .mul(order.unitPriceInBaseCurrency)
+ .mul(getFactor(order.type));
+
+ transactionInvestmentWithCurrencyEffect = order.quantity
+ .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
+ .mul(getFactor(order.type));
+
+ totalQuantityFromBuyTransactions =
+ totalQuantityFromBuyTransactions.plus(order.quantity);
+
+ totalInvestmentFromBuyTransactions =
+ totalInvestmentFromBuyTransactions.plus(transactionInvestment);
+
+ totalInvestmentFromBuyTransactionsWithCurrencyEffect =
+ totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
+ transactionInvestmentWithCurrencyEffect
+ );
+ } else if (order.type === 'SELL') {
+ if (totalUnits.gt(0)) {
+ transactionInvestment = totalInvestment
+ .div(totalUnits)
+ .mul(order.quantity)
+ .mul(getFactor(order.type));
+ transactionInvestmentWithCurrencyEffect =
+ totalInvestmentWithCurrencyEffect
+ .div(totalUnits)
+ .mul(order.quantity)
+ .mul(getFactor(order.type));
+ }
+ }
+
+ if (PortfolioCalculator.ENABLE_LOGGING) {
+ console.log('order.quantity', order.quantity.toNumber());
+ console.log('transactionInvestment', transactionInvestment.toNumber());
+
+ console.log(
+ 'transactionInvestmentWithCurrencyEffect',
+ transactionInvestmentWithCurrencyEffect.toNumber()
+ );
+ }
+
+ const totalInvestmentBeforeTransaction = totalInvestment;
+
+ const totalInvestmentBeforeTransactionWithCurrencyEffect =
+ totalInvestmentWithCurrencyEffect;
+
+ totalInvestment = totalInvestment.plus(transactionInvestment);
+
+ totalInvestmentWithCurrencyEffect =
+ totalInvestmentWithCurrencyEffect.plus(
+ transactionInvestmentWithCurrencyEffect
+ );
+
+ if (i >= indexOfStartOrder && !initialValue) {
+ if (
+ i === indexOfStartOrder &&
+ !valueOfInvestmentBeforeTransaction.eq(0)
+ ) {
+ initialValue = valueOfInvestmentBeforeTransaction;
+
+ initialValueWithCurrencyEffect =
+ valueOfInvestmentBeforeTransactionWithCurrencyEffect;
+ } else if (transactionInvestment.gt(0)) {
+ initialValue = transactionInvestment;
+
+ initialValueWithCurrencyEffect =
+ transactionInvestmentWithCurrencyEffect;
+ }
+ }
+
+ fees = fees.plus(order.feeInBaseCurrency ?? 0);
+
+ feesWithCurrencyEffect = feesWithCurrencyEffect.plus(
+ order.feeInBaseCurrencyWithCurrencyEffect ?? 0
+ );
+
+ totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
+
+ const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
+
+ const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
+ order.unitPriceInBaseCurrencyWithCurrencyEffect
+ );
+
+ const grossPerformanceFromSell =
+ order.type === 'SELL'
+ ? order.unitPriceInBaseCurrency
+ .minus(lastAveragePrice)
+ .mul(order.quantity)
+ : new Big(0);
+
+ const grossPerformanceFromSellWithCurrencyEffect =
+ order.type === 'SELL'
+ ? order.unitPriceInBaseCurrencyWithCurrencyEffect
+ .minus(lastAveragePriceWithCurrencyEffect)
+ .mul(order.quantity)
+ : new Big(0);
+
+ grossPerformanceFromSells = grossPerformanceFromSells.plus(
+ grossPerformanceFromSell
+ );
+
+ grossPerformanceFromSellsWithCurrencyEffect =
+ grossPerformanceFromSellsWithCurrencyEffect.plus(
+ grossPerformanceFromSellWithCurrencyEffect
+ );
+
+ lastAveragePrice = totalQuantityFromBuyTransactions.eq(0)
+ ? new Big(0)
+ : totalInvestmentFromBuyTransactions.div(
+ totalQuantityFromBuyTransactions
+ );
+
+ lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq(
+ 0
+ )
+ ? new Big(0)
+ : totalInvestmentFromBuyTransactionsWithCurrencyEffect.div(
+ totalQuantityFromBuyTransactions
+ );
+
+ if (PortfolioCalculator.ENABLE_LOGGING) {
+ console.log(
+ 'grossPerformanceFromSells',
+ grossPerformanceFromSells.toNumber()
+ );
+ console.log(
+ 'grossPerformanceFromSellWithCurrencyEffect',
+ grossPerformanceFromSellWithCurrencyEffect.toNumber()
+ );
+ }
+
+ const newGrossPerformance = valueOfInvestment
+ .minus(totalInvestment)
+ .plus(grossPerformanceFromSells);
+
+ const newGrossPerformanceWithCurrencyEffect =
+ valueOfInvestmentWithCurrencyEffect
+ .minus(totalInvestmentWithCurrencyEffect)
+ .plus(grossPerformanceFromSellsWithCurrencyEffect);
+
+ grossPerformance = newGrossPerformance;
+
+ grossPerformanceWithCurrencyEffect =
+ newGrossPerformanceWithCurrencyEffect;
+
+ if (order.itemType === 'start') {
+ feesAtStartDate = fees;
+ feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
+ grossPerformanceAtStartDate = grossPerformance;
+
+ grossPerformanceAtStartDateWithCurrencyEffect =
+ grossPerformanceWithCurrencyEffect;
+ }
+
+ if (i > indexOfStartOrder) {
+ // Only consider periods with an investment for the calculation of
+ // the time weighted investment
+ if (
+ valueOfInvestmentBeforeTransaction.gt(0) &&
+ ['BUY', 'SELL'].includes(order.type)
+ ) {
+ // Calculate the number of days since the previous order
+ const orderDate = new Date(order.date);
+ const previousOrderDate = new Date(orders[i - 1].date);
+
+ let daysSinceLastOrder = differenceInDays(
+ orderDate,
+ previousOrderDate
+ );
+ if (daysSinceLastOrder <= 0) {
+ // The time between two activities on the same day is unknown
+ // -> Set it to the smallest floating point number greater than 0
+ daysSinceLastOrder = Number.EPSILON;
+ }
+
+ // Sum up the total investment days since the start date to calculate
+ // the time weighted investment
+ totalInvestmentDays += daysSinceLastOrder;
+
+ sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add(
+ valueAtStartDate
+ .minus(investmentAtStartDate)
+ .plus(totalInvestmentBeforeTransaction)
+ .mul(daysSinceLastOrder)
+ );
+
+ sumOfTimeWeightedInvestmentsWithCurrencyEffect =
+ sumOfTimeWeightedInvestmentsWithCurrencyEffect.add(
+ valueAtStartDateWithCurrencyEffect
+ .minus(investmentAtStartDateWithCurrencyEffect)
+ .plus(totalInvestmentBeforeTransactionWithCurrencyEffect)
+ .mul(daysSinceLastOrder)
+ );
+ }
+
+ currentValues[order.date] = valueOfInvestment;
+
+ currentValuesWithCurrencyEffect[order.date] =
+ valueOfInvestmentWithCurrencyEffect;
+
+ netPerformanceValues[order.date] = grossPerformance
+ .minus(grossPerformanceAtStartDate)
+ .minus(fees.minus(feesAtStartDate));
+
+ netPerformanceValuesWithCurrencyEffect[order.date] =
+ grossPerformanceWithCurrencyEffect
+ .minus(grossPerformanceAtStartDateWithCurrencyEffect)
+ .minus(
+ feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
+ );
+
+ investmentValuesAccumulated[order.date] = totalInvestment;
+
+ investmentValuesAccumulatedWithCurrencyEffect[order.date] =
+ totalInvestmentWithCurrencyEffect;
+
+ investmentValuesWithCurrencyEffect[order.date] = (
+ investmentValuesWithCurrencyEffect[order.date] ?? new Big(0)
+ ).add(transactionInvestmentWithCurrencyEffect);
+
+ timeWeightedInvestmentValues[order.date] =
+ totalInvestmentDays > 0
+ ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
+ : new Big(0);
+
+ timeWeightedInvestmentValuesWithCurrencyEffect[order.date] =
+ totalInvestmentDays > 0
+ ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
+ totalInvestmentDays
+ )
+ : new Big(0);
+ }
+
+ if (PortfolioCalculator.ENABLE_LOGGING) {
+ console.log('totalInvestment', totalInvestment.toNumber());
+
+ console.log(
+ 'totalInvestmentWithCurrencyEffect',
+ totalInvestmentWithCurrencyEffect.toNumber()
+ );
+
+ console.log(
+ 'totalGrossPerformance',
+ grossPerformance.minus(grossPerformanceAtStartDate).toNumber()
+ );
+
+ console.log(
+ 'totalGrossPerformanceWithCurrencyEffect',
+ grossPerformanceWithCurrencyEffect
+ .minus(grossPerformanceAtStartDateWithCurrencyEffect)
+ .toNumber()
+ );
+ }
+
+ if (i === indexOfEndOrder) {
+ break;
+ }
+ }
+
+ const totalGrossPerformance = grossPerformance.minus(
+ grossPerformanceAtStartDate
+ );
+
+ const totalGrossPerformanceWithCurrencyEffect =
+ grossPerformanceWithCurrencyEffect.minus(
+ grossPerformanceAtStartDateWithCurrencyEffect
+ );
+
+ const totalNetPerformance = grossPerformance
+ .minus(grossPerformanceAtStartDate)
+ .minus(fees.minus(feesAtStartDate));
+
+ const timeWeightedAverageInvestmentBetweenStartAndEndDate =
+ totalInvestmentDays > 0
+ ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
+ : new Big(0);
+
+ const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect =
+ totalInvestmentDays > 0
+ ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
+ totalInvestmentDays
+ )
+ : new Big(0);
+
+ const grossPerformancePercentage =
+ timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
+ ? totalGrossPerformance.div(
+ timeWeightedAverageInvestmentBetweenStartAndEndDate
+ )
+ : new Big(0);
+
+ const grossPerformancePercentageWithCurrencyEffect =
+ timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt(
+ 0
+ )
+ ? totalGrossPerformanceWithCurrencyEffect.div(
+ timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
+ )
+ : new Big(0);
+
+ const feesPerUnit = totalUnits.gt(0)
+ ? fees.minus(feesAtStartDate).div(totalUnits)
+ : new Big(0);
+
+ const feesPerUnitWithCurrencyEffect = totalUnits.gt(0)
+ ? feesWithCurrencyEffect
+ .minus(feesAtStartDateWithCurrencyEffect)
+ .div(totalUnits)
+ : new Big(0);
+
+ const netPerformancePercentage =
+ timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
+ ? totalNetPerformance.div(
+ timeWeightedAverageInvestmentBetweenStartAndEndDate
+ )
+ : new Big(0);
+
+ const netPerformancePercentageWithCurrencyEffectMap: {
+ [key: DateRange]: Big;
+ } = {};
+
+ const netPerformanceWithCurrencyEffectMap: {
+ [key: DateRange]: Big;
+ } = {};
+
+ for (const dateRange of [
+ '1d',
+ '1y',
+ '5y',
+ 'max',
+ 'mtd',
+ 'wtd',
+ 'ytd'
+ // TODO:
+ // ...eachYearOfInterval({ end, start })
+ // .filter((date) => {
+ // return !isThisYear(date);
+ // })
+ // .map((date) => {
+ // return format(date, 'yyyy');
+ // })
+ ] as DateRange[]) {
+ const dateInterval = getIntervalFromDateRange(dateRange);
+ const endDate = dateInterval.endDate;
+ let startDate = dateInterval.startDate;
+
+ if (isBefore(startDate, start)) {
+ startDate = start;
+ }
+
+ const rangeEndDateString = format(endDate, DATE_FORMAT);
+ const rangeStartDateString = format(startDate, DATE_FORMAT);
+
+ const currentValuesAtDateRangeStartWithCurrencyEffect =
+ currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0);
+
+ const investmentValuesAccumulatedAtStartDateWithCurrencyEffect =
+ investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ??
+ new Big(0);
+
+ const grossPerformanceAtDateRangeStartWithCurrencyEffect =
+ currentValuesAtDateRangeStartWithCurrencyEffect.minus(
+ investmentValuesAccumulatedAtStartDateWithCurrencyEffect
+ );
+
+ let average = new Big(0);
+ let dayCount = 0;
+
+ for (let i = this.chartDates.length - 1; i >= 0; i -= 1) {
+ const date = this.chartDates[i];
+
+ if (date > rangeEndDateString) {
+ continue;
+ } else if (date < rangeStartDateString) {
+ break;
+ }
+
+ if (
+ investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&
+ investmentValuesAccumulatedWithCurrencyEffect[date].gt(0)
+ ) {
+ average = average.add(
+ investmentValuesAccumulatedWithCurrencyEffect[date].add(
+ grossPerformanceAtDateRangeStartWithCurrencyEffect
+ )
+ );
+
+ dayCount++;
+ }
+ }
+
+ if (dayCount > 0) {
+ average = average.div(dayCount);
+ }
+
+ netPerformanceWithCurrencyEffectMap[dateRange] =
+ netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus(
+ // If the date range is 'max', take 0 as a start value. Otherwise,
+ // the value of the end of the day of the start date is taken which
+ // differs from the buying price.
+ dateRange === 'max'
+ ? new Big(0)
+ : (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ??
+ new Big(0))
+ ) ?? new Big(0);
+
+ netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0)
+ ? netPerformanceWithCurrencyEffectMap[dateRange].div(average)
+ : new Big(0);
+ }
+
+ if (PortfolioCalculator.ENABLE_LOGGING) {
+ console.log(
+ `
+ ${symbol}
+ Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed(
+ 2
+ )} -> ${unitPriceAtEndDate.toFixed(2)}
+ Total investment: ${totalInvestment.toFixed(2)}
+ Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed(
+ 2
+ )}
+ Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed(
+ 2
+ )}
+ Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
+ 2
+ )}
+ Total dividend: ${totalDividend.toFixed(2)}
+ Gross performance: ${totalGrossPerformance.toFixed(
+ 2
+ )} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
+ Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed(
+ 2
+ )} / ${grossPerformancePercentageWithCurrencyEffect
+ .mul(100)
+ .toFixed(2)}%
+ Fees per unit: ${feesPerUnit.toFixed(2)}
+ Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed(
+ 2
+ )}
+ Net performance: ${totalNetPerformance.toFixed(
+ 2
+ )} / ${netPerformancePercentage.mul(100).toFixed(2)}%
+ Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[
+ 'max'
+ ].toFixed(2)}%`
+ );
+ }
+
+ return {
+ currentValues,
+ currentValuesWithCurrencyEffect,
+ feesWithCurrencyEffect,
+ grossPerformancePercentage,
+ grossPerformancePercentageWithCurrencyEffect,
+ initialValue,
+ initialValueWithCurrencyEffect,
+ investmentValuesAccumulated,
+ investmentValuesAccumulatedWithCurrencyEffect,
+ investmentValuesWithCurrencyEffect,
+ netPerformancePercentage,
+ netPerformancePercentageWithCurrencyEffectMap,
+ netPerformanceValues,
+ netPerformanceValuesWithCurrencyEffect,
+ netPerformanceWithCurrencyEffectMap,
+ timeWeightedInvestmentValues,
+ timeWeightedInvestmentValuesWithCurrencyEffect,
+ totalAccountBalanceInBaseCurrency,
+ totalDividend,
+ totalDividendInBaseCurrency,
+ totalInterest,
+ totalInterestInBaseCurrency,
+ totalInvestment,
+ totalInvestmentWithCurrencyEffect,
+ totalLiabilities,
+ totalLiabilitiesInBaseCurrency,
+ totalValuables,
+ totalValuablesInBaseCurrency,
+ grossPerformance: totalGrossPerformance,
+ grossPerformanceWithCurrencyEffect:
+ totalGrossPerformanceWithCurrencyEffect,
+ hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
+ netPerformance: totalNetPerformance,
+ timeWeightedInvestment:
+ timeWeightedAverageInvestmentBetweenStartAndEndDate,
+ timeWeightedInvestmentWithCurrencyEffect:
+ timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
+ };
+ }
+}
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
index cf808debb..6499ca3db 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
@@ -1,965 +1,24 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
-import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
-import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
-import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
-import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
SymbolMetrics
} from '@ghostfolio/common/interfaces';
-import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
-import { DateRange } from '@ghostfolio/common/types';
+import { PortfolioSnapshot } from '@ghostfolio/common/models';
-import { Logger } from '@nestjs/common';
-import { Big } from 'big.js';
-import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns';
-import { cloneDeep, sortBy } from 'lodash';
-
-export class TWRPortfolioCalculator extends PortfolioCalculator {
- private chartDates: string[];
-
- protected calculateOverallPerformance(
- positions: TimelinePosition[]
- ): PortfolioSnapshot {
- let currentValueInBaseCurrency = new Big(0);
- let grossPerformance = new Big(0);
- let grossPerformanceWithCurrencyEffect = new Big(0);
- let hasErrors = false;
- let netPerformance = new Big(0);
- let totalFeesWithCurrencyEffect = new Big(0);
- const totalInterestWithCurrencyEffect = new Big(0);
- let totalInvestment = new Big(0);
- let totalInvestmentWithCurrencyEffect = new Big(0);
- let totalTimeWeightedInvestment = new Big(0);
- let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
-
- for (const currentPosition of positions) {
- if (currentPosition.feeInBaseCurrency) {
- totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
- currentPosition.feeInBaseCurrency
- );
- }
-
- if (currentPosition.valueInBaseCurrency) {
- currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
- currentPosition.valueInBaseCurrency
- );
- } else {
- hasErrors = true;
- }
-
- if (currentPosition.investment) {
- totalInvestment = totalInvestment.plus(currentPosition.investment);
-
- totalInvestmentWithCurrencyEffect =
- totalInvestmentWithCurrencyEffect.plus(
- currentPosition.investmentWithCurrencyEffect
- );
- } else {
- hasErrors = true;
- }
-
- if (currentPosition.grossPerformance) {
- grossPerformance = grossPerformance.plus(
- currentPosition.grossPerformance
- );
-
- grossPerformanceWithCurrencyEffect =
- grossPerformanceWithCurrencyEffect.plus(
- currentPosition.grossPerformanceWithCurrencyEffect
- );
-
- netPerformance = netPerformance.plus(currentPosition.netPerformance);
- } else if (!currentPosition.quantity.eq(0)) {
- hasErrors = true;
- }
-
- if (currentPosition.timeWeightedInvestment) {
- totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus(
- currentPosition.timeWeightedInvestment
- );
-
- totalTimeWeightedInvestmentWithCurrencyEffect =
- totalTimeWeightedInvestmentWithCurrencyEffect.plus(
- currentPosition.timeWeightedInvestmentWithCurrencyEffect
- );
- } else if (!currentPosition.quantity.eq(0)) {
- Logger.warn(
- `Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
- 'PortfolioCalculator'
- );
-
- hasErrors = true;
- }
- }
-
- return {
- currentValueInBaseCurrency,
- hasErrors,
- positions,
- totalFeesWithCurrencyEffect,
- totalInterestWithCurrencyEffect,
- totalInvestment,
- totalInvestmentWithCurrencyEffect,
- errors: [],
- historicalData: [],
- totalLiabilitiesWithCurrencyEffect: new Big(0),
- totalValuablesWithCurrencyEffect: new Big(0)
- };
+export class TwrPortfolioCalculator extends PortfolioCalculator {
+ protected calculateOverallPerformance(): PortfolioSnapshot {
+ throw new Error('Method not implemented.');
}
- protected getSymbolMetrics({
- chartDateMap,
- dataSource,
- end,
- exchangeRates,
- marketSymbolMap,
- start,
- symbol
- }: {
- chartDateMap?: { [date: string]: boolean };
+ protected getSymbolMetrics({}: {
end: Date;
exchangeRates: { [dateString: string]: number };
marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
};
start: Date;
+ step?: number;
} & AssetProfileIdentifier): SymbolMetrics {
- const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
- const currentValues: { [date: string]: Big } = {};
- const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
- let fees = new Big(0);
- let feesAtStartDate = new Big(0);
- let feesAtStartDateWithCurrencyEffect = new Big(0);
- let feesWithCurrencyEffect = new Big(0);
- let grossPerformance = new Big(0);
- let grossPerformanceWithCurrencyEffect = new Big(0);
- let grossPerformanceAtStartDate = new Big(0);
- let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0);
- let grossPerformanceFromSells = new Big(0);
- let grossPerformanceFromSellsWithCurrencyEffect = new Big(0);
- let initialValue: Big;
- let initialValueWithCurrencyEffect: Big;
- let investmentAtStartDate: Big;
- let investmentAtStartDateWithCurrencyEffect: Big;
- const investmentValuesAccumulated: { [date: string]: Big } = {};
- const investmentValuesAccumulatedWithCurrencyEffect: {
- [date: string]: Big;
- } = {};
- const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {};
- let lastAveragePrice = new Big(0);
- let lastAveragePriceWithCurrencyEffect = new Big(0);
- const netPerformanceValues: { [date: string]: Big } = {};
- const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {};
- const timeWeightedInvestmentValues: { [date: string]: Big } = {};
-
- const timeWeightedInvestmentValuesWithCurrencyEffect: {
- [date: string]: Big;
- } = {};
-
- const totalAccountBalanceInBaseCurrency = new Big(0);
- let totalDividend = new Big(0);
- let totalDividendInBaseCurrency = new Big(0);
- let totalInterest = new Big(0);
- let totalInterestInBaseCurrency = new Big(0);
- let totalInvestment = new Big(0);
- let totalInvestmentFromBuyTransactions = new Big(0);
- let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
- let totalInvestmentWithCurrencyEffect = new Big(0);
- let totalLiabilities = new Big(0);
- let totalLiabilitiesInBaseCurrency = new Big(0);
- let totalQuantityFromBuyTransactions = new Big(0);
- let totalUnits = new Big(0);
- let totalValuables = new Big(0);
- let totalValuablesInBaseCurrency = new Big(0);
- let valueAtStartDate: Big;
- let valueAtStartDateWithCurrencyEffect: Big;
-
- // Clone orders to keep the original values in this.orders
- let orders: PortfolioOrderItem[] = cloneDeep(
- this.activities.filter(({ SymbolProfile }) => {
- return SymbolProfile.symbol === symbol;
- })
- );
-
- if (orders.length <= 0) {
- return {
- currentValues: {},
- currentValuesWithCurrencyEffect: {},
- feesWithCurrencyEffect: new Big(0),
- grossPerformance: new Big(0),
- grossPerformancePercentage: new Big(0),
- grossPerformancePercentageWithCurrencyEffect: new Big(0),
- grossPerformanceWithCurrencyEffect: new Big(0),
- hasErrors: false,
- initialValue: new Big(0),
- initialValueWithCurrencyEffect: new Big(0),
- investmentValuesAccumulated: {},
- investmentValuesAccumulatedWithCurrencyEffect: {},
- investmentValuesWithCurrencyEffect: {},
- netPerformance: new Big(0),
- netPerformancePercentage: new Big(0),
- netPerformancePercentageWithCurrencyEffectMap: {},
- netPerformanceValues: {},
- netPerformanceValuesWithCurrencyEffect: {},
- netPerformanceWithCurrencyEffectMap: {},
- timeWeightedInvestment: new Big(0),
- timeWeightedInvestmentValues: {},
- timeWeightedInvestmentValuesWithCurrencyEffect: {},
- timeWeightedInvestmentWithCurrencyEffect: new Big(0),
- totalAccountBalanceInBaseCurrency: new Big(0),
- totalDividend: new Big(0),
- totalDividendInBaseCurrency: new Big(0),
- totalInterest: new Big(0),
- totalInterestInBaseCurrency: new Big(0),
- totalInvestment: new Big(0),
- totalInvestmentWithCurrencyEffect: new Big(0),
- totalLiabilities: new Big(0),
- totalLiabilitiesInBaseCurrency: new Big(0),
- totalValuables: new Big(0),
- totalValuablesInBaseCurrency: new Big(0)
- };
- }
-
- const dateOfFirstTransaction = new Date(orders[0].date);
-
- const endDateString = format(end, DATE_FORMAT);
- const startDateString = format(start, DATE_FORMAT);
-
- const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol];
- const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol];
-
- if (
- !unitPriceAtEndDate ||
- (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start))
- ) {
- return {
- currentValues: {},
- currentValuesWithCurrencyEffect: {},
- feesWithCurrencyEffect: new Big(0),
- grossPerformance: new Big(0),
- grossPerformancePercentage: new Big(0),
- grossPerformancePercentageWithCurrencyEffect: new Big(0),
- grossPerformanceWithCurrencyEffect: new Big(0),
- hasErrors: true,
- initialValue: new Big(0),
- initialValueWithCurrencyEffect: new Big(0),
- investmentValuesAccumulated: {},
- investmentValuesAccumulatedWithCurrencyEffect: {},
- investmentValuesWithCurrencyEffect: {},
- netPerformance: new Big(0),
- netPerformancePercentage: new Big(0),
- netPerformancePercentageWithCurrencyEffectMap: {},
- netPerformanceWithCurrencyEffectMap: {},
- netPerformanceValues: {},
- netPerformanceValuesWithCurrencyEffect: {},
- timeWeightedInvestment: new Big(0),
- timeWeightedInvestmentValues: {},
- timeWeightedInvestmentValuesWithCurrencyEffect: {},
- timeWeightedInvestmentWithCurrencyEffect: new Big(0),
- totalAccountBalanceInBaseCurrency: new Big(0),
- totalDividend: new Big(0),
- totalDividendInBaseCurrency: new Big(0),
- totalInterest: new Big(0),
- totalInterestInBaseCurrency: new Big(0),
- totalInvestment: new Big(0),
- totalInvestmentWithCurrencyEffect: new Big(0),
- totalLiabilities: new Big(0),
- totalLiabilitiesInBaseCurrency: new Big(0),
- totalValuables: new Big(0),
- totalValuablesInBaseCurrency: new Big(0)
- };
- }
-
- // Add a synthetic order at the start and the end date
- orders.push({
- date: startDateString,
- fee: new Big(0),
- feeInBaseCurrency: new Big(0),
- itemType: 'start',
- quantity: new Big(0),
- SymbolProfile: {
- dataSource,
- symbol
- },
- type: 'BUY',
- unitPrice: unitPriceAtStartDate
- });
-
- orders.push({
- date: endDateString,
- fee: new Big(0),
- feeInBaseCurrency: new Big(0),
- itemType: 'end',
- SymbolProfile: {
- dataSource,
- symbol
- },
- quantity: new Big(0),
- type: 'BUY',
- unitPrice: unitPriceAtEndDate
- });
-
- let lastUnitPrice: Big;
-
- const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {};
-
- for (const order of orders) {
- ordersByDate[order.date] = ordersByDate[order.date] ?? [];
- ordersByDate[order.date].push(order);
- }
-
- if (!this.chartDates) {
- this.chartDates = Object.keys(chartDateMap).sort();
- }
-
- for (const dateString of this.chartDates) {
- if (dateString < startDateString) {
- continue;
- } else if (dateString > endDateString) {
- break;
- }
-
- if (ordersByDate[dateString]?.length > 0) {
- for (const order of ordersByDate[dateString]) {
- order.unitPriceFromMarketData =
- marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice;
- }
- } else {
- orders.push({
- date: dateString,
- fee: new Big(0),
- feeInBaseCurrency: new Big(0),
- quantity: new Big(0),
- SymbolProfile: {
- dataSource,
- symbol
- },
- type: 'BUY',
- unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice,
- unitPriceFromMarketData:
- marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice
- });
- }
-
- const lastOrder = orders.at(-1);
-
- lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice;
- }
-
- // Sort orders so that the start and end placeholder order are at the correct
- // position
- orders = sortBy(orders, ({ date, itemType }) => {
- let sortIndex = new Date(date);
-
- if (itemType === 'end') {
- sortIndex = addMilliseconds(sortIndex, 1);
- } else if (itemType === 'start') {
- sortIndex = addMilliseconds(sortIndex, -1);
- }
-
- return sortIndex.getTime();
- });
-
- const indexOfStartOrder = orders.findIndex(({ itemType }) => {
- return itemType === 'start';
- });
-
- const indexOfEndOrder = orders.findIndex(({ itemType }) => {
- return itemType === 'end';
- });
-
- let totalInvestmentDays = 0;
- let sumOfTimeWeightedInvestments = new Big(0);
- let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0);
-
- for (let i = 0; i < orders.length; i += 1) {
- const order = orders[i];
-
- if (PortfolioCalculator.ENABLE_LOGGING) {
- console.log();
- console.log();
- console.log(
- i + 1,
- order.date,
- order.type,
- order.itemType ? `(${order.itemType})` : ''
- );
- }
-
- const exchangeRateAtOrderDate = exchangeRates[order.date];
-
- if (order.type === 'DIVIDEND') {
- const dividend = order.quantity.mul(order.unitPrice);
-
- totalDividend = totalDividend.plus(dividend);
- totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
- dividend.mul(exchangeRateAtOrderDate ?? 1)
- );
- } else if (order.type === 'INTEREST') {
- const interest = order.quantity.mul(order.unitPrice);
-
- totalInterest = totalInterest.plus(interest);
- totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
- interest.mul(exchangeRateAtOrderDate ?? 1)
- );
- } else if (order.type === 'ITEM') {
- const valuables = order.quantity.mul(order.unitPrice);
-
- totalValuables = totalValuables.plus(valuables);
- totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus(
- valuables.mul(exchangeRateAtOrderDate ?? 1)
- );
- } else if (order.type === 'LIABILITY') {
- const liabilities = order.quantity.mul(order.unitPrice);
-
- totalLiabilities = totalLiabilities.plus(liabilities);
- totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
- liabilities.mul(exchangeRateAtOrderDate ?? 1)
- );
- }
-
- if (order.itemType === 'start') {
- // Take the unit price of the order as the market price if there are no
- // orders of this symbol before the start date
- order.unitPrice =
- indexOfStartOrder === 0
- ? orders[i + 1]?.unitPrice
- : unitPriceAtStartDate;
- }
-
- if (order.fee) {
- order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1);
- order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul(
- exchangeRateAtOrderDate ?? 1
- );
- }
-
- const unitPrice = ['BUY', 'SELL'].includes(order.type)
- ? order.unitPrice
- : order.unitPriceFromMarketData;
-
- if (unitPrice) {
- order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1);
-
- order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul(
- exchangeRateAtOrderDate ?? 1
- );
- }
-
- const valueOfInvestmentBeforeTransaction = totalUnits.mul(
- order.unitPriceInBaseCurrency
- );
-
- const valueOfInvestmentBeforeTransactionWithCurrencyEffect =
- totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect);
-
- if (!investmentAtStartDate && i >= indexOfStartOrder) {
- investmentAtStartDate = totalInvestment ?? new Big(0);
-
- investmentAtStartDateWithCurrencyEffect =
- totalInvestmentWithCurrencyEffect ?? new Big(0);
-
- valueAtStartDate = valueOfInvestmentBeforeTransaction;
-
- valueAtStartDateWithCurrencyEffect =
- valueOfInvestmentBeforeTransactionWithCurrencyEffect;
- }
-
- let transactionInvestment = new Big(0);
- let transactionInvestmentWithCurrencyEffect = new Big(0);
-
- if (order.type === 'BUY') {
- transactionInvestment = order.quantity
- .mul(order.unitPriceInBaseCurrency)
- .mul(getFactor(order.type));
-
- transactionInvestmentWithCurrencyEffect = order.quantity
- .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
- .mul(getFactor(order.type));
-
- totalQuantityFromBuyTransactions =
- totalQuantityFromBuyTransactions.plus(order.quantity);
-
- totalInvestmentFromBuyTransactions =
- totalInvestmentFromBuyTransactions.plus(transactionInvestment);
-
- totalInvestmentFromBuyTransactionsWithCurrencyEffect =
- totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
- transactionInvestmentWithCurrencyEffect
- );
- } else if (order.type === 'SELL') {
- if (totalUnits.gt(0)) {
- transactionInvestment = totalInvestment
- .div(totalUnits)
- .mul(order.quantity)
- .mul(getFactor(order.type));
- transactionInvestmentWithCurrencyEffect =
- totalInvestmentWithCurrencyEffect
- .div(totalUnits)
- .mul(order.quantity)
- .mul(getFactor(order.type));
- }
- }
-
- if (PortfolioCalculator.ENABLE_LOGGING) {
- console.log('order.quantity', order.quantity.toNumber());
- console.log('transactionInvestment', transactionInvestment.toNumber());
-
- console.log(
- 'transactionInvestmentWithCurrencyEffect',
- transactionInvestmentWithCurrencyEffect.toNumber()
- );
- }
-
- const totalInvestmentBeforeTransaction = totalInvestment;
-
- const totalInvestmentBeforeTransactionWithCurrencyEffect =
- totalInvestmentWithCurrencyEffect;
-
- totalInvestment = totalInvestment.plus(transactionInvestment);
-
- totalInvestmentWithCurrencyEffect =
- totalInvestmentWithCurrencyEffect.plus(
- transactionInvestmentWithCurrencyEffect
- );
-
- if (i >= indexOfStartOrder && !initialValue) {
- if (
- i === indexOfStartOrder &&
- !valueOfInvestmentBeforeTransaction.eq(0)
- ) {
- initialValue = valueOfInvestmentBeforeTransaction;
-
- initialValueWithCurrencyEffect =
- valueOfInvestmentBeforeTransactionWithCurrencyEffect;
- } else if (transactionInvestment.gt(0)) {
- initialValue = transactionInvestment;
-
- initialValueWithCurrencyEffect =
- transactionInvestmentWithCurrencyEffect;
- }
- }
-
- fees = fees.plus(order.feeInBaseCurrency ?? 0);
-
- feesWithCurrencyEffect = feesWithCurrencyEffect.plus(
- order.feeInBaseCurrencyWithCurrencyEffect ?? 0
- );
-
- totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
-
- const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
-
- const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
- order.unitPriceInBaseCurrencyWithCurrencyEffect
- );
-
- const grossPerformanceFromSell =
- order.type === 'SELL'
- ? order.unitPriceInBaseCurrency
- .minus(lastAveragePrice)
- .mul(order.quantity)
- : new Big(0);
-
- const grossPerformanceFromSellWithCurrencyEffect =
- order.type === 'SELL'
- ? order.unitPriceInBaseCurrencyWithCurrencyEffect
- .minus(lastAveragePriceWithCurrencyEffect)
- .mul(order.quantity)
- : new Big(0);
-
- grossPerformanceFromSells = grossPerformanceFromSells.plus(
- grossPerformanceFromSell
- );
-
- grossPerformanceFromSellsWithCurrencyEffect =
- grossPerformanceFromSellsWithCurrencyEffect.plus(
- grossPerformanceFromSellWithCurrencyEffect
- );
-
- lastAveragePrice = totalQuantityFromBuyTransactions.eq(0)
- ? new Big(0)
- : totalInvestmentFromBuyTransactions.div(
- totalQuantityFromBuyTransactions
- );
-
- lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq(
- 0
- )
- ? new Big(0)
- : totalInvestmentFromBuyTransactionsWithCurrencyEffect.div(
- totalQuantityFromBuyTransactions
- );
-
- if (PortfolioCalculator.ENABLE_LOGGING) {
- console.log(
- 'grossPerformanceFromSells',
- grossPerformanceFromSells.toNumber()
- );
- console.log(
- 'grossPerformanceFromSellWithCurrencyEffect',
- grossPerformanceFromSellWithCurrencyEffect.toNumber()
- );
- }
-
- const newGrossPerformance = valueOfInvestment
- .minus(totalInvestment)
- .plus(grossPerformanceFromSells);
-
- const newGrossPerformanceWithCurrencyEffect =
- valueOfInvestmentWithCurrencyEffect
- .minus(totalInvestmentWithCurrencyEffect)
- .plus(grossPerformanceFromSellsWithCurrencyEffect);
-
- grossPerformance = newGrossPerformance;
-
- grossPerformanceWithCurrencyEffect =
- newGrossPerformanceWithCurrencyEffect;
-
- if (order.itemType === 'start') {
- feesAtStartDate = fees;
- feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
- grossPerformanceAtStartDate = grossPerformance;
-
- grossPerformanceAtStartDateWithCurrencyEffect =
- grossPerformanceWithCurrencyEffect;
- }
-
- if (i > indexOfStartOrder) {
- // Only consider periods with an investment for the calculation of
- // the time weighted investment
- if (
- valueOfInvestmentBeforeTransaction.gt(0) &&
- ['BUY', 'SELL'].includes(order.type)
- ) {
- // Calculate the number of days since the previous order
- const orderDate = new Date(order.date);
- const previousOrderDate = new Date(orders[i - 1].date);
-
- let daysSinceLastOrder = differenceInDays(
- orderDate,
- previousOrderDate
- );
- if (daysSinceLastOrder <= 0) {
- // The time between two activities on the same day is unknown
- // -> Set it to the smallest floating point number greater than 0
- daysSinceLastOrder = Number.EPSILON;
- }
-
- // Sum up the total investment days since the start date to calculate
- // the time weighted investment
- totalInvestmentDays += daysSinceLastOrder;
-
- sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add(
- valueAtStartDate
- .minus(investmentAtStartDate)
- .plus(totalInvestmentBeforeTransaction)
- .mul(daysSinceLastOrder)
- );
-
- sumOfTimeWeightedInvestmentsWithCurrencyEffect =
- sumOfTimeWeightedInvestmentsWithCurrencyEffect.add(
- valueAtStartDateWithCurrencyEffect
- .minus(investmentAtStartDateWithCurrencyEffect)
- .plus(totalInvestmentBeforeTransactionWithCurrencyEffect)
- .mul(daysSinceLastOrder)
- );
- }
-
- currentValues[order.date] = valueOfInvestment;
-
- currentValuesWithCurrencyEffect[order.date] =
- valueOfInvestmentWithCurrencyEffect;
-
- netPerformanceValues[order.date] = grossPerformance
- .minus(grossPerformanceAtStartDate)
- .minus(fees.minus(feesAtStartDate));
-
- netPerformanceValuesWithCurrencyEffect[order.date] =
- grossPerformanceWithCurrencyEffect
- .minus(grossPerformanceAtStartDateWithCurrencyEffect)
- .minus(
- feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
- );
-
- investmentValuesAccumulated[order.date] = totalInvestment;
-
- investmentValuesAccumulatedWithCurrencyEffect[order.date] =
- totalInvestmentWithCurrencyEffect;
-
- investmentValuesWithCurrencyEffect[order.date] = (
- investmentValuesWithCurrencyEffect[order.date] ?? new Big(0)
- ).add(transactionInvestmentWithCurrencyEffect);
-
- timeWeightedInvestmentValues[order.date] =
- totalInvestmentDays > 0
- ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
- : new Big(0);
-
- timeWeightedInvestmentValuesWithCurrencyEffect[order.date] =
- totalInvestmentDays > 0
- ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
- totalInvestmentDays
- )
- : new Big(0);
- }
-
- if (PortfolioCalculator.ENABLE_LOGGING) {
- console.log('totalInvestment', totalInvestment.toNumber());
-
- console.log(
- 'totalInvestmentWithCurrencyEffect',
- totalInvestmentWithCurrencyEffect.toNumber()
- );
-
- console.log(
- 'totalGrossPerformance',
- grossPerformance.minus(grossPerformanceAtStartDate).toNumber()
- );
-
- console.log(
- 'totalGrossPerformanceWithCurrencyEffect',
- grossPerformanceWithCurrencyEffect
- .minus(grossPerformanceAtStartDateWithCurrencyEffect)
- .toNumber()
- );
- }
-
- if (i === indexOfEndOrder) {
- break;
- }
- }
-
- const totalGrossPerformance = grossPerformance.minus(
- grossPerformanceAtStartDate
- );
-
- const totalGrossPerformanceWithCurrencyEffect =
- grossPerformanceWithCurrencyEffect.minus(
- grossPerformanceAtStartDateWithCurrencyEffect
- );
-
- const totalNetPerformance = grossPerformance
- .minus(grossPerformanceAtStartDate)
- .minus(fees.minus(feesAtStartDate));
-
- const timeWeightedAverageInvestmentBetweenStartAndEndDate =
- totalInvestmentDays > 0
- ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
- : new Big(0);
-
- const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect =
- totalInvestmentDays > 0
- ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
- totalInvestmentDays
- )
- : new Big(0);
-
- const grossPerformancePercentage =
- timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
- ? totalGrossPerformance.div(
- timeWeightedAverageInvestmentBetweenStartAndEndDate
- )
- : new Big(0);
-
- const grossPerformancePercentageWithCurrencyEffect =
- timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt(
- 0
- )
- ? totalGrossPerformanceWithCurrencyEffect.div(
- timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
- )
- : new Big(0);
-
- const feesPerUnit = totalUnits.gt(0)
- ? fees.minus(feesAtStartDate).div(totalUnits)
- : new Big(0);
-
- const feesPerUnitWithCurrencyEffect = totalUnits.gt(0)
- ? feesWithCurrencyEffect
- .minus(feesAtStartDateWithCurrencyEffect)
- .div(totalUnits)
- : new Big(0);
-
- const netPerformancePercentage =
- timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
- ? totalNetPerformance.div(
- timeWeightedAverageInvestmentBetweenStartAndEndDate
- )
- : new Big(0);
-
- const netPerformancePercentageWithCurrencyEffectMap: {
- [key: DateRange]: Big;
- } = {};
-
- const netPerformanceWithCurrencyEffectMap: {
- [key: DateRange]: Big;
- } = {};
-
- for (const dateRange of [
- '1d',
- '1y',
- '5y',
- 'max',
- 'mtd',
- 'wtd',
- 'ytd'
- // TODO:
- // ...eachYearOfInterval({ end, start })
- // .filter((date) => {
- // return !isThisYear(date);
- // })
- // .map((date) => {
- // return format(date, 'yyyy');
- // })
- ] as DateRange[]) {
- const dateInterval = getIntervalFromDateRange(dateRange);
- const endDate = dateInterval.endDate;
- let startDate = dateInterval.startDate;
-
- if (isBefore(startDate, start)) {
- startDate = start;
- }
-
- const rangeEndDateString = format(endDate, DATE_FORMAT);
- const rangeStartDateString = format(startDate, DATE_FORMAT);
-
- const currentValuesAtDateRangeStartWithCurrencyEffect =
- currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0);
-
- const investmentValuesAccumulatedAtStartDateWithCurrencyEffect =
- investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ??
- new Big(0);
-
- const grossPerformanceAtDateRangeStartWithCurrencyEffect =
- currentValuesAtDateRangeStartWithCurrencyEffect.minus(
- investmentValuesAccumulatedAtStartDateWithCurrencyEffect
- );
-
- let average = new Big(0);
- let dayCount = 0;
-
- for (let i = this.chartDates.length - 1; i >= 0; i -= 1) {
- const date = this.chartDates[i];
-
- if (date > rangeEndDateString) {
- continue;
- } else if (date < rangeStartDateString) {
- break;
- }
-
- if (
- investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&
- investmentValuesAccumulatedWithCurrencyEffect[date].gt(0)
- ) {
- average = average.add(
- investmentValuesAccumulatedWithCurrencyEffect[date].add(
- grossPerformanceAtDateRangeStartWithCurrencyEffect
- )
- );
-
- dayCount++;
- }
- }
-
- if (dayCount > 0) {
- average = average.div(dayCount);
- }
-
- netPerformanceWithCurrencyEffectMap[dateRange] =
- netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus(
- // If the date range is 'max', take 0 as a start value. Otherwise,
- // the value of the end of the day of the start date is taken which
- // differs from the buying price.
- dateRange === 'max'
- ? new Big(0)
- : (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ??
- new Big(0))
- ) ?? new Big(0);
-
- netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0)
- ? netPerformanceWithCurrencyEffectMap[dateRange].div(average)
- : new Big(0);
- }
-
- if (PortfolioCalculator.ENABLE_LOGGING) {
- console.log(
- `
- ${symbol}
- Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed(
- 2
- )} -> ${unitPriceAtEndDate.toFixed(2)}
- Total investment: ${totalInvestment.toFixed(2)}
- Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed(
- 2
- )}
- Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed(
- 2
- )}
- Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
- 2
- )}
- Total dividend: ${totalDividend.toFixed(2)}
- Gross performance: ${totalGrossPerformance.toFixed(
- 2
- )} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
- Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed(
- 2
- )} / ${grossPerformancePercentageWithCurrencyEffect
- .mul(100)
- .toFixed(2)}%
- Fees per unit: ${feesPerUnit.toFixed(2)}
- Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed(
- 2
- )}
- Net performance: ${totalNetPerformance.toFixed(
- 2
- )} / ${netPerformancePercentage.mul(100).toFixed(2)}%
- Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[
- 'max'
- ].toFixed(2)}%`
- );
- }
-
- return {
- currentValues,
- currentValuesWithCurrencyEffect,
- feesWithCurrencyEffect,
- grossPerformancePercentage,
- grossPerformancePercentageWithCurrencyEffect,
- initialValue,
- initialValueWithCurrencyEffect,
- investmentValuesAccumulated,
- investmentValuesAccumulatedWithCurrencyEffect,
- investmentValuesWithCurrencyEffect,
- netPerformancePercentage,
- netPerformancePercentageWithCurrencyEffectMap,
- netPerformanceValues,
- netPerformanceValuesWithCurrencyEffect,
- netPerformanceWithCurrencyEffectMap,
- timeWeightedInvestmentValues,
- timeWeightedInvestmentValuesWithCurrencyEffect,
- totalAccountBalanceInBaseCurrency,
- totalDividend,
- totalDividendInBaseCurrency,
- totalInterest,
- totalInterestInBaseCurrency,
- totalInvestment,
- totalInvestmentWithCurrencyEffect,
- totalLiabilities,
- totalLiabilitiesInBaseCurrency,
- totalValuables,
- totalValuablesInBaseCurrency,
- grossPerformance: totalGrossPerformance,
- grossPerformanceWithCurrencyEffect:
- totalGrossPerformanceWithCurrencyEffect,
- hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
- netPerformance: totalNetPerformance,
- timeWeightedInvestment:
- timeWeightedAverageInvestmentBetweenStartAndEndDate,
- timeWeightedInvestmentWithCurrencyEffect:
- timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
- };
+ throw new Error('Method not implemented.');
}
}
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index 797d0449a..c3e46d50d 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -105,6 +105,7 @@ export class PortfolioController {
const {
accounts,
+ createdAt,
hasErrors,
holdings,
markets,
@@ -233,6 +234,7 @@ export class PortfolioController {
return {
accounts,
+ createdAt,
hasError,
holdings,
platforms,
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 8b295aad4..a3d9e3c4a 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -15,6 +15,11 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model
import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
+import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific';
+import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets';
+import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe';
+import { RegionalMarketClusterRiskJapan } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/japan';
+import { RegionalMarketClusterRiskNorthAmerica } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/north-america';
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 { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
@@ -77,7 +82,7 @@ import {
parseISO,
set
} from 'date-fns';
-import { isEmpty, uniq } from 'lodash';
+import { isEmpty } from 'lodash';
import { PortfolioCalculator } from './calculator/portfolio-calculator';
import {
@@ -290,7 +295,7 @@ export class PortfolioService {
activities,
filters,
userId,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: this.request.user.Settings.settings.baseCurrency
});
@@ -367,11 +372,11 @@ export class PortfolioService {
activities,
filters,
userId,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: userCurrency
});
- const { currentValueInBaseCurrency, hasErrors, positions } =
+ const { createdAt, currentValueInBaseCurrency, hasErrors, positions } =
await portfolioCalculator.getSnapshot();
const cashDetails = await this.accountService.getCashDetails({
@@ -612,6 +617,7 @@ export class PortfolioService {
return {
accounts,
+ createdAt,
hasErrors,
holdings,
markets,
@@ -674,7 +680,7 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({
activities,
userId,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: userCurrency
});
@@ -944,7 +950,7 @@ export class PortfolioService {
activities,
filters,
userId,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: this.request.user.Settings.settings.baseCurrency
});
@@ -1075,19 +1081,18 @@ export class PortfolioService {
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
- const accountBalanceItems =
- await this.accountBalanceService.getAccountBalanceItems({
+ const [accountBalanceItems, { activities }] = await Promise.all([
+ this.accountBalanceService.getAccountBalanceItems({
filters,
userId,
userCurrency
- });
-
- const { activities } =
- await this.orderService.getOrdersForPortfolioCalculator({
+ }),
+ this.orderService.getOrdersForPortfolioCalculator({
filters,
userCurrency,
userId
- });
+ })
+ ]);
if (accountBalanceItems.length === 0 && activities.length === 0) {
return {
@@ -1111,7 +1116,7 @@ export class PortfolioService {
activities,
filters,
userId,
- calculationType: PerformanceCalculationType.TWR,
+ calculationType: PerformanceCalculationType.ROAI,
currency: userCurrency
});
@@ -1167,12 +1172,19 @@ export class PortfolioService {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const userSettings = this.request.user.Settings.settings as UserSettings;
- const { accounts, holdings, markets, summary } = await this.getDetails({
- impersonationId,
- userId,
- withMarkets: true,
- withSummary: true
- });
+ const { accounts, holdings, markets, marketsAdvanced, summary } =
+ await this.getDetails({
+ impersonationId,
+ userId,
+ withMarkets: true,
+ withSummary: true
+ });
+
+ const marketsAdvancedTotalInBaseCurrency = getSum(
+ Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => {
+ return new Big(valueInBaseCurrency);
+ })
+ ).toNumber();
const marketsTotalInBaseCurrency = getSum(
Object.values(markets).map(({ valueInBaseCurrency }) => {
@@ -1265,7 +1277,40 @@ export class PortfolioService {
)
],
userSettings
- )
+ ),
+ regionalMarketClusterRisk:
+ summary.ordersCount > 0
+ ? await this.rulesService.evaluate(
+ [
+ new RegionalMarketClusterRiskAsiaPacific(
+ this.exchangeRateDataService,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.asiaPacific.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskEmergingMarkets(
+ this.exchangeRateDataService,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.emergingMarkets.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskEurope(
+ this.exchangeRateDataService,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.europe.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskJapan(
+ this.exchangeRateDataService,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.japan.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskNorthAmerica(
+ this.exchangeRateDataService,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.northAmerica.valueInBaseCurrency
+ )
+ ],
+ userSettings
+ )
+ : undefined
};
return { rules, statistics: this.getReportStatistics(rules) };
@@ -1987,14 +2032,16 @@ export class PortfolioService {
where: { id: filters[0].id }
});
} else {
- const accountIds = uniq(
- activities
- .filter(({ accountId }) => {
- return accountId;
- })
- .map(({ accountId }) => {
- return accountId;
- })
+ const accountIds = Array.from(
+ new Set(
+ activities
+ .filter(({ accountId }) => {
+ return accountId;
+ })
+ .map(({ accountId }) => {
+ return accountId;
+ })
+ )
);
currentAccounts = await this.accountService.accounts({
diff --git a/apps/api/src/app/redis-cache/redis-cache.service.ts b/apps/api/src/app/redis-cache/redis-cache.service.ts
index c972c30a1..51db93ec6 100644
--- a/apps/api/src/app/redis-cache/redis-cache.service.ts
+++ b/apps/api/src/app/redis-cache/redis-cache.service.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { Milliseconds } from 'cache-manager';
import { RedisCache } from 'cache-manager-redis-yet';
import { createHash } from 'crypto';
+import ms from 'ms';
@Injectable()
export class RedisCacheService {
@@ -59,6 +60,26 @@ export class RedisCacheService {
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
}
+ public async isHealthy() {
+ try {
+ const client = this.cache.store.client;
+
+ const isHealthy = await Promise.race([
+ client.ping(),
+ new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error('Redis health check timeout')),
+ ms('2 seconds')
+ )
+ )
+ ]);
+
+ return isHealthy === 'PONG';
+ } catch (error) {
+ return false;
+ }
+ }
+
public async remove(key: string) {
return this.cache.del(key);
}
diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts
index 89cdd416c..5d9a49a29 100644
--- a/apps/api/src/app/symbol/symbol.controller.ts
+++ b/apps/api/src/app/symbol/symbol.controller.ts
@@ -47,7 +47,7 @@ export class SymbolController {
try {
return this.symbolService.lookup({
includeIndices,
- query: query.toLowerCase(),
+ query,
user: this.request.user
});
} catch {
diff --git a/apps/api/src/app/tag/create-tag.dto.ts b/apps/api/src/app/tag/create-tag.dto.ts
deleted file mode 100644
index 650a0ce12..000000000
--- a/apps/api/src/app/tag/create-tag.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { IsString } from 'class-validator';
-
-export class CreateTagDto {
- @IsString()
- name: string;
-}
diff --git a/apps/api/src/app/tag/tag.module.ts b/apps/api/src/app/tag/tag.module.ts
deleted file mode 100644
index 48587c54e..000000000
--- a/apps/api/src/app/tag/tag.module.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-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 {}
diff --git a/apps/api/src/app/tag/tag.service.ts b/apps/api/src/app/tag/tag.service.ts
deleted file mode 100644
index c4a5447ac..000000000
--- a/apps/api/src/app/tag/tag.service.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-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 {
- return this.prismaService.tag.delete({ where });
- }
-
- public async getTag(
- tagWhereUniqueInput: Prisma.TagWhereUniqueInput
- ): Promise {
- 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, userId }) => {
- return {
- id,
- name,
- userId,
- activityCount: _count.orders
- };
- });
- }
-
- public async updateTag({
- data,
- where
- }: {
- data: Prisma.TagUpdateInput;
- where: Prisma.TagWhereUniqueInput;
- }): Promise {
- return this.prismaService.tag.update({
- data,
- where
- });
- }
-}
diff --git a/apps/api/src/app/tag/update-tag.dto.ts b/apps/api/src/app/tag/update-tag.dto.ts
deleted file mode 100644
index b26ffde11..000000000
--- a/apps/api/src/app/tag/update-tag.dto.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { IsString } from 'class-validator';
-
-export class UpdateTagDto {
- @IsString()
- id: string;
-
- @IsString()
- name: string;
-}
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index b5c71179f..40bc1b2b5 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -13,6 +13,11 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model
import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
+import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific';
+import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets';
+import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe';
+import { RegionalMarketClusterRiskJapan } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/japan';
+import { RegionalMarketClusterRiskNorthAmerica } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/north-america';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@@ -81,6 +86,9 @@ export class UserService {
orderBy: { alias: 'asc' },
where: { GranteeUser: { id } }
}),
+ this.prismaService.order.count({
+ where: { userId: id }
+ }),
this.prismaService.order.findFirst({
orderBy: {
date: 'asc'
@@ -91,8 +99,9 @@ export class UserService {
]);
const access = userData[0];
- const firstActivity = userData[1];
- let tags = userData[2];
+ const activitiesCount = userData[1];
+ const firstActivity = userData[2];
+ let tags = userData[3];
let systemMessage: SystemMessage;
@@ -112,6 +121,7 @@ export class UserService {
}
return {
+ activitiesCount,
id,
permissions,
subscription,
@@ -268,7 +278,35 @@ export class UserService {
undefined,
undefined,
undefined
- ).getSettings(user.Settings.settings)
+ ).getSettings(user.Settings.settings),
+ RegionalMarketClusterRiskAsiaPacific:
+ new RegionalMarketClusterRiskAsiaPacific(
+ undefined,
+ undefined,
+ undefined
+ ).getSettings(user.Settings.settings),
+ RegionalMarketClusterRiskEmergingMarkets:
+ new RegionalMarketClusterRiskEmergingMarkets(
+ undefined,
+ undefined,
+ undefined
+ ).getSettings(user.Settings.settings),
+ RegionalMarketClusterRiskEurope: new RegionalMarketClusterRiskEurope(
+ undefined,
+ undefined,
+ undefined
+ ).getSettings(user.Settings.settings),
+ RegionalMarketClusterRiskJapan: new RegionalMarketClusterRiskJapan(
+ undefined,
+ undefined,
+ undefined
+ ).getSettings(user.Settings.settings),
+ RegionalMarketClusterRiskNorthAmerica:
+ new RegionalMarketClusterRiskNorthAmerica(
+ undefined,
+ undefined,
+ undefined
+ ).getSettings(user.Settings.settings)
};
let currentPermissions = getPermissions(user.role);
@@ -313,7 +351,11 @@ export class UserService {
currentPermissions,
permissions.accessHoldingsChart,
permissions.createAccess,
- permissions.readAiPrompt
+ permissions.createMarketDataOfOwnAssetProfile,
+ permissions.createOwnTag,
+ permissions.readAiPrompt,
+ permissions.readMarketDataOfOwnAssetProfile,
+ permissions.updateMarketDataOfOwnAssetProfile
);
// Reset benchmark
diff --git a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json b/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
index f77297d07..6ac25e5d9 100644
--- a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
+++ b/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
@@ -1,6 +1,7 @@
{
"3": "The Three Musketeers",
"7": "Lucky7",
+ "32": "Project 32",
"42": "42 Coin",
"47": "President Trump",
"300": "300 token",
@@ -16,15 +17,16 @@
"1717": "1717 Masonic Commemorative Token",
"2015": "2015 coin",
"2024": "2024",
+ "2025": "2025 TOKEN",
"2049": "TOKEN 2049",
"2192": "LERNITAS",
"$MAID": "MaidCoin",
- "$ROPE": "Rope",
"$TREAM": "World Stream Finance",
"00": "ZER0ZER0",
"007": "007 coin",
"0DOG": "Bitcoin Dogs",
"0KN": "0 Knowledge Network",
+ "0LNETWORK": "0L Network",
"0NE": "Stone",
"0X0": "0x0.ai",
"0X1": "0x1.tools: AI Multi-tool Plaform",
@@ -41,6 +43,8 @@
"0xVPN": "0xVPN.org",
"1-UP": "1-UP",
"1000SATS": "SATS",
+ "1000X": "1000x by Virtuals",
+ "101M": "101M",
"10SET": "Tenset",
"1ART": "ArtWallet",
"1CAT": "Bitcoin Cats",
@@ -51,6 +55,7 @@
"1FLR": "Flare Token",
"1GOLD": "1irstGold",
"1GUY": "1GUY",
+ "1HUB": "1HubAI",
"1INCH": "1inch",
"1IRST": "1irstcoin",
"1MCT": "MicroCreditToken",
@@ -92,9 +97,11 @@
"3CRV": "LP 3pool Curve",
"3D3D": "3d3d",
"3DES": "3DES",
+ "3DVANCE": "3D Vance",
"3FT": "ThreeFold Token",
"3KM": "3 Kingdoms Multiverse",
"3P": "Web3Camp",
+ "3RDEYE": "3rd Eye",
"3ULL": "3ULL Coin",
"3ULLV1": "Playa3ull Games v1",
"3XD": "3DChain",
@@ -116,6 +123,7 @@
"50TRUMP": "50TRUMP",
"50X": "50x.com",
"5IRE": "5ire",
+ "69MINUTES": "69 Minutes",
"77G": "GraphenTech",
"7E": "7ELEVEN",
"88MPH": "88mph",
@@ -130,7 +138,9 @@
"A": "Alpha Token",
"A1INCH": "1inch (Arbitrum Bridge)",
"A2A": "A2A",
+ "A2I": "Arcana AI",
"A4": "A4 Finance",
+ "A47": "AGENDA 47",
"A4M": "AlienForm",
"A51": "A51 Finance",
"A5T": "Alpha5",
@@ -138,6 +148,7 @@
"AA": "Alva",
"AAA": "Moon Rabbit",
"AAAHHM": "Plankton in Pain",
+ "AAAI": "AAAI_agent by Virtuals",
"AAB": "AAX Token",
"AABL": "Abble",
"AAC": "Double-A Chain",
@@ -212,6 +223,7 @@
"ACM": "AC Milan Fan Token",
"ACN": "AvonCoin",
"ACOIN": "ACoin",
+ "ACOLYT": "Acolyte by Virtuals",
"ACP": "Anarchists Prime",
"ACPT": "Crypto Accept",
"ACQ": "Acquire.Fi",
@@ -231,7 +243,8 @@
"ADA": "Cardano",
"ADAB": "Adab Solutions",
"ADACASH": "ADACash",
- "ADAI": "Aave DAI",
+ "ADAI": "Aave Interest bearing DAI",
+ "ADAIV1": "Aave DAI",
"ADAM": "Adam Back",
"ADANA": "Adanaspor Fan Token",
"ADAO": "ADADao",
@@ -262,6 +275,7 @@
"ADON": "Adonis",
"ADP": "Adappter Token",
"ADR": "Adroverse",
+ "ADRI": "AdRise",
"ADRX": "Adrenaline Chain",
"ADS": "Adshares",
"ADT": "AdToken",
@@ -320,6 +334,7 @@
"AGATA": "Agatech",
"AGB": "Apes Go Bananas",
"AGC": "Argocoin",
+ "AGEN": "Agent Krasnov",
"AGENT": "AgentLayer",
"AGENTFUN": "AgentFun.AI",
"AGET": "Agetron",
@@ -328,10 +343,12 @@
"AGF": "Augmented Finance",
"AGG": "AGG",
"AGI": "Delysium",
+ "AGIALPHA": "AGI ALPHA AGENT",
"AGII": "AGII",
"AGIL": "Agility LSD",
"AGIV1": "SingularityNET v1",
"AGIX": "SingularityNET",
+ "AGIXBT": "AGIXBT by Virtuals",
"AGLA": "Angola",
"AGLD": "Adventure Gold",
"AGM": "Argoneum",
@@ -353,7 +370,11 @@
"AHT": "AhaToken",
"AI": "Sleepless",
"AI16Z": "ai16z",
+ "AI21X": "ai21x",
+ "AI23T": "23 Turtles",
+ "AI69SAKURA": "Sakura",
"AIA": "AIA Chain",
+ "AIAGENT": "Aiagent.app",
"AIAI": "All In AI",
"AIAKITA": "AiAkita",
"AIAT": "AI Analysis Token",
@@ -363,12 +384,14 @@
"AIBCOIN": "AIBLOCK",
"AIBK": "AIB Utility Token",
"AIBU": "AIBUZZ TOKEN",
- "AIC": "AI Crypto",
+ "AIC": "AI Companions",
"AICELL": "AICell",
"AICH": "AIChain",
"AICO": "AICON",
"AICODE": "AI CODE",
"AICORE": "AICORE",
+ "AICRYPTO": "AI Crypto",
+ "AICRYPTOKEN": "AI Crypto Token",
"AID": "AidCoin",
"AIDA": "Ai-Da robot",
"AIDI": "Aidi Inu",
@@ -381,6 +404,7 @@
"AIDUS": "AIDUS Token",
"AIE": "A.I.Earn",
"AIEN": "AIENGLISH",
+ "AIEPK": "EpiK Protocol",
"AIF": "AI FREEDOM TOKEN",
"AIFLOKI": "AI Floki",
"AIFUN": "AI Agent Layer",
@@ -396,15 +420,19 @@
"AIMBOT": "AimBot AI",
"AIMEE": "AIMEE",
"AIMET": "AI Metaverse",
+ "AIMONICA": "Aimonica Brands",
"AIMR": "MeromAI",
"AIMS": "HighCastle Token",
"AIMX": "Aimedis",
+ "AIMXV1": "Aimedis v1",
"AIN": "AI Network",
"AINA": "Ainastasia",
"AINN": "AINN",
+ "AINTI": "AIntivirus",
"AINU": "Ainu Token",
"AION": "Aion",
"AIONE": "AiONE",
+ "AIOS": "INT OS",
"AIOT": "AIOT Token",
"AIOZ": "AIOZ Network",
"AIPAD": "AIPAD",
@@ -418,6 +446,7 @@
"AIRBTC": "AIRBTC",
"AIRDROP": "AIRDROP2049",
"AIRE": "Tokenaire",
+ "AIREVOLUTION": "AI Revolution Coin",
"AIRI": "aiRight",
"AIRIAN": "AIRian",
"AIRT": "Aircraft",
@@ -436,16 +465,24 @@
"AITK": "AITK",
"AITN": "Artificial Intelligence Technology Network",
"AITRA": "Aitra",
+ "AITRUMP": "AITRUMP",
"AITT": "AITrading",
"AIUS": "Arbius",
+ "AIV": "AIVeronica",
+ "AIVA": "AI Voice Agents",
+ "AIVIA": "AI Virtual Agents",
"AIWALLET": "AiWallet Token",
+ "AIWS": "AIWS",
"AIX": "Aigang",
"AIXBT": "aixbt by Virtuals",
"AIXERC": "AI-X",
+ "AIXT": "AIXTerminal",
"AJNA": "Ajna Protocol",
"AJUN": "Ajuna Network",
"AK12": "AK12",
"AKA": "Akroma",
+ "AKAL": "AKA Liberty",
+ "AKASHA": "Akasha by Bloomverse",
"AKI": "Aki Network",
"AKIT": "Akita Inu",
"AKITA": "Akita Inu",
@@ -485,15 +522,18 @@
"ALF": "AlphaCoin",
"ALG": "Algory",
"ALGB": "Algebra",
+ "ALGERIA": "Algeria",
"ALGO": "Algorand",
"ALGOBLK": "AlgoBlocks",
"ALGOW": "Algowave",
"ALH": "AlloHash",
"ALI": "Alethea Artificial Liquid Intelligence Token",
"ALIAS": "Alias",
+ "ALIBABAAI": "Alibaba AI Agent",
"ALIC": "AliCoin",
"ALICE": "My Neighbor Alice",
"ALICEA": "Alice AI",
+ "ALICEW": "Alice Weidel",
"ALIEN": "AlienCoin",
"ALIENPEP": "Alien Pepe",
"ALIF": " ALIF COIN",
@@ -515,6 +555,7 @@
"ALN": "Aluna",
"ALNV1": "Aluna v1",
"ALOHA": "Aloha",
+ "ALON": "Alon",
"ALOT": "Dexalot",
"ALP": "Alphacon",
"ALPA": "Alpaca",
@@ -569,6 +610,7 @@
"AMERI": "AMERICAN EAGLE",
"AMERIC": "American True Hero",
"AMERICA": "America",
+ "AMERICAI": "AMERICA AI Agent",
"AMERICANCOIN": "AmericanCoin",
"AMF": "AddMeFast",
"AMG": "DeHeroGame Amazing Token",
@@ -614,9 +656,11 @@
"ANDY": "ANDY",
"ANDYB": "AndyBlast",
"ANDYBNB": "Andy",
- "ANDYBSC": "ANDY",
+ "ANDYBSC": "Andy BSC",
+ "ANDYBSCVIP": "ANDY",
"ANDYMAN": "ANDYMAN",
"ANDYSOL": "Andy on SOL",
+ "ANEX": "AstroNexus",
"ANGEL": "Crypto Angel",
"ANGL": "Angel Token",
"ANGLE": "ANGLE",
@@ -625,8 +669,10 @@
"ANI": "Anime Token",
"ANIM": "Animalia",
"ANIMA": "Realm Anima",
- "ANIME": "Anime",
+ "ANIME": "Animecoin",
"ANIMECOIN": "Animecoin",
+ "ANIMEONBASE": "Anime",
+ "ANITA": "Anita AI",
"ANJ": "Aragon Court",
"ANJI": "Anji",
"ANK": "AlphaLink",
@@ -637,6 +683,7 @@
"ANKRETH": "Ankr Staked ETH",
"ANKRFTM": "Ankr Staked FTM",
"ANKRMATIC": "Ankr Staked MATIC",
+ "ANLOG": "Analog",
"ANML": "Animal Concerts",
"ANN": "Annex Finance",
"ANON": "ANON",
@@ -667,6 +714,7 @@
"AOS": "AOS",
"AOT": "Age of Tanks",
"AP": "AppleSwap AI",
+ "AP3X": "Apex token",
"APAD": "Anypad",
"APC": "AlpaCoin",
"APCG": "ALLPAYCOIN",
@@ -675,6 +723,7 @@
"APED": "Baddest Alpha Ape Bundle",
"APEDEV": "The dev is an Ape",
"APEFUN": "Ape",
+ "APEMAN": "APEMAN",
"APEPE": "Ape and Pepe",
"APES": "APES",
"APETARDIO": "Apetardio",
@@ -743,13 +792,14 @@
"ARBS": "Arbswap",
"ARBT": "ARBITRAGE",
"ARBUZ": "ARBUZ",
- "ARC": "Arc",
+ "ARC": "AI Rig Complex",
"ARCA": "Legend of Arcadia",
"ARCAD": "Arcadeum",
"ARCADE": "ARCADE",
"ARCADECITY": "Arcade City",
"ARCADEF": "arcadefi",
"ARCADEN": "ArcadeNetwork",
+ "ARCAI": "ARCAI",
"ARCANE": "Arcane Token",
"ARCAS": "Arcas",
"ARCH": "Archway",
@@ -757,6 +807,7 @@
"ARCHCOIN": "ArchCoin",
"ARCHE": "Archean",
"ARCHIVE": "Chainback",
+ "ARCINTEL": "Arc",
"ARCO": "AquariusCoin",
"ARCONA": "Arcona",
"ARCT": "ArbitrageCT",
@@ -849,6 +900,7 @@
"ASD": "AscendEX Token",
"ASDEX": "AstraDEX",
"ASEED": "aUSD SEED (Acala)",
+ "ASF": "Asymmetry Finance Token",
"ASG": "Asgard",
"ASGC": "ASG",
"ASH": "ASH",
@@ -858,6 +910,7 @@
"ASIMI": "ASIMI",
"ASIX": "ASIX+",
"ASK": "Permission Coin",
+ "ASKAI": "ASKAI",
"ASKO": "Asko",
"ASM": "Assemble Protocol",
"ASMO": "AS Monaco Fan Token",
@@ -884,6 +937,7 @@
"ASTRAFER": "Astrafer",
"ASTRAFERV1": "Astrafer v1",
"ASTRAL": "Astral",
+ "ASTRALAB": "Astra Labs",
"ASTRO": "Astroport",
"ASTROC": "Astroport Classic",
"ASTROLION": "AstroLion",
@@ -925,6 +979,7 @@
"ATLX": "Atlantis Loans Polygon",
"ATM": "Atletico de Madrid Fan Token",
"ATMA": "ATMA",
+ "ATMBSC": "ATM",
"ATMC": "Autumncoin",
"ATMCHAIN": "ATMChain",
"ATMCOIN": "ATM",
@@ -985,6 +1040,8 @@
"AUTHORSHIP": "Authorship",
"AUTISM": "AUTISM",
"AUTO": "Auto",
+ "AUTOMATIC": "Automatic Treasury Machine",
+ "AUTONO": "Autonomi",
"AUTUMN": "Autumn",
"AUVERSE": "AuroraVerse",
"AUX": "Auxilium",
@@ -1003,6 +1060,7 @@
"AVAV": "AVAV",
"AVAV1": "AVA v1",
"AVAX": "Avalanche",
+ "AVAXAI": "AIvalanche DeFAI Agents",
"AVAXIOU": "Avalanche IOU",
"AVB": "Autonomous Virtual Beings",
"AVDO": "AvocadoCoin",
@@ -1025,6 +1083,7 @@
"AVTM": "Aventis Metaverse",
"AVXL": "Avaxlauncher",
"AVXT": "Avaxtars Token",
+ "AWARE": "ChainAware.ai",
"AWAX": "AWAX",
"AWC": "Atomic Wallet Coin",
"AWK": "Awkward Monkey Base",
@@ -1068,6 +1127,7 @@
"AZA": "Kaliza",
"AZART": "Azart",
"AZBI": "AZBI CORE",
+ "AZER": "Azerop",
"AZERO": "Aleph Zero",
"AZIT": "Azit",
"AZR": "Azure",
@@ -1087,7 +1147,8 @@
"B2G": "Bitcoiin2Gen",
"B2M": "Bit2Me",
"B2X": "SegWit2x",
- "B3": "B3 Coin",
+ "B3": "B3",
+ "B3COIN": "B3 Coin",
"B3X": "Bnext Token",
"B91": "B91",
"BA": "BAHA",
@@ -1108,6 +1169,9 @@
"BABYBOME": "Book of Baby Memes",
"BABYBOMEOW": "Baby of BOMEOW",
"BABYBONK": "Baby Bonk",
+ "BABYBROC": "Baby Broccoli",
+ "BABYBROCCOL": "Baby Broccoli",
+ "BABYBROCCOLI": "BabyBroccoli",
"BABYBTC": "BABYBTC",
"BABYC": "Baby Cat",
"BABYCAT": "Baby Cat Coin",
@@ -1121,6 +1185,7 @@
"BABYD": "Baby Dragon",
"BABYDENG": "Baby Moo Deng",
"BABYDOGE": "BabyDoge",
+ "BABYDOGE2": "Baby Doge 2.0",
"BABYDOGEINU": "BABY DOGE INU",
"BABYDOGEZILLA": "BabyDogeZilla",
"BABYDRAGON": "Baby Dragon",
@@ -1141,6 +1206,7 @@
"BABYJERRY": "Baby Jerry",
"BABYJESUS": "BabyJesusCoin",
"BABYKABOSU": "Baby Kabosu",
+ "BABYKEKIUS": "Baby Kekius Maximus",
"BABYKITTY": "BabyKitty",
"BABYLONG": "Baby Long",
"BABYM": "BabyMAGA",
@@ -1153,6 +1219,7 @@
"BABYMUSK": "Baby Musk",
"BABYMYRO": "Babymyro",
"BABYNEIRO": "Baby Neiro",
+ "BABYNEIROB": "Baby Neiro",
"BABYOKX": "BABYOKX",
"BABYP": "BabyPepe",
"BABYPEIPEI": "Baby PeiPei",
@@ -1170,6 +1237,7 @@
"BABYSHIRO": "Baby Shiro Neko",
"BABYSHIV": "Baby Shiva",
"BABYSLERF": "BabySlerf",
+ "BABYSNAKE": "Baby Snake BSC",
"BABYSOL": "Baby Solana",
"BABYSORA": "Baby Sora",
"BABYSWEEP": "BabySweep",
@@ -1189,6 +1257,7 @@
"BACON": "BaconDAO (BACON)",
"BAD": "Bad Idea AI",
"BADA": "Bad Alien Division",
+ "BADAI": "BAD Coin",
"BADC": "BADCAT",
"BADCAT": "Andy’s Alter Ego",
"BADGER": "Badger DAO",
@@ -1196,6 +1265,7 @@
"BAG": "Bag",
"BAGS": "Basis Gold Share",
"BAHAMAS": "Bahamas",
+ "BAHIA": "Esporte Clube Bahia Fan Token",
"BAI": "BearAI",
"BAICA": "Baica",
"BAJU": "Bajun Network",
@@ -1209,6 +1279,7 @@
"BAKT": "Backed Protocol",
"BAL": "Balancer",
"BALA": "Shambala",
+ "BALANCE": "Balance AI",
"BALD": "Bald",
"BALIN": "Balin Bank",
"BALL": "Game 5 BALL",
@@ -1220,9 +1291,11 @@
"BAMA": "BabyAMA",
"BAMBIT": "BAMBIT",
"BAMBOO": "BambooDeFi",
+ "BAMF": "BAMF",
"BAMITCOIN": "Bamit",
"BAN": "Comedian",
"BANANA": "Banana Gun",
+ "BANANACHARITY": "BANANA",
"BANANAF": "Banana For Scale",
"BANANAS": "Monkey Peepo",
"BANANO": "Banano",
@@ -1272,6 +1345,7 @@
"BASEDP": "Based Pepe",
"BASEDR": "Based Rabbit",
"BASEDS": "BasedSwap",
+ "BASEDTURBO": "Based Turbo",
"BASEDV1": "Based Money v1",
"BASEHEROES": "Baseheroes",
"BASEPROTOCOL": "Base Protocol",
@@ -1309,6 +1383,7 @@
"BBCH": "Binance Wrapped BCH",
"BBCT": "TraDove B2BCoin",
"BBDC": "Block Beats Network",
+ "BBDOGITO": "BabyBullDogito",
"BBDT": "BBD Token",
"BBEER": "BABY BEERCOIN",
"BBF": "Bubblefong",
@@ -1325,6 +1400,7 @@
"BBP": "BiblePay",
"BBR": "Boolberry",
"BBRETT": "Baby Brett",
+ "BBROCCOLI": "Baby Broccoli",
"BBS": "BBSCoin",
"BBSOL": "Bybit Staked SOL",
"BBT": "BitBook",
@@ -1387,12 +1463,14 @@
"BDAY": "Birthday Cake",
"BDB": "Big Data Block",
"BDC": "BILLION•DOLLAR•CAT",
+ "BDCA": "BitDCA",
"BDCC": "BDCC COIN",
"BDCLBSC": "BorderCollieBSC",
"BDG": "BitDegree",
"BDID": "BDID",
"BDL": "Bitdeal",
"BDOG": "Bulldog Token",
+ "BDOGITO": "BullDogito",
"BDOT": "Binance Wrapped DOT",
"BDP": "Big Data Protocol",
"BDPI": "Interest Bearing Defi Pulse Index",
@@ -1408,8 +1486,10 @@
"BEAMMW": "Beam",
"BEAN": "Bean",
"BEANS": "Moonbeans",
+ "BEARIN": "Bear in Bathrobe",
"BEAST": "MrBeast",
"BEAT": "BEAT Token",
+ "BEATAI": "eBeat AI",
"BEATLES": "JohnLennonC0IN",
"BEATS": "Sol Beats",
"BEBE": "BEBE",
@@ -1422,6 +1502,7 @@
"BEE": "Herbee",
"BEEF": "PepeBull",
"BEEG": "Beeg Blue Whale",
+ "BEENZ": "BEENZ",
"BEEP": "BEEP",
"BEER": "BEERCOIN",
"BEERUSCAT": "BeerusCat",
@@ -1466,6 +1547,9 @@
"BEPE": "Blast Pepe",
"BEPR": "Blockchain Euro Project",
"BEPRO": "BEPRO Network",
+ "BERA": "Berachain",
+ "BERAETH": "Berachain Staked ETH",
+ "BERASTONE": "StakeStone Berachain Vault Token",
"BERF": "BERF",
"BERG": "Bloxberg",
"BERN": "BERNcash",
@@ -1513,17 +1597,22 @@
"BFT": "BF Token",
"BFTB": "Brazil Fan Token",
"BFTC": "BITS FACTOR",
+ "BFWOG": "Based Fwog (basedfwog.info)",
"BFX": "BitFinex Tokens",
"BG": "BunnyPark Game",
"BGB": "Bitget token",
"BGBP": "Binance GBP Stable Coin",
"BGBV1": "Bitget Token v1",
"BGC": "Bee Token",
+ "BGCI": "Bloomberg Galaxy Crypto Index",
+ "BGEO": "BGEO",
"BGG": "BGG Token",
"BGLD": "Based Gold",
"BGONE": "BigONE Token",
"BGPT": "BlockGPT",
+ "BGR": "Bitgrit",
"BGS": "Battle of Guardians Share",
+ "BGSC": "BugsCoin",
"BGSOL": "Bitget SOL Staking",
"BGUY": "The Big Guy",
"BGVT": "Bit Game Verse Token",
@@ -1531,7 +1620,8 @@
"BHAT": "BH Network",
"BHAX": "Bithashex",
"BHBD": "bHBD",
- "BHC": "BillionHappiness",
+ "BHC": "Billion Happiness",
+ "BHCV1": "Billion Happiness v1",
"BHEROES": "BombHeroes coin",
"BHIG": "BuckHathCoin",
"BHIRE": "BitHIRE",
@@ -1542,6 +1632,7 @@
"BIAO": "Biaocoin",
"BIB": "BIB Token",
"BIBI": "BIBI",
+ "BIBI2025": "Bibi",
"BIBL": "Biblecoin",
"BIBO": "Bible of Memes",
"BIC": "Bikercoins",
@@ -1561,9 +1652,12 @@
"BIDZV1": "BIDZ Coin v1",
"BIFI": "Beefy.Finance",
"BIFIF": "BiFi",
+ "BIFIV1": "Beefy v1",
"BIG": "Big Eyes",
+ "BIGBALLS": "Edward Coristine",
"BIGBANGCORE": "BigBang Core",
"BIGCOIN": "BigCoin",
+ "BIGFACTS": "BIGFACTS",
"BIGFOOT": "BigFoot Town",
"BIGHAN": "BighanCoin",
"BIGLEZ": "THE BIG LEZ SHOW",
@@ -1591,7 +1685,7 @@
"BINS": "Bitsense",
"BINTEX": "Bintex Futures",
"BINU": "Blast Inu",
- "BIO": "BITONE",
+ "BIO": "Bio Protocol",
"BIOB": "BioBar",
"BIOC": "BioCrypt",
"BIOCOIN": "Biocoin",
@@ -1606,6 +1700,7 @@
"BIRB": "Birb",
"BIRD": "Bird.Money",
"BIRDCHAIN": "Birdchain",
+ "BIRDD": "BIRD DOG",
"BIRDDOG": "Bird Dog",
"BIRDO": "Bird Dog",
"BIS": "Bismuth",
@@ -1630,6 +1725,7 @@
"BITCNY": "bitCNY",
"BITCO": "Bitcoin Black Credit Card",
"BITCOINC": "Bitcoin Classic",
+ "BITCOINOTE": "BitcoiNote",
"BITCOINP": "Bitcoin Private",
"BITCOINV": "BitcoinV",
"BITCONNECT": "BitConnect Coin",
@@ -1646,6 +1742,7 @@
"BITNEW": "BitNewChain",
"BITO": "BitoPro Exchange Token",
"BITOK": "BitOKX",
+ "BITONE": "BITONE",
"BITORB": "BitOrbit",
"BITRA": "Bitratoken",
"BITRADIO": "Bitradio",
@@ -1667,7 +1764,9 @@
"BITVOLT": "BitVolt",
"BITWORLD": "Bit World Token",
"BITX": "BitScreener",
- "BITZ": "Bitz Coin",
+ "BITXOXO": "Bitxoxo",
+ "BITZ": "MARBITZ",
+ "BITZBIZ": "Bitz Coin",
"BIUT": "Bit Trust System",
"BIVE": "BIZVERSE",
"BIX": "BiboxCoin",
@@ -1771,11 +1870,12 @@
"BLTV": "BLTV Token",
"BLU": "BlueCoin",
"BLUB": "BLUB",
- "BLUE": "Blue Protocol",
+ "BLUE": "Bluefin",
"BLUEBUTT": "BLUE BUTT CHEESE",
"BLUEG": "Blue Guy",
"BLUEM": "BlueMove",
"BLUEN": "Blue Norva",
+ "BLUEPROTOCOL": "Blue Protocol",
"BLUES": "Blueshift",
"BLUESC": "BluesCrypto",
"BLUESPARROW": "BlueSparrow Token",
@@ -1825,6 +1925,7 @@
"BNA": "BananaTok",
"BNANA": "Chimpion",
"BNB": "Binance Coin",
+ "BNBAI": "BNB Agents",
"BNBBONK": "BNB BONK",
"BNBBUNNY": "BNB BUNNY",
"BNBCAT": "BNBcat",
@@ -1840,12 +1941,14 @@
"BNBLION": "BNB LION",
"BNBOLYMPIC": "BNB OLYMPIC",
"BNBP": "BNBPot",
+ "BNBPRINTER": "BNBPrinter",
"BNBSNAKE": "BNB SNAKE",
"BNBSONGOKU": "BNBsongoku",
"BNBTC": "BNbitcoin",
"BNBVEGETA": "BNB VEGETA",
"BNBWHALES": "BNB Whales",
"BNBX": "Stader BNBx",
+ "BNBXBT": "BNBXBT",
"BNC": "Bifrost Native Coin",
"BND": "Bened",
"BNF": "BonFi",
@@ -1871,6 +1974,7 @@
"BNU": "ByteNext",
"BNUSD": "Balanced Dollars",
"BNX": "BinaryX",
+ "BNXV1": "BinaryX v1",
"BNY": "TaskBunny",
"BOA": "BOSAGORA",
"BOAI": "BOLICAI",
@@ -1887,11 +1991,13 @@
"BOBER": "BOBER",
"BOBFUN": "BOB",
"BOBLS": "Boblles",
+ "BOBMARLEY": "Bob Marley Meme",
"BOBO": "BOBO",
"BOBOT": "Bobo The Bear",
"BOBS": "Bob's Repair",
"BOBT": "BOB Token",
"BOBUKI": "Bobuki Neko",
+ "BOBY": "BOBY",
"BOC": "BOCOIN",
"BOCA": "BookOfPussyCats",
"BOCAC": "BocaChica token",
@@ -1923,6 +2029,7 @@
"BOLD": "Bold",
"BOLI": "BolivarCoin",
"BOLT": "Bolt",
+ "BOLTAI": "Bolt AI",
"BOLTT": "BolttCoin",
"BOM": "Book Of Matt Furie",
"BOMA": "Book of Maga",
@@ -1938,12 +2045,14 @@
"BOMES": "BOOK OF MEMES",
"BOMET": "BOME TRUMP",
"BOMK": "BOMK",
+ "BOMT": "Baby One More Time",
"BON": "Bonpay",
"BONA": "Bonafi",
"BOND": "BarnBridge",
"BONDAPPETIT": "BondAppetit",
"BONDLY": "Bondly",
"BONDLYV1": "Bondly Finance",
+ "BONDX": "BondX",
"BONE": "Bone ShibaSwap",
"BONES": "Moonshots Farm",
"BONESCOIN": "BonesCoin",
@@ -1973,7 +2082,9 @@
"BOO": "Spookyswap",
"BOOB": "BooBank",
"BOOE": "Book of Ethereum",
+ "BOOF": "Boofus by Virtuals",
"BOOFI": "Boo Finance",
+ "BOOG": "BOOG base",
"BOOK": "Solbook",
"BOOKIE": "BookieBot",
"BOOKO": "Book of Pets",
@@ -1997,6 +2108,7 @@
"BORA": "BORA",
"BORED": "Bored Museum",
"BORG": "SwissBorg",
+ "BORGY": "BORGY",
"BORING": "BoringDAO",
"BORK": "Bork",
"BORKIE": "Borkie",
@@ -2015,11 +2127,13 @@
"BOSU": "Bosu Inu",
"BOT": "Bot Planet",
"BOTC": "BotChain",
+ "BOTIFY": "BOTIFY",
"BOTS": "ArkDAO",
"BOTTO": "Botto",
"BOTX": "BOTXCOIN",
"BOU": "Boulle",
"BOUNCE": "Bounce Token",
+ "BOUNTY": "ChainBounty",
"BOUTS": "BoutsPro",
"BOW": "Archer Swap",
"BOWE": "Book of Whales",
@@ -2074,6 +2188,7 @@
"BR": "BOHR",
"BR34P": "BR34P",
"BRACE": "Bitci Racing Token",
+ "BRAI": "Brain Frog",
"BRAIN": "BrainCoin",
"BRAINERS": "Brainers",
"BRAINLET": "Brainlet",
@@ -2135,6 +2250,14 @@
"BRNK": "Brank",
"BRNX": "Bronix",
"BRO": "Bro the cat",
+ "BROC": "Broccoli (broc.wtf)",
+ "BROCC": "Broccoli",
+ "BROCCO": "Broccoli (firstbroccoli.com)",
+ "BROCCOL": "Broccoli (broccolibsc.com)",
+ "BROCCOLI": "CZ'S Dog (broccoli.gg)",
+ "BROCCOLIBNB": "BROCCOLI (broccolibnb.xyz)",
+ "BROCCOLICZ": "Broccoli (broccoli_cz)",
+ "BROCCOLIVIP": "Broccoli(broccoli.vip)",
"BROCK": "Bitrock",
"BROGG": "Brett's Dog",
"BROKE": "Broke Again",
@@ -2144,6 +2267,7 @@
"BROTHER": "BROTHER",
"BROWN": "BrowniesSwap",
"BROZ": "Brozinkerbell",
+ "BRP": "BananaRepublic",
"BRRR": "Burrow",
"BRS": "Broovs Projects",
"BRT": "Bikerush",
@@ -2163,6 +2287,7 @@
"BS": "BlackShadowCoin",
"BSAFE": "BlockSafe",
"BSAFU": "BlockSAFU",
+ "BSAI": "Bitcoin Silver AI",
"BSATOSHI": "BabySatoshi",
"BSB": "Based Street Bets",
"BSC": "BSC Layer",
@@ -2179,6 +2304,7 @@
"BSCV": "Bscview",
"BSDETH": "Based ETH",
"BSE": "base season",
+ "BSEN": "Baby Sen by Sentio",
"BSEND": "BitSend",
"BSFM": "BABY SAFEMOON",
"BSG": "Baby Squid Game",
@@ -2191,6 +2317,7 @@
"BSKT": "BasketCoin",
"BSL": "BankSocial",
"BSOL": "BlazeStake Staked SOL",
+ "BSOP": "Bsop",
"BSOV": "BitcoinSoV",
"BSP": "BallSwap",
"BSPM": "Bitcoin Supreme",
@@ -2228,6 +2355,7 @@
"BTCA": "BITCOIN ADDITIONAL",
"BTCAB": "Bitcoin Avalanche Bridged",
"BTCACT": "BITCOIN Act",
+ "BTCAI": "BTC AI Agent",
"BTCAS": "BitcoinAsia",
"BTCAT": "Bitcoin Cat",
"BTCB": "Bitcoin BEP2",
@@ -2249,7 +2377,7 @@
"BTCL": "BTC Lite",
"BTCM": "BTCMoon",
"BTCMT": "Minto",
- "BTCN": "BitcoiNote",
+ "BTCN": "Bitcorn",
"BTCNOW": "Blockchain Technology Co.",
"BTCP": "Bitcoin Palladium",
"BTCPAY": "Bitcoin Pay",
@@ -2258,6 +2386,7 @@
"BTCRED": "Bitcoin Red",
"BTCRY": "BitCrystal",
"BTCS": "Bitcoin Scrypt",
+ "BTCSR": "BTC Strategic Reserve",
"BTCST": "BTC Standard Hashrate Token",
"BTCT": "Bitcoin Token",
"BTCUS": "Bitcoinus",
@@ -2278,6 +2407,7 @@
"BTL": "Bitlocus",
"BTLC": "BitLuckCoin",
"BTM": "Bytom",
+ "BTMETA": "BTCASH",
"BTMG": "Bitcademy Football",
"BTMI": "BitMiles",
"BTMK": "BitMark",
@@ -2288,6 +2418,7 @@
"BTNYX": "BitOnyx Token",
"BTO": "Bottos",
"BTOP": "Botopia.Finance",
+ "BTORO": "Bitoro Network",
"BTP": "Bitpaid",
"BTPL": "Bitcoin Planet",
"BTQ": "BitQuark",
@@ -2300,6 +2431,7 @@
"BTRS": "Bitball Treasure",
"BTRST": "Braintrust",
"BTRU": "Biblical Truth",
+ "BTRUMP": "Baron Trump",
"BTS": "Bitshares",
"BTSC": "BTS Chain",
"BTSE": "BTSE Token",
@@ -2345,19 +2477,23 @@
"BUGG": "Bugg Inu",
"BUGS": "Bugs Bunny",
"BUIDL": "Starter.xyz",
+ "BUIL": "BUILD",
"BUILD": "BuildAI",
"BUILDIN": "Buildin Token",
"BUILDTEAM": "BuildTeam",
+ "BUILT": "Built Different",
"BUK": "CryptoBuk",
"BUL": "bul",
"BULDAK": "Buldak",
"BULEI": "Bulei",
- "BULL": "Bullieverse",
+ "BULL": "Tron Bull",
+ "BULLBEAR": "BullBear AI",
"BULLC": "BuySell",
"BULLF": "BULL FINANCE",
"BULLI": "Bullish On Ethereum",
+ "BULLIEVERSE": "Bullieverse",
"BULLINU": "Bull inu",
- "BULLION": "BullionFX",
+ "BULLIONFX": "BullionFX",
"BULLISH": "bullish",
"BULLMOON": "Bull Moon",
"BULLPEPE": "Bullpepe",
@@ -2373,7 +2509,9 @@
"BUND": "Bund V2.0",
"BUNDL": "Bundl Tools",
"BUNI": "Bunicorn",
- "BUNN": "Bunni",
+ "BUNKER": "BunkerCoin",
+ "BUNNI": "Bunni",
+ "BUNNIV1": "Timeless",
"BUNNY": "Pancake Bunny",
"BUNNYINU": "Bunny Inu",
"BUNNYM": "BUNNY MEV BOT",
@@ -2394,6 +2532,8 @@
"BUSY": "Busy DAO",
"BUT": "BitUP Token",
"BUTT": "Buttercat",
+ "BUTTCOIN": "The Next Bitcoin",
+ "BUTTHOLE": "Butthole Coin",
"BUX": "BUX",
"BUXCOIN": "Buxcoin",
"BUY": "Burency",
@@ -2484,6 +2624,7 @@
"CAI": "Cai Token",
"CAID": "ClearAid",
"CAIR": "Crypto-AI-Robo.com",
+ "CAIV": "CARVIS",
"CAIX": "CAIx",
"CAIZ": "Caizcoin",
"CAKE": "PancakeSwap",
@@ -2499,6 +2640,7 @@
"CAM": "Consumption Avatar Matrix",
"CAMC": "Camcoin",
"CAMEL": "The Camel",
+ "CAMINO": "Camino Network",
"CAMLY": "Camly Coin",
"CAMP": "Camp",
"CAMT": "CAMELL",
@@ -2522,8 +2664,10 @@
"CAPRICOIN": "CapriCoin",
"CAPS": "Ternoa",
"CAPT": "Bitcoin Captain",
+ "CAPTAINBNB": "CaptainBNB",
"CAPTAINPLANET": "Captain Planet",
"CAPY": "Capybara",
+ "CAPYBARA": "Capybara",
"CAR": "CarBlock",
"CARAT": "Carats Token",
"CARBO": "CleanCarbon",
@@ -2540,7 +2684,8 @@
"CAROL": "CAROLToken",
"CARPE": "CarpeDiemCoin",
"CARR": "Carnomaly",
- "CARROT": "CarrotSwap",
+ "CARROT": "Carrot by Puffer",
+ "CARROTSWAP": "CarrotSwap",
"CART": "CryptoArt.Ai",
"CARTAXI": "CarTaxi",
"CARTERCOIN": "CarterCoin",
@@ -2577,6 +2722,7 @@
"CATELON": "CatElonMars",
"CATEX": "CATEX",
"CATFISH": "Catfish",
+ "CATG": "Crypto Agent Trading",
"CATGAME": "Cookie Cat Game",
"CATGIRL": "Catgirl",
"CATGOKU": "Catgoku",
@@ -2602,6 +2748,7 @@
"CATSY": "CAT SYLVESTER",
"CATT": "Catex",
"CATTO": "Cat Token",
+ "CATTON": "Catton AI",
"CATVAX": "Catvax",
"CATVILLS": "Catvills Coin",
"CATW": "Cat wif Hands",
@@ -2722,6 +2869,7 @@
"CEODOGE": "CEO DOGE",
"CERBER": "CERBEROGE",
"CERE": "Cere Network",
+ "CEREB": "Cerebrum",
"CERES": "Ceres",
"CES": "swap.coffee",
"CESC": "Crypto Escudo",
@@ -2748,6 +2896,7 @@
"CFXT": "Chainflix",
"CFun": "CFun",
"CGA": "Cryptographic Anomaly",
+ "CGAI": "GDAI Agent",
"CGAR": "CryptoGuards",
"CGG": "Chain Guardians",
"CGL": "Crypto Gladiator Shards",
@@ -2757,8 +2906,11 @@
"CGPU": "CloudGPU",
"CGS": "Crypto Gladiator Shards",
"CGT": "Coin Gabbar Token",
+ "CGTV1": "Curio Governance",
+ "CGTV2": "Curio Gas Token",
"CGU": "Crypto Gaming United",
"CGV": "Cogito Finance",
+ "CGX": "Forkast",
"CHA": "Charity Coin",
"CHACHA": "Chacha",
"CHAD": "Chad Coin",
@@ -2789,6 +2941,7 @@
"CHAT": "Solchat",
"CHATAI": "ChatAI Token",
"CHATGPT": "AI Dragon",
+ "CHATOSHI": "chAtoshI",
"CHATTY": "ChatGPT's Mascot",
"CHB": "COINHUB TOKEN",
"CHBR": "CryptoHub",
@@ -2810,7 +2963,8 @@
"CHEESEBALL": "Cheeseball the Wizard",
"CHEESECOIN": "Cheesecoin",
"CHEESUS": "Cheesus",
- "CHEF": "Chefdotfun",
+ "CHEF": "CoinChef",
+ "CHEFDOTFUN": "Chefdotfun",
"CHENG": "Chengshi",
"CHEQ": "CHEQD Network",
"CHER": "Cherry Network",
@@ -2841,8 +2995,10 @@
"CHILDAI": "Singularity's Child gonzoai",
"CHILI": "CHILI",
"CHILL": "ChillPill",
+ "CHILLAX": "Chillax",
"CHILLGUY": "Chill Guy",
"CHIM": "Chimera",
+ "CHINA": "China Coin",
"CHINAU": "Chinau",
"CHINAZILLA": "ChinaZilla",
"CHINGON": "Mexico Chingon",
@@ -2875,9 +3031,11 @@
"CHOPPER": "Chopper Inu",
"CHOPPY": "Choppy",
"CHORIZO": "Chorizo",
+ "CHORUZ": "Choruz AI",
"CHOW": "Chow Chow Finance",
"CHOY": "Bok Choy",
"CHP": "CoinPoker Token",
+ "CHPD": "Chirppad",
"CHR": "Chroma",
"CHRETT": "Chinese BRETT",
"CHRISPUMP": "Christmas Pump",
@@ -2950,11 +3108,13 @@
"CLAY": "Clay Nation",
"CLB": "Cloudbric",
"CLBR": "Colibri Protocol",
+ "CLBTC": "clBTC",
"CLCT": "CollectCoin",
"CLD": "Cloud",
"CLDX": "Cloverdex",
- "CLEAR": "Clear Water",
+ "CLEAR": "Everclear",
"CLEARPOLL": "ClearPoll",
+ "CLEARWATER": "Clear Water",
"CLEG": "Chain of Legends",
"CLEO": "Cleo Tech",
"CLEV": "CLever Token",
@@ -2993,6 +3153,8 @@
"CLU": "CluCoin",
"CLUB": "ClubCoin",
"CLUD": "CludCoin",
+ "CLUSTR": "Clustr Labs",
+ "CLUTCH": "Clutch",
"CLV": "Clover Finance",
"CLVA": "Clever DeFi",
"CLVX": "Calvex",
@@ -3067,6 +3229,8 @@
"COCK": "Shibacock",
"COCO": "COCO COIN",
"COCONUT": "Coconut",
+ "COCOR": "Cocoro",
+ "COCORO": "Cocoro",
"COD": "Chief of Deswamp",
"CODA": "CODA",
"CODAI": "CODAI",
@@ -3088,6 +3252,7 @@
"COGI": "COGI",
"COGS": "Cogmento",
"COI": "Coinnec",
+ "COINAI": "Coinbase AI Agent",
"COINB": "Coinbidex",
"COINBT": "CoinBot",
"COINDEFI": "Coin",
@@ -3103,10 +3268,12 @@
"COJ": "Cojam",
"COK": "Cat Own Kimono",
"COKE": "Cocaine Cowboy Shards",
+ "COKEONS": "Coke on Sol",
"COL": "Clash of Lilliput",
"COLA": "Cola",
"COLL": "Collateral Pay",
"COLLAR": "PolyPup Finance",
+ "COLLAT": "Collaterize",
"COLLE": "Collective Care",
"COLLEA": "Colle AI",
"COLLECT": "CoinCollect",
@@ -3133,6 +3300,7 @@
"COMT": "Community Token",
"CONAN": "Conan",
"CONC": "Concentrator",
+ "CONCHO": "Sapo Concho",
"CONDENSATE": "Condensate",
"CONDO": "CONDO",
"CONE": "BitCone",
@@ -3176,6 +3344,7 @@
"CORN": "CORN",
"CORNELLA": "CORNELLA",
"CORSI": "Cane Corso",
+ "CORTEX": "Cortex Protocol",
"CORX": "CorionX",
"COS": "Contentos",
"COSHI": "CoShi Inu",
@@ -3239,6 +3408,7 @@
"CPTN": "Captain Max",
"CPU": "CPUcoin",
"CPX": "Apex Token",
+ "CPXTB": "Coin Prediction Tool On Base",
"CPY": "COPYTRACK",
"CQST": "ConquestCoin",
"CQT": "Covalent",
@@ -3334,9 +3504,11 @@
"CRONA": "CronaSwap",
"CRONK": "CRONK",
"CROPPER": "CropperFinance",
+ "CROW": "cr0w by Virtuals",
"CROWD": "CrowdCoin",
"CROWDWIZ": "Crowdwiz",
"CROWN": "Crown by Third Time Games",
+ "CROWWITH": "crow with knife",
"CROX": "CroxSwap",
"CRP": "Crypton",
"CRPS": "CryptoPennies",
@@ -3367,16 +3539,21 @@
"CRYP": "CrypticCoin",
"CRYPT": "CryptCoin",
"CRYPTER": "Crypteriumcoin",
+ "CRYPTOA": "CryptoAI",
+ "CRYPTOAGENT": "CRYPTO AGENT TRUMP",
"CRYPTOAI": "CryptoAI",
"CRYPTOB": "Crypto Burger",
"CRYPTOBEAST": "CryptoBeast",
"CRYPTOBL": "CryptoBlades Kingdoms",
+ "CRYPTOBR": "Crypto Bro",
"CRYPTOBULLION": "CryptoBullion",
+ "CRYPTODELIVERY": "Crypto Delivery",
"CRYPTOE": "Cryptoenter",
"CRYPTOEM": "Crypto Emperor Trump",
"CRYPTOF": "CryptoFarmers",
"CRYPTOH": "CryptoHunterTrading",
"CRYPTOJ": "Crypto Journey",
+ "CRYPTOJESUS": "Crypto Jesus Trump",
"CRYPTON": "CRYPTON",
"CRYPTONITE": "Cryptonite",
"CRYPTOOFFICIAL": "Crypto",
@@ -3437,6 +3614,7 @@
"CTLX": "Cash Telex",
"CTN": "Continuum Finance",
"CTO": "BaseCTO",
+ "CTOAI": "ClustroAI",
"CTOK": "Codyfight",
"CTP": "Ctomorrow Platform",
"CTPL": "Cultiplan",
@@ -3455,9 +3633,10 @@
"CU": "Crypto Unicorns",
"CUAN": "CuanSwap.com",
"CUB": "Cub Finance",
- "CUBE": "Cube Network",
+ "CUBE": "Somnium Space CUBEs",
"CUBEAUTO": "Cube",
"CUBEB": "CubeBase",
+ "CUBENETWORK": "Cube Network",
"CUCCI": "Cat in Gucci",
"CUCK": "Cuckadoodledoo",
"CUDOS": "Cudos",
@@ -3475,7 +3654,6 @@
"CURA": "Cura Network",
"CURE": "Curecoin",
"CURI": "Curium",
- "CURIO": "Curio Governance",
"CURLY": "Curly",
"CURR": "Curry",
"CURRY": "CurrySwap",
@@ -3517,6 +3695,7 @@
"CWAR": "Cryowar Token",
"CWBTC": "Compound Wrapped BTC",
"CWD": "CROWD",
+ "CWDV1": "Linkflow",
"CWEB": "Coinweb",
"CWEX": "Crypto Wine Exchange",
"CWIF": "catwifhat",
@@ -3572,7 +3751,9 @@
"CYS": "BlooCYS",
"CYT": "Cryptokenz",
"CZ": "CHANGPENG ZHAO (changpengzhao.club)",
+ "CZBROCCOLI": "Cz Broccoli",
"CZC": "Crazy Coin",
+ "CZDOG": "CZ Dog",
"CZF": "CZodiac Farming Token",
"CZGOAT": "CZ THE GOAT",
"CZKING": "CZKING",
@@ -3582,7 +3763,6 @@
"CZSHARES": "CZshares",
"CZUSD": "CZUSD",
"CZZ": "ClassZZ",
- "D": "Denarius",
"D11": "DeFi11",
"D2O": "DAM Finance",
"D2T": "Dash 2 Trade",
@@ -3611,6 +3791,7 @@
"DAFI": "Dafi Protocol",
"DAFT": "DaftCoin",
"DAG": "Constellation",
+ "DAGESTAN": "Dagestan And Forget",
"DAGO": "Dago Mining",
"DAGS": "Dagcoin",
"DAGT": "Digital Asset Guarantee Token",
@@ -3623,6 +3804,7 @@
"DAIQ": "Daiquilibrium",
"DAISY": "Daisy Launch Pad",
"DAK": "dak",
+ "DAKU": "Der Daku",
"DAL": "DAOLaunch",
"DALI": "Dalichain",
"DALMA": "Dalma Inu",
@@ -3633,6 +3815,7 @@
"DAMOON": "Damoon Coin",
"DAN": "Daneel",
"DANA": "Ardana",
+ "DANCING": "Dancing Michi",
"DANG": "Guangdang",
"DANGEL": "dAngel Fund",
"DANJ": "Danjuan Cat",
@@ -3661,10 +3844,12 @@
"DARED": "Daredevil Dog",
"DARICO": "Darico",
"DARIK": "Darik",
- "DARK": "Dark",
+ "DARK": "Dark Frontiers",
+ "DARKCOIN": "Dark",
"DARKEN": "Dark Energy Crystals",
"DARKMAGACOIN": "DARK MAGA",
"DARKT": "Dark Trump",
+ "DARKTOKEN": "DarkToken",
"DART": "dART Insurance",
"DARX": "Bitdaric",
"DAS": "DAS",
@@ -3675,6 +3860,7 @@
"DASIAv": "DASIA",
"DAT": "Datum",
"DATA": "Streamr",
+ "DATAB": "Databot",
"DATAO": "Data Ownership Protocol",
"DATAWALLET": "DataWallet",
"DATOM": "Drop Staked ATOM",
@@ -3740,7 +3926,7 @@
"DCN": "Dentacoin",
"DCNT": "Decanect",
"DCNTR": "Decentrahub Coin",
- "DCOIN": "Crypto Delivery",
+ "DCOIN": "Dogcoin",
"DCR": "Decred",
"DCRE": "DeltaCredits",
"DCRN": "Decred-Next",
@@ -3759,6 +3945,7 @@
"DDIM": "DuckDaoDime",
"DDK": "DDKoin",
"DDL": "Donocle",
+ "DDM": "DDM Deutsche Mark",
"DDMT": "Dongdaemun Token",
"DDN": "Den Domains",
"DDOS": "disBalancer",
@@ -3784,15 +3971,21 @@
"DEDE": "Dede",
"DEDI": "Dedium",
"DEDPRZ": "DEDPRZ",
+ "DEE": "Deep AI",
"DEEBO": "Deebo the Bear",
"DEED": "Deed (Ordinals)",
"DEEM": "iShares MSCI Emerging Markets ETF Defichain",
"DEEP": "DeepBook Protocol",
"DEEPCLOUD": "DeepCloud AI",
"DEEPG": "Deep Gold",
+ "DEEPSEARCH": "Grok 3 DeepSearch",
+ "DEEPSEEK": "Global DePIN Chain",
+ "DEEPSEEKR1": "DeepSeek R1",
"DEER": "ToxicDeer Finance",
+ "DEERSEIZED": "Deer Seized by US Government",
"DEEX": "DEEX",
"DEEZ": "DEEZ NUTS",
+ "DEFAI": "DeFAI",
"DEFC": "Defi Coin",
"DEFEND": "Blockdefend AI",
"DEFI": "DeFi",
@@ -3838,6 +4031,7 @@
"DEMI": "DeMi",
"DEMIR": "Adana Demirspor Token",
"DEMOS": "DEMOS",
+ "DENARIUS": "Denarius",
"DENT": "Dent",
"DENTX": "DENTNet",
"DEO": "Demeter",
@@ -3869,6 +4063,7 @@
"DEUS": "DEUS Finance",
"DEUSD": "Elixir deUSD",
"DEV": "Deviant Coin",
+ "DEVAI": "DEV AI",
"DEVCOIN": "DevCoin",
"DEVE": "Develocity Finance",
"DEVI": "DEVITA",
@@ -3877,9 +4072,11 @@
"DEVVE": "Devve",
"DEVX": "Developeo",
"DEX": "DEX",
+ "DEX223": "DEX223",
"DEXA": "DEXA COIN",
"DEXC": "DexCoyote Legends",
"DEXE": "DeXe",
+ "DEXEV1": "DeXe v1",
"DEXG": "Dextoken Governance",
"DEXIO": "Dexioprotocol",
"DEXM": "Dexmex",
@@ -3980,6 +4177,7 @@
"DIGI": "Digiverse",
"DIGIC": "DigiCube",
"DIGIF": "DigiFel",
+ "DIGIMON": "Digimon",
"DIGIT": "Digital Asset Rights Token",
"DIGITAL": "Digital Reserve Currency",
"DIGITS": "Digits DAO",
@@ -4014,6 +4212,7 @@
"DISK": "Dark Lisk",
"DISPEPE": "Disabled Pepe",
"DISTR": "Distributed Autonomous Organization",
+ "DISTRIBUTE": "DISTRIBUTE",
"DIT": "Ditcoin",
"DITH": "Dither AI",
"DIVA": "DIVA Protocol",
@@ -4088,6 +4287,7 @@
"DNO": "Denaro",
"DNODE": "DecentraNode",
"DNOTES": "Dnotes",
+ "DNOW": "DuelNow",
"DNS": "BitDNS",
"DNT": "district0x",
"DNTX": "DNAtix",
@@ -4130,10 +4330,12 @@
"DOGE1SAT": "DOGE-1SATELLITE",
"DOGE2": "Dogecoin 2.0",
"DOGE20": "Doge 2.0",
+ "DOGEAI": "DOGEai",
"DOGEB": "DogeBonk",
"DOGEBNB": "DogeBNB",
"DOGEC": "DogeCash",
"DOGECAST": "Dogecast",
+ "DOGECAUCUS": "Doge Caucus",
"DOGECEO": "Doge CEO",
"DOGECO": "Dogecolony",
"DOGECOIN": "Buff Doge Coin",
@@ -4142,6 +4344,7 @@
"DOGED": "DogeCoinDark",
"DOGEDAO": "DogeDao",
"DOGEDASH": "Doge Dash",
+ "DOGEDI": "Doge Dividends",
"DOGEFA": "DOGEFATHER",
"DOGEFATHER": "Dogefather",
"DOGEFORK": "DogeFork",
@@ -4155,6 +4358,7 @@
"DOGEM": "Doge Matrix",
"DOGEMETA": "Dogemetaverse",
"DOGEMOB": "DOGEMOB",
+ "DOGEMOON": "DOGE TO MOON",
"DOGENFT": "The Doge NFT",
"DOGEP": "Doge Protocol",
"DOGEPAY": "Doge Payment",
@@ -4164,6 +4368,7 @@
"DOGERA": "Dogera",
"DOGES": "Dogeswap",
"DOGESWAP": "Dogeswap Token (HECO)",
+ "DOGETF": "DOGE ETF",
"DOGETH": "EtherDoge",
"DOGEVERSE": "DogeVerse",
"DOGEWHALE": "Dogewhale",
@@ -4186,13 +4391,16 @@
"DOGLAI": "Doglaikacoin",
"DOGMI": "DOGMI",
"DOGO": "DogemonGo",
+ "DOGONB": "Dog on Base",
"DOGPAD": "DogPad Finance",
+ "DOGPU": "DogeGPU",
"DOGRMY": "DogeArmy",
"DOGS": "Dogs",
"DOGSROCK": "Dogs Rock",
"DOGSS": "DOGS SOL",
"DOGSSO": "DOGS Solana",
"DOGSWAG": "DogSwaghat",
+ "DOGUN": "Dogun",
"DOGW": "DOGWIFHOOD",
"DOGWIFHAT": "dogwifhat",
"DOGWIFSEAL": "dogwifseal",
@@ -4216,6 +4424,7 @@
"DONA": "DONASWAP",
"DONAL": "Donald Pump",
"DONALD": "DONALD TRUMP",
+ "DONALDP": "Donald Pump",
"DONALDT": "Donald The Trump",
"DONATION": "DonationCoin",
"DONG": "DongCoin",
@@ -4267,6 +4476,7 @@
"DPEX": "DPEX",
"DPI": "DeFiPulse Index",
"DPIE": "DeFiPie",
+ "DPIN": "DPIN",
"DPLAT": "zbyte",
"DPLN": "DePlan",
"DPLTR": "Palantir Tokenized Stock Defichain",
@@ -4327,6 +4537,8 @@
"DRS": "Digital Rupees",
"DRT": "DomRaider",
"DRUGS": "Big Pharmai",
+ "DRV": "Derive",
+ "DRX": "DRX Token",
"DRXNE": "Droxne",
"DRZ": "Droidz",
"DS": "DeStorage",
@@ -4353,7 +4565,7 @@
"DSTR": "Dynamic Supply Tracker",
"DSUN": "DsunDAO",
"DSYNC": "Destra Network",
- "DT": "DarkToken",
+ "DT": "Drift Zone",
"DT1": "Dollar Token 1",
"DTA": "Data",
"DTB": "Databits",
@@ -4386,6 +4598,7 @@
"DUC": "DucatusCoin",
"DUCATO": "Ducato Protocol Token",
"DUCK": "Unit Protocol New",
+ "DUCKAI": "Duck AI",
"DUCKC": "DuckCoin",
"DUCKD": "DuckDuckCoin",
"DUCKER": "Ducker",
@@ -4401,6 +4614,7 @@
"DUET": "Duet Protocol",
"DUG": "DUG",
"DUGE": "DUGE",
+ "DUK": "DUKE COIN",
"DUK+": "Dukascoin",
"DUKE": "Duke Inu",
"DUKO": "DUKO",
@@ -4463,6 +4677,7 @@
"DYAD": "Dyad Stable",
"DYC": "Dycoin",
"DYDX": "dYdX",
+ "DYDXV1": "dYdX v1",
"DYM": "Dymension",
"DYN": "Dynamic",
"DYNA": "Dynamix",
@@ -4493,6 +4708,7 @@
"EA": "EagleCoin",
"EAC": "Education Assessment Cult",
"EADX": "EADX Token",
+ "EAFIN": "EAFIN",
"EAG": "Emerging Assets Group",
"EAGLE": "Eagle Token",
"EAGS": "EagsCoin",
@@ -4500,6 +4716,7 @@
"EARLY": "Early Risers",
"EARLYF": "EarlyFans",
"EARN": "EarnGuild",
+ "EARNB": "Earn BTC",
"EARTH": "Earth Token",
"EARTHCOIN": "EarthCoin",
"EASYF": "EasyFeedback",
@@ -4591,12 +4808,15 @@
"EDUCOIN": "EduCoin",
"EDUM": "EDUM",
"EDUX": "Edufex",
+ "EDWIN": "Edwin",
"EDX": "Equilibrium",
+ "EEFS": "Eefs",
"EER": "Ethereum eRush",
"EETH": "ether fi",
"EFBAI": "EuroFootball AI",
"EFC": "Everton Fan Token",
"EFCR": "EFLANCER",
+ "EFFECT": "Effect AI",
"EFFT": "Effort Economy ",
"EFI": "Efinity",
"EFIL": "Ethereum Wrapped Filecoin",
@@ -4622,7 +4842,8 @@
"EGI": "eGame",
"EGL": "The Eagle Of Truth",
"EGLD": "eGold",
- "EGO": "EGOcoin",
+ "EGO": "Paysenger EGO",
+ "EGOCOIN": "EGOcoin",
"EGOD": "EgodCoin",
"EGOLD": "EGOLD",
"EGON": "EgonCoin",
@@ -4695,6 +4916,7 @@
"ELON": "Dogelon Mars",
"ELON2024": "ELON 2024(BSC)",
"ELON404": "Elon404",
+ "ELON4AFD": "Elon for AfD",
"ELONCAT": "ELON CAT COIN",
"ELOND": "ELON DOGE",
"ELONDOGE": "ELON DOGE",
@@ -4722,7 +4944,7 @@
"ELUSKMON": "Elusk Mon",
"ELV": "Elvantis",
"ELVN": "11Minutes",
- "ELX": "Energy Ledger",
+ "ELX": "Elixir Network",
"ELY": "Elysian",
"ELYS": "Elys Network",
"ELYSIUM": "Elysium",
@@ -4760,6 +4982,7 @@
"EMU": "eMusic",
"EMV": "Ethereum Movie Venture",
"EMX": "EMX",
+ "EMYC": "E Money",
"ENA": "Ethena",
"ENC": "Encores Token",
"ENCD": "Encircled",
@@ -4773,6 +4996,7 @@
"ENE": "EneCoin",
"ENEAR": "Near (Energiswap)",
"ENEDEX": "Enedex",
+ "ENERGYLEDGER": "Energy Ledger",
"ENERGYX": "Safe Energy",
"ENG": "Enigma",
"ENGT": "Engagement Token",
@@ -4786,6 +5010,7 @@
"ENQ": "Enecuum",
"ENQAI": "enqAI",
"ENRG": "EnergyCoin",
+ "ENRON": "Enron",
"ENRX": "Enrex",
"ENS": "Ethereum Name Service",
"ENT": "Eternity",
@@ -4883,8 +5108,10 @@
"ESGC": "ESG Chain",
"ESH": "Switch",
"ESHIB": "Euro Shiba Inu",
+ "ESM": "EL SALVADOR MEME",
"ESN": "Ethersocial",
"ESNC": "Galaxy Arena Metaverse",
+ "ESOL": "Earn Solana",
"ESP": "Espers",
"ESPL": "ESPL ARENA",
"ESPR": "Espresso Bot",
@@ -4946,6 +5173,7 @@
"ETHPOW": "ETHPoW",
"ETHPR": "Ethereum Premium",
"ETHPY": "Etherpay",
+ "ETHR": "Ethereal",
"ETHS": "EthereumScrypt",
"ETHSHIB": "Eth Shiba",
"ETHV": "Ethverse",
@@ -4991,6 +5219,7 @@
"EUROP": "Europa Coin",
"EURQ": "Quantoz EURQ",
"EURR": "StablR Euro",
+ "EURRV1": "StablR Euro v1",
"EURS": "STASIS EURS",
"EURT": "Euro Tether",
"EURTV1": "Euro Tether v1",
@@ -5001,8 +5230,10 @@
"EV": "EVAI",
"EVA": "Evadore",
"EVAI": "EVA Intelligence",
+ "EVAL": "Chromia's EVAL by Virtuals",
"EVAN": "Evanesco Network",
"EVAULT": "EthereumVault",
+ "EVAV1": "Evadore v1",
"EVC": "Eventchain",
"EVCC": "Eco Value Coin",
"EVCOIN": "EverestCoin",
@@ -5067,6 +5298,7 @@
"EXOS": "Exobots",
"EXP": "Expanse",
"EXPAND": "Gems",
+ "EXPERT": "EXPERT_MONEY",
"EXPO": "Exponential Capital",
"EXRD": "Radix",
"EXRN": "EXRNchain",
@@ -5100,6 +5332,7 @@
"F9": "Falcon Nine",
"FAB": "FABRK Token",
"FABA": "Faba Invest",
+ "FABIENNE": "Fabienne",
"FABRIC": "MetaFabric",
"FAC": "Flying Avocado Cat",
"FACE": "FaceDAO",
@@ -5109,12 +5342,15 @@
"FACTORY": "ChainFactory",
"FACTR": "Defactor",
"FADO": "FADO Go",
+ "FAFO": "FAFO",
+ "FAFOSOL": "Fafo",
"FAG": "PoorFag",
"FAH": "Falcons",
- "FAI": "Fairum",
+ "FAI": "Freysa AI",
"FAIR": "FairCoin",
"FAIRC": "Faireum Token",
"FAIRG": "FairGame",
+ "FAIRUM": "Fairum",
"FAKE": "FAKE COIN",
"FAKEAI": "DeepFakeAI",
"FAKT": "Medifakt",
@@ -5144,7 +5380,11 @@
"FARME": "Farmers Only",
"FARMING": "Farming Bad",
"FARMS": "Farmsent",
+ "FARTAI": "Fart AI",
+ "FARTBOY": "Fartboy",
"FARTCOIN": "Fartcoin",
+ "FARTDEV": "Fart Dev",
+ "FARTING": "Farting Unicorn",
"FAS": "fast construction coin",
"FAST": "Fastswap",
"FASTAI": "Fast And Ai",
@@ -5288,6 +5528,7 @@
"FINS": "AutoShark DEX",
"FINT": "FintraDao",
"FINU": "Formula Inu",
+ "FINVESTA": "Finvesta",
"FIO": "FIO Protocol",
"FIONA": "Fiona",
"FIONABSC": "Fiona",
@@ -5308,6 +5549,7 @@
"FITFI": "Step App",
"FITT": "Fitmint",
"FIU": "beFITTER",
+ "FIUSD": "Sygnum FIUSD Liquidity Fund",
"FIWA": "Defi Warrior",
"FIX00": "FIX00",
"FJB": "Freedom. Jobs. Business.",
@@ -5361,7 +5603,9 @@
"FLO": "Flo",
"FLOAT": "Float Protocol",
"FLOCHI": "Flochi",
+ "FLOCK": "FLock.io",
"FLOCKA": "Waka Flocka",
+ "FLOCKE": "Flockerz",
"FLOKA": "FLOKA",
"FLOKEI": "FLOKEI",
"FLOKI": "Floki Inu",
@@ -5384,6 +5628,8 @@
"FLORKY": "Florky",
"FLOSHIDO": "FLOSHIDO INU",
"FLOT": "FireLotto",
+ "FLOTUS47": "Melania Trump",
+ "FLOURI": "Flourishing AI",
"FLOVI": "Flovi inu",
"FLOVM": "FLOV MARKET",
"FLOW": "Flow",
@@ -5395,12 +5641,14 @@
"FLRBRG": "Floor Cheese Burger",
"FLRS": "Flourish Coin",
"FLS": "Flits",
+ "FLSH": "FlashWash",
"FLT": "Fluence",
"FLUFFI": "Fluffington",
"FLUFFY": "FLUFFY",
"FLUFFYS": "Fluffys",
"FLUI": "Fluidity",
"FLUID": "Fluid",
+ "FLUIDTRADE": "Fluid",
"FLURRY": "Flurry Finance",
"FLUT": "Flute",
"FLUTTERCOIN": "FlutterCoin",
@@ -5489,6 +5737,7 @@
"FORT": "Forta",
"FORTH": "Ampleforth Governance Token",
"FORTHB": "ForthBox",
+ "FORTKNOX": "Fort Knox",
"FORTUNA": "Fortuna",
"FORTUNE": "Fortune",
"FORWARD": "Forward Protocol",
@@ -5516,6 +5765,7 @@
"FPEPE": "Based Father Pepe",
"FPFT": "Peruvian National Football Team Fan Token",
"FPI": "Frax Price Index",
+ "FPIBANK": "FPIBANK",
"FPIS": "Frax Price Index Share",
"FQS": "FQSwap V2",
"FR": "Freedom Reserve",
@@ -5545,13 +5795,15 @@
"FREET": "FreeTrump",
"FREL": "Freela",
"FREN": "FREN",
+ "FRENC": "Frencoin",
"FRENCH": "French On Base",
"FRENS": "Farmer Friends",
"FRESCO": "Fresco",
"FRF": "France REV Finance",
"FRGST": "Froggies Token",
"FRGX": "FRGX",
- "FRIC": "Frictionless",
+ "FRIC": "Fric",
+ "FRICTION": "Frictionless",
"FRIEND": "Friend.tech",
"FRIES": "Soltato FRIES",
"FRIN": "Fringe Finance",
@@ -5561,6 +5813,7 @@
"FRM": "Ferrum Network",
"FRN": "Francs",
"FRNT": "Final Frontier",
+ "FROC": "Based Froc",
"FROG": "FrogSwap",
"FROGB": "Frog Bsc",
"FROGCEO": "Frog Ceo",
@@ -5601,6 +5854,7 @@
"FSO": "FSociety",
"FST": "Futureswap",
"FSTC": "FastCoin",
+ "FSTR": "Fourth Star",
"FSW": "Falconswap",
"FT": "Fracton Protocol",
"FTB": "Fit&Beat",
@@ -5628,18 +5882,22 @@
"FTVT": "FashionTV Token",
"FTW": "FutureWorks",
"FTX": "FintruX",
+ "FTXAI": "FTX AI Agent",
"FTXT": "FUTURAX",
"FU": "FU Money",
"FUBAO": "FUBAO",
"FUCK": "Fuck Token",
+ "FUCKTRUMP": "FUCK TRUMP",
"FUD": "FUD.finance",
- "FUEL": "Jetfuel Finance",
+ "FUEGO": "FUEGO",
+ "FUEL": "Fuel Network",
"FUELX": "Fuel",
"FUFU": "Fufu Token",
"FUG": "FUG",
"FUJIN": "Fujinto",
"FUKU": "FUKU-KUN",
"FUL": "Fulcrom Finance",
+ "FULLSEND": "Fullsend Community Coin",
"FUMO": "Alien Milady Fumo",
"FUN": "FUN Token",
"FUNASSYI": "Funassyi",
@@ -5707,6 +5965,7 @@
"G1X": "GoldFinX",
"G3": "GAM3S.GG",
"G50": "G50",
+ "G7": "Game7",
"G8C": "ONEG8.ONE",
"G999": "G999",
"GAC": "Green Art Coin",
@@ -5754,6 +6013,7 @@
"GAMEST": "GameStop Coin",
"GAMESTARS": "Game Stars",
"GAMESTO": "GameStop",
+ "GAMESTOP": "GameStop",
"GAMESTUMP": "GAMESTUMP",
"GAMET": "GAME Token",
"GAMEX": "GameX",
@@ -5885,6 +6145,7 @@
"GENX": "Genx Token",
"GENXNET": "Genesis Network",
"GENZ": "GENZ Token",
+ "GENZAI": "GENZAI",
"GEO": "GeoCoin",
"GEOD": "GEODNET",
"GEODB": "GeoDB",
@@ -5912,6 +6173,7 @@
"GFCS": "Global Funeral Care",
"GFI": "Goldfinch",
"GFLY": "BattleFly",
+ "GFM": "GoFundMeme",
"GFN": "Graphene",
"GFOX": "Galaxy Fox",
"GFT": "Gifto",
@@ -5924,6 +6186,7 @@
"GGC": "Global Game Coin",
"GGCM": "Gold Guaranteed Coin",
"GGG": "Good Games Guild",
+ "GGGG": "Good Game Gary Gensler",
"GGH": "Green Grass Hopper",
"GGM": "Monster Galaxy",
"GGMT": "GG MetaGame",
@@ -5963,8 +6226,10 @@
"GIFT": "GiftNet",
"GIG": "GigaCoin",
"GIGA": "Gigachad",
+ "GIGABRAIN": "Gigabrain by virtuals",
"GIGACAT": "GIGACAT",
"GIGACHAD": "GigaChad",
+ "GIGAG": "GIGAGEEK",
"GIGASWAP": "GigaSwap",
"GIGGLE": "Giggle Academy",
"GIGS": "Climate101",
@@ -5978,6 +6243,7 @@
"GINGER": "GINGER",
"GINNAN": "Ginnan The Cat",
"GINOA": "Ginoa",
+ "GINU": "Green Shiba Inu",
"GINUX": "Green Shiba Inu",
"GINZA": "GINZA NETWORK",
"GIO": "Graviocoin",
@@ -6060,6 +6326,7 @@
"GMNT": "Gmining",
"GMPD": "GamesPad",
"GMR": "GAMER",
+ "GMRT": "Gamertag Token",
"GMRV1": "GAMER v1",
"GMRV2": "GAMER v2",
"GMRX": "Gaimin",
@@ -6101,6 +6368,7 @@
"GOC": "GoCrypto",
"GOCHU": "Gochujangcoin",
"GOD": "Bitcoin God",
+ "GODC": "Godcoin",
"GODCAT": "GodcatExplodingKittens",
"GODE": "Gode Chain",
"GODEX": "GUARD OF DECENT",
@@ -6115,9 +6383,11 @@
"GOGLZ": "GOGGLES",
"GOGO": "GOGO Finance",
"GOGU": "GOGU Coin",
+ "GOHOME": "GOHOME",
"GOIN": "GOinfluencer",
"GOJOCOIN": "Gojo Coin",
- "GOKU": "Goku",
+ "GOKU": "Goku Super Saiyan",
+ "GOKUINU": "Goku (gokuinu.io)",
"GOL": "GogolCoin",
"GOLC": "GOLCOIN",
"GOLD": "CyberDragon Gold",
@@ -6195,7 +6465,9 @@
"GPN": "Gamepass Network",
"GPO": "GoldPesa Option",
"GPPT": "Pluto Project Coin",
- "GPS": "Triffic",
+ "GPRO": "GoldPro",
+ "GPS": "GoPlus Security",
+ "GPSTOKEN": "GPS Token",
"GPT": "QnA3.AI",
"GPT4O": "GPT-4o",
"GPTG": "GPT Guru",
@@ -6212,6 +6484,7 @@
"GRAIL": "Camelot Token",
"GRAIN": "Granary",
"GRAM": "Gram",
+ "GRAND": "Grand Theft Ape",
"GRANDCOIN": "GrandCoin",
"GRANDMA": "Grandma",
"GRAPE": "GrapeCoin",
@@ -6248,6 +6521,7 @@
"GRIMEVO": "Grim EVO",
"GRIMEX": "SpaceGrime",
"GRIN": "Grin",
+ "GRIND": "Self Improving",
"GRL": "Greelance",
"GRLC": "Garlicoin",
"GRM": "GridMaster",
@@ -6259,6 +6533,8 @@
"GROGGO": "Groggo By Matt Furie",
"GROK": "Grok",
"GROK2": "GROK 2.0",
+ "GROK3": "Grok 3",
+ "GROKAI": "Grok AI Agent",
"GROKBANK": "Grok Bank",
"GROKBOY": "GrokBoy",
"GROKCAT": "Grok Cat",
@@ -6275,12 +6551,14 @@
"GROKOLAUS": "GROKolaus",
"GROKQUEEN": "Grok Queen",
"GROKSORAX": "GROKSORAX",
+ "GROKVANCE": "GROK VANCE",
"GROKX": "GROKX",
"GROKXAI": "Grok X Ai",
"GRON": "Gron Digital",
"GROOOOOK": "Groooook",
"GROOVE": "GROOVE",
- "GROW": "GrownCoin",
+ "GROW": "Grow Token",
+ "GROWNCOIN": "GrownCoin",
"GROWTH": "GROWTH DeFi",
"GROYPER": "Groyper",
"GRP": "Grape",
@@ -6339,6 +6617,7 @@
"GTSE": "Global Tourism Sharing Ecology",
"GTTM": "Going To The Moon",
"GTX": "GALLACTIC",
+ "GTY": "G-Agents AI",
"GUA": "GUA",
"GUAC": "Guacamole",
"GUAN": "Guanciale by Virtuals",
@@ -6348,6 +6627,7 @@
"GUARDAI": "GuardAI",
"GUC": "Green Universe Coin",
"GUCCI": "GUCCI",
+ "GUDTEK": "ai16zterminalfartARCzereLLMswarm",
"GUE": "GuerillaCoin",
"GUESS": "Peerguess",
"GUGU": "gugu",
@@ -6401,6 +6681,7 @@
"GZT": "Golden Zen Token",
"GZX": "GreenZoneX",
"Glo Dollar": "USDGLO",
+ "H1DR4": "H1DR4 by Virtuals",
"H2O": "H2O Dao",
"H2ON": "H2O Securities",
"H3O": "Hydrominer",
@@ -6457,6 +6738,7 @@
"HARR": "HARRIS DOGS",
"HARRIS": "KAMALA HARRIS",
"HARRISV": "Harris V Trump",
+ "HARRYBOLZ": "Harry Bolz",
"HARRYP": "HarryPotterObamaSonic10Inu (ERC20)",
"HARRYPO": "HarryPotterObamaPacMan8Inu",
"HART": "HARA",
@@ -6502,6 +6784,7 @@
"HBX": "Hyperbridge",
"HBZ": "HBZ Coin",
"HC": "HyperCash",
+ "HCAT": "Hover Cat",
"HCC": "HappyCreatorCoin",
"HCT": "HurricaneSwap Token",
"HCXP": "HCX PAY",
@@ -6520,6 +6803,7 @@
"HEALT": "Healthmedi",
"HEART": "Humans",
"HEARTBOUT": "HeartBout Pay",
+ "HEARTN": "Heart Number",
"HEARTR": "Heart Rate",
"HEAT": "Heat Ledger",
"HEAVEN": "Heaven Token",
@@ -6534,6 +6818,7 @@
"HEGG": "Hummingbird Egg",
"HEGIC": "Hegic",
"HEHE": "hehe",
+ "HEI": "Heima",
"HEL": "Hello Puppy",
"HELA": "Science Cult Mascot",
"HELI": "Helion",
@@ -6546,9 +6831,14 @@
"HEM": "Hemera",
"HEMAN": "HE-MAN",
"HEMULE": "Hemule",
+ "HENG": "HengCoin",
+ "HENL": "henlo",
+ "HENLO": "Henlo",
+ "HENLOV1": "Henlo v1",
"HEP": "Health Potion",
"HER": "Hero Node",
"HERA": "Hero Arena",
+ "HERAF": "Hera Finance",
"HERB": "HerbCoin",
"HERBE": "Herbee",
"HERME": "Hermes DAO",
@@ -6558,11 +6848,13 @@
"HERO": "Metahero",
"HEROC": "HEROcoin",
"HEROES": "Dehero Community Token",
+ "HEROESAI": "HEROES AI",
"HEROESC": "HeroesChained",
"HEROI": "Heroic Saga Shiba",
"HET": "HavEther",
"HETA": "HetaChain",
"HETH": "Huobi Ethereum",
+ "HEU": "Heurist AI",
"HEWE": "Health & Wealth",
"HEX": "HEX",
"HEXC": "HexCoin",
@@ -6616,7 +6908,9 @@
"HIMO": "Himo World",
"HIMOONBIRDS": "hiMOONBIRDS",
"HINA": "Hina Inu",
- "HINT": "Hintchain",
+ "HINAGI": "Hinagi",
+ "HINT": "Hive Intelligence",
+ "HINTCH": "Hintchain",
"HINU": "HajiIni",
"HIOD": "hiOD",
"HIODBS": "hiODBS",
@@ -6645,6 +6939,7 @@
"HKFLOKI": "hong kong floki",
"HKG": "Hacker Gold",
"HKN": "Hacken",
+ "HKU5": "New Coronavirus",
"HLC": "HalalChain",
"HLD": "HyperLending",
"HLDY": "HOLIDAY",
@@ -6652,6 +6947,8 @@
"HLINK": "Chainlink (Harmony One Bridge)",
"HLM": "Helium",
"HLN": "Holonus",
+ "HLO": "Halo",
+ "HLOV1": "Halo v1",
"HLP": "Purpose Coin",
"HLPR": "HELPER COIN",
"HLPT": "HLP Token",
@@ -6697,6 +6994,7 @@
"HODL": "HOdlcoin",
"HOG": "Hog",
"HOGE": "Hoge Finance",
+ "HOGONSOLANA": "HOG",
"HOHOHO": "Santa Floki v2.0",
"HOICHI": "Hoichi",
"HOKA": "Hokkaido Inu",
@@ -6705,9 +7003,11 @@
"HOLA": "Hola Token",
"HOLD": "HOLD",
"HOLDEX": "Holdex Finance",
+ "HOLDON4": "HoldOn4DearLife",
"HOLY": "Holy Trinity",
"HOM": "Homeety",
"HOME": "OtterHome",
+ "HOMEBREW": "Homebrew Robotics Club",
"HOMER": "Homer Simpson",
"HOMERB": "Homer BSC",
"HOMERO": "Homer Of Meme",
@@ -6722,6 +7022,7 @@
"HONK": "Honk",
"HONKLER": "Honkler",
"HONOR": "HonorLand",
+ "HOODRAT": "Hoodrat Coin",
"HOOF": "Metaderby Hoof",
"HOOK": "Hooked Protocol",
"HOOP": "Chibi Dinos",
@@ -6745,6 +7046,7 @@
"HOTMOON": "HotMoon Token",
"HOTN": "HotNow",
"HOTT": "HOT Token",
+ "HOUND": "BaseHoundBot by Virtuals",
"HOUSE": "Klaymore Stakehouse",
"HOW": "HowInu",
"HOWL": "Coyote",
@@ -6777,6 +7079,7 @@
"HSAI": "HealthSci.AI",
"HSC": "HashCoin",
"HSF": "Hillstone Finance",
+ "HSK": "HashKey Platform Token",
"HSN": "Hyper Speed Network",
"HSP": "Horse Power",
"HSS": "Hashshare",
@@ -6792,11 +7095,12 @@
"HTDF": "Orient Walt",
"HTE": "Hepton",
"HTER": "Biogen",
+ "HTERM": "Hiero Terminal",
"HTK": "Hard To Kill",
"HTM": "Hatom",
"HTML": "HTML Coin",
"HTMOON": "HTMOON",
- "HTN": "Heart Number",
+ "HTN": "Hoosat Network",
"HTO": "Heavenland HTO",
"HTR": "Hathor",
"HTT": "Hello Art",
@@ -6813,9 +7117,11 @@
"HUGO": "Hugo Inu",
"HUH": "HUH Token",
"HUHCAT": "huhcat",
+ "HULEZHI": "HU LE ZHI",
"HUM": "Humanscape",
"HUMAI": "Humanoid AI",
"HUMP": "Hump",
+ "HUMV1": "Humanscape v1",
"HUND": "HUND MEME COIN",
"HUNDRED": "HUNDRED",
"HUNNY": "Pancake Hunny",
@@ -6834,6 +7140,7 @@
"HVE2": "Uhive",
"HVH": "HAVAH",
"HVI": "Hungarian Vizsla Inu",
+ "HVLO": "Hivello",
"HVN": "Hiveterminal Token",
"HVNT": "HiveNet Token",
"HVT": "HyperVerse",
@@ -6909,6 +7216,7 @@
"ICE": "Ice Open Network",
"ICEC": "IceCream",
"ICECR": "Ice Cream Sandwich",
+ "ICECREAM": "IceCream AI",
"ICELAND": "ICE LAND",
"ICETH": "Interest Compounding ETH Index",
"ICG": "Invest Club Global",
@@ -6952,6 +7260,7 @@
"IDO": "Idexo",
"IDOL": "IDOLINU",
"IDORU": "Vip2Fan",
+ "IDRISS": "IDRISS",
"IDRT": "Rupiah Token",
"IDRX": "IDRX",
"IDT": "InvestDigital",
@@ -7008,6 +7317,7 @@
"IMGZ": "Imigize",
"IMI": "Influencer",
"IML": "IMMLA",
+ "IMMIGRATION": "Immigration Customs Enforcement",
"IMMO": "ImmortalDAO Finance",
"IMMORTAL": "IMMORTAL.COM",
"IMO": "IMO",
@@ -7047,6 +7357,8 @@
"INDIAN": "Indian Call Center",
"INDICOIN": "IndiCoin",
"INDU": "INDU4.0",
+ "INDUSTRIAL": "Industrial",
+ "INDX": "CryptoIndex",
"INDY": "Indigo Protocol",
"INE": "IntelliShare",
"INEDIBLE": "INEDIBLE",
@@ -7061,6 +7373,7 @@
"INFINI": "Infinity Economics",
"INFLR": "Inflr",
"INFO": "Infomatix",
+ "INFR": "infraX",
"INFRA": "Bware",
"INFT": "Infinito",
"INFTT": "iNFT Token",
@@ -7086,7 +7399,6 @@
"INSP": "Inspect",
"INSPI": "InspireAI",
"INSR": "Insurabler",
- "INST": "Instadapp",
"INSTAMINE": "Instamine Nuggets",
"INSTAR": "Insights Network",
"INSUR": "InsurAce",
@@ -7143,6 +7455,7 @@
"IOV": "Starname",
"IOVT": "IOV",
"IOWN": "iOWN Token",
+ "IP": "Story",
"IP3": "Cripco",
"IPAD": "Infinity Pad",
"IPC": "IPChain",
@@ -7187,11 +7500,13 @@
"ISKY": "Infinity Skies",
"ISL": "IslaCoin",
"ISLAMI": "ISLAMICOIN",
+ "ISLAND": "ISLAND Token",
"ISLM": "Islamic Coin",
"ISME": "Root Protocol",
"ISP": "Ispolink",
"ISR": "Insureum",
"ISRG.CUR": "Intuitive Surgical, Inc.",
+ "ISSOU": "Risitas",
"ISSP": "ISSP",
"IST": "Inter Stable Token",
"ISTEP": "iSTEP",
@@ -7248,7 +7563,7 @@
"IZX": "IZX",
"IZZY": "Izzy",
"InBit": "PrepayWay",
- "J": "JoinCoin",
+ "J": "Jambo",
"J8T": "JET8",
"J9BC": "J9CASINO",
"JACK": "Jack Token",
@@ -7256,8 +7571,12 @@
"JACY": "JACY",
"JADE": "Jade Protocol",
"JADEC": "Jade Currency",
+ "JAE": "JaeCoin",
"JAGO": "Jagotrack",
+ "JAI": "Japanese Akita Inu",
"JAIHO": "Jaiho Crypto",
+ "JAIHOZ": "Jaihoz by Virtuals",
+ "JAILSTOOL": "Stool Prisondente",
"JAKE": "Jake The Dog",
"JAM": "Tune.Fm",
"JAN": "Storm Warfare",
@@ -7265,6 +7584,7 @@
"JANET": "Janet",
"JANI": "JANI",
"JANRO": "Janro The Rat",
+ "JAPAN": "Japan Open Chain",
"JAR": "Jarvis+",
"JARED": "Jared From Subway",
"JARY": "JeromeAndGary",
@@ -7287,6 +7607,7 @@
"JDAI": "Dai (TON Bridge)",
"JDC": "JustDatingSite",
"JDO": "JINDO",
+ "JDV": "JD Vance",
"JED": "JEDSTAR",
"JEDALS": "Yoda Coin Swap",
"JEET": "Jeet",
@@ -7297,6 +7618,8 @@
"JEJUDOGE": "Jejudoge",
"JELLI": "JELLI",
"JELLY": "Jelly eSports",
+ "JELLYAI": "jelly ai agent",
+ "JELLYJELLY": "Jelly-My-Jelly",
"JEM": "Jem",
"JEN": "JEN COIN",
"JENNER": "Caitlyn Jenner",
@@ -7309,15 +7632,18 @@
"JET": "Jet Protocol",
"JETCAT": "Jetcat",
"JETCOIN": "Jetcoin",
+ "JETFUEL": "Jetfuel Finance",
"JETTON": "JetTon Game",
"JEUR": "Jarvis Synthetic Euro",
"JEW": "Shekel",
"JEWEL": "DeFi Kingdoms",
+ "JEWELRY": "Jewelry Token",
"JEX": "JEX Token",
"JF": "Jswap.Finance",
"JFI": "JackPool.finance",
"JFIN": "JFIN Coin",
"JFIVE": "Jonny Five",
+ "JFOX": "JuniperFox AI",
"JFP": "JUSTICE FOR PEANUT",
"JGLP": "Jones GLP",
"JGN": "Juggernaut",
@@ -7347,6 +7673,7 @@
"JMT": "JMTIME",
"JMZ": "Jimizz",
"JNB": "Jinbi Token",
+ "JNFTC": "Jumbo Blockchain",
"JNGL": "Jungle Labz",
"JNS": "Janus",
"JNT": "Jibrel Network Token",
@@ -7354,14 +7681,17 @@
"JNY": "JNY",
"JOB": "Jobchain",
"JOBS": "JobsCoin",
+ "JOBSEEK": "JobSeek AI",
"JOC": "Speed Star JOC",
"JOE": "JOE",
"JOEB": "Joe Biden",
"JOEBIDEN2024 ": "JOEBIDEN2024",
+ "JOECOIN": "Joe Coin",
"JOEY": "Joey Inu",
"JOGECO": "Jogecodog",
"JOHM": "Johm lemmon",
"JOHNNY": "Johnny The Bull",
+ "JOINCOIN": "JoinCoin",
"JOINT": "Joint Ventures",
"JOJO": "JOJO",
"JOK": "JokInTheBox",
@@ -7441,6 +7771,7 @@
"JWBTC": "Wrapped Bitcoin (TON Bridge)",
"JWIF": "Jerrywifhat",
"JWL": "Jewels",
+ "JYAI": "Jerry The Turtle By Matt Furie",
"JYC": "Joe-Yo Coin",
"K21": "K21",
"K2G": "Kasko2go",
@@ -7462,7 +7793,9 @@
"KAIK": "KAI KEN",
"KAIKEN": "Kaiken Shiba",
"KAILY": "Kailith",
+ "KAIM": "Kai Meme",
"KAINET": "KAINET",
+ "KAITO": "KAITO",
"KAKA": "KAKA NFT World",
"KAKAXA": "KAKAXA",
"KAKI": "Doge KaKi",
@@ -7488,6 +7821,7 @@
"KANGAL": "Kangal",
"KANGO": "KANGO",
"KAP": "KAP Games",
+ "KAPPA": "Kappa",
"KAPPY": "Kappy",
"KAPU": "Kapu",
"KAR": "Karura",
@@ -7499,6 +7833,7 @@
"KARMAD": "Karma DAO",
"KARRAT": "KARRAT",
"KART": "Dragon Kart",
+ "KARUM": "Karum Coin",
"KAS": "Kaspa",
"KASBOT": "KASBOT THE GUARDIAN OF 𐤊ASPA",
"KASHIN": "KASHIN",
@@ -7517,7 +7852,6 @@
"KAVA": "Kava",
"KAWA": "Kawakami Inu",
"KAYI": "Kayı",
- "KB3": "B3Coin",
"KBC": "Karatgold coin",
"KBD": "Kyberdyne",
"KBOND": "Klondike Bond",
@@ -7553,9 +7887,12 @@
"KEEP": "Keep Network",
"KEES": "Korea Entertainment Education & Shopping",
"KEI": "Keisuke Inu",
+ "KEIRA": "Keira",
"KEK": "KekCoin",
+ "KEKARMY": "Kek",
"KEKE": "KEK",
"KEKEC": "THE BALKAN DWARF",
+ "KEKIUS": "Kekius Maximus",
"KEL": "KelVPN",
"KELP": "KELP",
"KELPE": "Kelp Earned Points",
@@ -7576,6 +7913,9 @@
"KET": "KET",
"KETAMINE": "Ketamine",
"KETAN": "Ketan",
+ "KEVIN": "Kevin (kevinonbase.xyz)",
+ "KEVINTOKENME": "KEVIN (kevintoken.me)",
+ "KEVINTOKENNET": "Kevin",
"KEX": "Kira Network",
"KEXCOIN": "KexCoin",
"KEY": "SelfKey",
@@ -7605,6 +7945,8 @@
"KICKS": "GetKicks",
"KIDEN": "RoboKiden",
"KIF": "KittenFinance",
+ "KIKI": "KIKICat",
+ "KIKIF": "Kiki Flaminki",
"KIKO": "KIKO",
"KILLA": "The Bitcoin Killa",
"KILLER": "Fat Cat Killer",
@@ -7613,6 +7955,7 @@
"KIM": "King Money",
"KIMBO": "Kimbo",
"KIMCHI": "KIMCHI.finance",
+ "KIMIAI": "Kimi AI Agent",
"KIN": "Kin",
"KIND": "Kind Ads",
"KINE": "Kine Protocol",
@@ -7643,6 +7986,7 @@
"KINK": "Kinka",
"KINT": "Kintsugi",
"KINU": "Kragger Inu",
+ "KIP": "KIP",
"KIRA": "Kira the Injective Cat",
"KIRBY": "Kirby Inu",
"KIRBYCEO": "Kirby CEO",
@@ -7723,7 +8067,9 @@
"KODACHI": "Kodachi Token",
"KOGE": "BNB48 Club Token",
"KOGECOIN": "KogeCoin.io",
+ "KOGIN": "Kogin by Virtuals",
"KOI": "Koi",
+ "KOII": "Koii",
"KOIN": "Koinos",
"KOINB": "KoinBülteni Token",
"KOINETWORK": "Koi Network",
@@ -7823,6 +8169,7 @@
"KUBE": "KubeCoin",
"KUBO": "KUBO",
"KUBOS": "KubosCoin",
+ "KUDAI": "Kudai",
"KUE": "Kuende",
"KUJI": "Kujira",
"KUKU": "KuKu",
@@ -7867,6 +8214,8 @@
"KZC": "KZCash",
"KZEN": "Kaizen",
"L": "L inu",
+ "L1": "Lamina1",
+ "L1X": "Layer One X",
"L2": "Leverj Gluon",
"L2DAO": "Layer2DAO",
"L3": "Layer3",
@@ -7907,6 +8256,7 @@
"LANDV1": "Landshare v1",
"LANDW": "LandWolf",
"LANDWOLF": "LANDWOLF",
+ "LANDWOLFAVAX": "LANDWOLF (AVAX)",
"LANDWOLFETH": "Landwolf",
"LANDWU": "LandWu",
"LANE": "LaneAxis",
@@ -7928,7 +8278,8 @@
"LATX": "Latium",
"LAUGHCOIN": "Laughcoin",
"LAUNCH": "Launchblock.com",
- "LAVA": "Lavaswap",
+ "LAVA": "Lava Network",
+ "LAVASWAP": "Lavaswap",
"LAVAX": "LavaX Labs",
"LAVE": "Lavandos",
"LAVITA": "Lavita AI",
@@ -7936,7 +8287,7 @@
"LAWO": "Law Of Attraction",
"LAX": "LAPO",
"LAY3R": "AutoLayer",
- "LAYER": "UniLayer",
+ "LAYER": "Solayer",
"LAZ": "Lazarus",
"LAZIO": "Lazio Fan Token",
"LAZYCAT": "LAZYCAT",
@@ -7967,6 +8318,7 @@
"LCR": "Lucro",
"LCRO": "Liquid CRO",
"LCS": "LocalCoinSwap",
+ "LCSH": "LC SHIB",
"LCSN": "Lacostoken",
"LCT": "LendConnect",
"LCWP": "LiteCoinW Plus",
@@ -7992,8 +8344,11 @@
"LEE": "Love Earn Enjoy",
"LEET": "LeetSwap",
"LEG": "Legia Warsaw Fan Token",
+ "LEGEND": "Legend",
"LEGION": "LEGION",
+ "LEGIT": "LEGIT",
"LEGO": "Lego Coin",
+ "LEI": "Leia Games",
"LEIA": "Leia",
"LELE": "Lelecoin",
"LEMC": "LemonChain",
@@ -8022,6 +8377,7 @@
"LESLIE": "Leslie",
"LESS": "Less Network",
"LESSF": "LessFnGas",
+ "LESTE": "LESTER by Virtuals",
"LESTER": "Litecoin Mascot",
"LET": "LinkEye",
"LETIT": "Letit",
@@ -8039,7 +8395,7 @@
"LEXI": "LEXIT",
"LEZ": "Peoplez",
"LEZGI": "LEZGI Token",
- "LF": "Linkflow",
+ "LF": "LF",
"LFC": "BigLifeCoin",
"LFDOG": "lifedog",
"LFG": "Gamerse",
@@ -8054,7 +8410,9 @@
"LGC": "LiveGreen Coin",
"LGCY": "LGCY Network",
"LGD": "Legends Cryptocurrency",
+ "LGG": "Let's Go Gambling",
"LGNDX": "LegendX",
+ "LGNS": "Longinus",
"LGO": "Legolas Exchange",
"LGOLD": "LYFE GOLD",
"LGOT": "LGO Token",
@@ -8070,7 +8428,7 @@
"LIBERO": "Libero Financial",
"LIBERTA": "The Libertarian Dog",
"LIBFX": "Libfx",
- "LIBRA": "0L Network",
+ "LIBRA": "Libra",
"LIBRAP": "Libra Protocol",
"LIBRE": "Libre",
"LIC": "Ligercoin",
@@ -8093,13 +8451,16 @@
"LIGHTSPEED": "LightSpeedCoin",
"LIGMA": "Ligma Node",
"LIGO": "Ligo",
+ "LIHUA": "LIHUA",
"LIKE": "Only1",
"LIKEC": "LikeCoin",
"LILA": "LiquidLayer",
"LILB": "Lil Brett",
"LILFLOKI": "Lil Floki",
+ "LILO": "Lilo",
"LILPUMP": "lilpump",
"LILY": "LILY-The Gold Digger",
+ "LIMBO": "Limbo",
"LIME": "iMe Lab",
"LIMEX": "Limestone Network",
"LIMITEDCOIN": "Limited Coin",
@@ -8165,8 +8526,10 @@
"LKY": "LuckyCoin",
"LL": "LightLink",
"LLAND": "Lyfe Land",
+ "LLD": "Liberland dollar",
"LLG": "Loligo",
"LLION": "Lydian Lion",
+ "LLM": "Large Language Model Based",
"LLT": "LILLIUS",
"LM": "LeisureMeta",
"LMAO": "LMAO Finance",
@@ -8215,6 +8578,7 @@
"LOE": "Legends of Elysium",
"LOF": "Land of Fantasy",
"LOFI": "LOFI",
+ "LOFIBUZZ": "LOFI",
"LOG": "Wood Coin",
"LOGO": "LOGOS",
"LOGX": "LogX Network",
@@ -8338,6 +8702,7 @@
"LTO": "LTO Network",
"LTOV1": "LTO Network v1",
"LTOV2": "LTO Network v2",
+ "LTP": "Listapie",
"LTPC": "Lightpaycoin",
"LTR": "LogiTron",
"LTRBT": "Little Rabbit",
@@ -8366,6 +8731,7 @@
"LUFFY": "Luffy",
"LUFFYG": "Luffy G5",
"LUFFYOLD": "Luffy",
+ "LUFFYV1": "Luffy v1",
"LUIGI": "Luigi Inu",
"LUIS": "Tongue Cat",
"LULU": "LULU",
@@ -8394,7 +8760,8 @@
"LUSH": "Lush AI",
"LUT": "Cinemadrom",
"LUTETIUM": "Lutetium Coin",
- "LUX": "LUXCoin",
+ "LUX": "Lux Token",
+ "LUXCOIN": "LUXCoin",
"LUXO": "Luxo",
"LUXU": "Luxury Travel Token",
"LUXY": "Luxy",
@@ -8449,6 +8816,7 @@
"MADAGASCARTOKEN": "Madagascar Token",
"MADANA": "MADANA",
"MADC": "MadCoin",
+ "MADCOIN": "MAD",
"MADH": "Madhouse",
"MADOG": "MarvelDoge",
"MADP": "Mad Penguin",
@@ -8456,13 +8824,17 @@
"MAEP": "Maester Protocol",
"MAF": "MetaMAFIA",
"MAG": "Magnify Cash",
- "MAGA": "MAGA Hat",
+ "MAGA": "MAGA",
"MAGA2024": "MAGA2024",
+ "MAGA47": "MAGA 47",
"MAGAA": "MAGA AGAIN",
+ "MAGABRO": "M.A.G.A. Bro",
"MAGAC": "MAGA CAT",
"MAGACA": "MAGA CAT",
"MAGACAT": "MAGACAT",
"MAGADOGE": "MAGA DOGE",
+ "MAGAF": "MAGA FRENS",
+ "MAGAHAT": "MAGA Hat",
"MAGAIBA": "Magaiba",
"MAGAN": "Maganomics On Solana",
"MAGANOMICS": "Maganomics",
@@ -8478,6 +8850,7 @@
"MAGICV": "Magicverse",
"MAGIK": "Magik Finance",
"MAGN": "Magnate Finance",
+ "MAGNE": "Magnetix",
"MAGNET": "Yield Magnet",
"MAGNET6900": "MAGNET6900",
"MAGNETWORK": "Magnet",
@@ -8508,6 +8881,7 @@
"MAND": "Mandala Exchange Token",
"MANDALA": "Mandala Exchange Token",
"MANDOX": "MandoX",
+ "MANDY": "MANDY COIN",
"MANE": "MANE",
"MANEKI": "MANEKI",
"MANGA": "Manga Token",
@@ -8518,7 +8892,9 @@
"MANT": "Mantle USD",
"MANTA": "Manta Network",
"MANTLE": "Mantle",
+ "MANUSAI": "Manus AI Agent",
"MANYU": "Little Manyu",
+ "MANYUDOG": "MANYU",
"MAO": "Mao",
"MAOW": "MAOW",
"MAP": "MAP Protocol",
@@ -8577,6 +8953,7 @@
"MATAR": "MATAR AI",
"MATCH": "Matching Game",
"MATE": "Mate",
+ "MATES": "MATES",
"MATH": "MATH",
"MATIC": "Polygon",
"MATICX": "Stader MaticX",
@@ -8593,6 +8970,7 @@
"MAWA": "Kumala Herris",
"MAWC": "Magawincat",
"MAX": "Matr1x",
+ "MAXAIAGENT": "MAX",
"MAXCOIN": "MaxCoin",
"MAXETH": "Max on ETH",
"MAXI": "Maximus",
@@ -8601,12 +8979,14 @@
"MAXX": "MAXX Finance",
"MAY": "Theresa May Coin",
"MAYACOIN": "MayaCoin",
+ "MAYILONG": "Yi long ma",
"MAYO": "Mr Mayonnaise the Cat",
"MAYP": "Maya Preferred",
"MAZC": "MyMazzu",
"MAZI": "MaziMatic",
"MAZZE": "Mazze",
"MB": "MineBee",
+ "MB28": "MBridge28",
"MB4": "Matthew Box 404",
"MB8": "MB8 Coin",
"MBAG": "MoonBag",
@@ -8699,6 +9079,7 @@
"MDM": "Medium",
"MDN": "Modicoin",
"MDOGE": "First Dog In Mars",
+ "MDOGS": "Money Dogs",
"MDR": "Mudra MDR",
"MDS": "MediShares",
"MDT": "Measurable Data Token",
@@ -8741,7 +9122,8 @@
"MEI": "Mei Solutions",
"MEIZHU": "GUANGZHOU ZOO NEW BABY PANDA",
"MEL": "MELX",
- "MELANIA": "Melania Trump",
+ "MELANIA": "Melania Meme",
+ "MELANIATRUMP": "Melania Trump",
"MELB": "Minelab",
"MELD": "MELD",
"MELI": "Meli Games",
@@ -8756,6 +9138,7 @@
"MEM": "Memecoin",
"MEMAGX": "Meta Masters Guild Games",
"MEMD": "MemeDAO",
+ "MEMDEX": "Memdex100",
"MEME": "Memecoin",
"MEMEAI": "Meme Ai",
"MEMECUP": "Meme Cup",
@@ -8772,6 +9155,7 @@
"MEMESQUAD": "Meme Squad",
"MEMET": "MEMETOON",
"MEMETIC": "Memetic",
+ "MEMHASH": "Memhash",
"MEMORYCOIN": "MemoryCoin",
"MEN": "METAHUB FINANCE",
"MENDI": "Mendi Finance",
@@ -8790,6 +9174,7 @@
"MERCU": "Merculet",
"MERCURY": "Mercury",
"MEREDITH": "Taylor Swift's Cat MEREDITH",
+ "MERG": "Merge Token",
"MERGE": "Merge",
"MERI": "Merebel",
"MERIDIAN": "Meridian Network LOCK",
@@ -8806,6 +9191,7 @@
"META": "MetaDAO",
"METAA": "META ARENA",
"METABOT": "Robot Warriors",
+ "METABRAW": "Metabrawl",
"METAC": "Metacoin",
"METACA": "MetaCash",
"METACAT": "MetaCat",
@@ -8819,6 +9205,7 @@
"METAF": "MetaFastest",
"METAG": "MetagamZ",
"METAGEAR": "MetaGear",
+ "METAIVERSE": "MetAIverse",
"METAL": "Metal Blockchain",
"METALCOIN": "MetalCoin",
"METAMEME": "met a meta metameme",
@@ -8903,6 +9290,7 @@
"MHUNT": "MetaShooter",
"MI": "XiaoMiCoin",
"MIA": "MiamiCoin",
+ "MIAO": "MIAOCoin",
"MIB": "Mobile Integrated Blockchain",
"MIBO": "miBoodle",
"MIBR": "MIBR Fan Token",
@@ -8916,6 +9304,7 @@
"MIDAI": "Midway AI",
"MIDAS": "Midas",
"MIDASDOLLAR": "Midas Dollar Share",
+ "MIDLE": "Midle",
"MIDN": "Midnight",
"MIDNIGHT": "Midnight",
"MIE": "MIE Network",
@@ -8969,6 +9358,7 @@
"MINI": "mini",
"MINIBNBTIGER": "MiniBNBTiger",
"MINID": "Mini Donald",
+ "MINIDO": "MiniDoge",
"MINIDOGE": "MiniDOGE",
"MINIFOOTBALL": "Minifootball",
"MINIMYRO": "Mini Myro",
@@ -8986,6 +9376,7 @@
"MINTCOIN": "MintCoin",
"MINTE": "Minter HUB",
"MINTME": "MintMe.com Coin",
+ "MINTO": "The AI Mascot",
"MINTYS": "MintySwap",
"MINU": "Minu",
"MINUTE": "MINUTE Vault (NFTX)",
@@ -9005,7 +9396,9 @@
"MISHA": "Vitalik's Dog",
"MISHKA": "Mishka Token",
"MISS": "MISS",
+ "MISSK": "Miss Kaka",
"MIST": "Mist",
+ "MISTCOIN": "MistCoin",
"MISTE": "Mister Miggles",
"MISTRAL": "Mistral AI",
"MIT": "Galaxy Blitz",
@@ -9082,7 +9475,7 @@
"MNB": "MoneyBag",
"MNBR": "MN Bridge",
"MNC": "MainCoin",
- "MND": "Mound Token",
+ "MND": "Mind",
"MNDCC": "Mondo Community Coin",
"MNDE": "Marinade",
"MNE": "Minereum",
@@ -9159,6 +9552,7 @@
"MOGT": "MOG TRUMP",
"MOGU": "Mogu",
"MOGUL": "Mogul Productions",
+ "MOGULV1": "Mogul Productions v1",
"MOGUT": "Mogutou",
"MOGX": "Mogu",
"MOH": "Medal of Honour",
@@ -9166,6 +9560,7 @@
"MOIN": "MoinCoin",
"MOJI": "Moji",
"MOJO": "Mojocoin",
+ "MOJOB": "Mojo on Base",
"MOK": "MocktailSwap",
"MOL": "Molecule",
"MOLA": "MoonLana",
@@ -9177,6 +9572,7 @@
"MOMA": "Mochi Market",
"MOMIJI": "MAGA Momiji",
"MOMO": "MOMO 2.0",
+ "MOMO2025": "momo",
"MON": "MON Protocol",
"MONA": "MonaCoin",
"MONAIZE": "Monaize",
@@ -9272,6 +9668,7 @@
"MOTHER": "Mother Iggy",
"MOTI": "Motion",
"MOTO": "Motocoin",
+ "MOUND": "Mound Token",
"MOUTAI": "Moutai",
"MOV": "MovieCoin",
"MOVD": "MOVE Network",
@@ -9287,6 +9684,7 @@
"MOXIE": "Moxie",
"MOYA": "MOYA",
"MOZ": "Mozik",
+ "MOZA": "Mozaic",
"MP": "Membership Placeholders",
"MP3": "MP3",
"MPAA": "MPAA",
@@ -9311,6 +9709,7 @@
"MQL": "MiraQle",
"MQST": "MonsterQuest",
"MR": "Meta Ruffy",
+ "MRB": "MoonRabbits",
"MRBASED": "MrBased",
"MRBOB": "MR BOB COIN",
"MRCH": "MerchDAO",
@@ -9328,6 +9727,7 @@
"MRS": "Metars Genesis",
"MRSA": "MrsaCoin",
"MRSMIGGLES": "Mrs Miggles",
+ "MRST": "Mars Token",
"MRT": "MinersReward",
"MRUN": "Metarun",
"MRV": "Macroverse",
@@ -9375,6 +9775,7 @@
"MTGT": "MTG Token",
"MTGX": "Montage Token",
"MTH": "Monetha",
+ "MTHB": "MTHAIBAHT",
"MTHD": "Method Finance",
"MTHN": "MTH Network",
"MTIK": "MatikaToken",
@@ -9440,6 +9841,7 @@
"MUSICAI": "MusicAI",
"MUSICOIN": "Musicoin",
"MUSK": "Musk",
+ "MUSKAI": "Musk AI Agent",
"MUSKMEME": "MUSK MEME",
"MUSKVSZUCK": "Cage Match",
"MUST": "MUST Protocol",
@@ -9471,12 +9873,16 @@
"MWC": "MimbleWimbleCoin",
"MWCC": "Metaworld",
"MWD": "MEW WOOF DAO",
+ "MWETH": "Moonwell Flagship ETH (Morpho Vault)",
+ "MWH": "Melania Wif Hat",
"MX": "MX Token",
"MXC": "Machine Xchange Coin",
"MXD": "Denarius",
"MXGP": "MXGP Fan Token",
"MXM": "Maximine",
+ "MXNA": "Machina",
"MXNB": "MXNB",
+ "MXNBC": "Rekt Burgundy by Virtuals",
"MXNT": "Tether MXNt",
"MXRP": "Monsta XRP",
"MXT": "MixTrust",
@@ -9517,6 +9923,7 @@
"Medu": "Medusa",
"N0031": "nYFI",
"N1": "NFTify",
+ "N3": "Network3",
"N3DR": "NeorderDAO ",
"N64": "N64",
"N7": "Number7",
@@ -9574,9 +9981,11 @@
"NAWS": "NAWS.AI",
"NAX": "NextDAO",
"NAYM": "NAYM",
+ "NAYUTA": "Nayuta Coin",
"NAZ": "NAZDAQ",
"NAZA": "NAZA",
"NAZAR": "NAZAR PROTOCOL",
+ "NAZIELON": "NAZI ELON",
"NBABSC": "NBA BSC",
"NBAI": "Nebula AI",
"NBAR": "NOBAR",
@@ -9593,7 +10002,7 @@
"NBS": "New BitShares",
"NBT": "NanoByte",
"NBXC": "Nibble",
- "NC": "Nayuta Coin",
+ "NC": "Nodecoin",
"NCA": "NeuroCrypto Ads",
"NCASH": "Nucleus Vision",
"NCAT": "Neuracat",
@@ -9670,6 +10079,7 @@
"NERF": "Neural Radiance Field",
"NERO": "Nero Token",
"NERVE": "NERVE",
+ "NES": "Nest AI",
"NESS": "Ness LAB",
"NEST": "Nest Protocol",
"NESTREE": "Nestree",
@@ -9687,6 +10097,7 @@
"NETZ": "MainnetZ",
"NETZ1": "NETZERO",
"NEU": "Neumark",
+ "NEUR": "neur.sh",
"NEURA": "Neurahub",
"NEURAL": "NeuralAI",
"NEURALINK": "Neuralink",
@@ -9705,6 +10116,7 @@
"NEWB": "Newbium",
"NEWBV1": "Newbium v1",
"NEWC": "New Cat",
+ "NEWERASOL": "New Era AI",
"NEWG": "NewGold",
"NEWM": "NEWM",
"NEWO": "New Order",
@@ -9719,6 +10131,7 @@
"NEXAI": "NexAI",
"NEXBOX": "NexBox",
"NEXBT": "Native XBTPro Exchange Token",
+ "NEXEA": "NEXEA",
"NEXG": "NexGami",
"NEXM": "Nexum",
"NEXMI": "NexMillionaires",
@@ -9730,8 +10143,10 @@
"NEXTV1": "Connext Network",
"NEXUSAI": "NexusAI",
"NEXXO": "Nexxo",
- "NEZHA": "NezhaToken",
+ "NEZHA": "NEZHA",
+ "NEZHATOKEN": "NezhaToken",
"NFAI": "Not Financial Advice",
+ "NFAIV1": "Not Financial Advice v1",
"NFCR": "NFCore",
"NFD": "Feisty Doge NFT",
"NFE": "Edu3Labs",
@@ -9774,6 +10189,7 @@
"NHI": "Non Human Intelligence",
"NHT": "Neighbourhoods",
"NIAO": "NIAO",
+ "NIBBLES": "Nibbles",
"NIBI": "Nibiru Chain",
"NIC": "NewInvestCoin",
"NICE": "Nice",
@@ -9794,8 +10210,10 @@
"NIKO": "NikolAI",
"NILE": "Nile",
"NIM": "Nimiq",
+ "NIMBUS": "Nimbus AI",
"NIMFA": "Nimfamoney",
"NIN": "Next Innovation",
+ "NINA": "NINA",
"NINJ": "Ninja Protocol",
"NINJA": "Dog Wif Nunchucks",
"NINJACAT": "NinjaCat",
@@ -9817,6 +10235,7 @@
"NITO": "Nitroken",
"NITRO": "Nitro League",
"NITROE": "NitroEX",
+ "NITROFROG": "Nitro",
"NITROG": "Nitro",
"NIX": "NIX",
"NIZA": "Niza Global",
@@ -9856,7 +10275,8 @@
"NOBL": "NobleCoin",
"NOBS": "No BS Crypto",
"NOCHILL": "AVAX HAS NO CHILL",
- "NODE": "Whole Network",
+ "NODE": "NodelyAI",
+ "NODESYNAPSE": "NodeSynapse",
"NODIDDY": "NODIDDY",
"NODIS": "Nodis",
"NODL": "Nodle Network",
@@ -9878,9 +10298,11 @@
"NOODS": "Noods",
"NOOOO": "NOOOO",
"NOOT": "NOOT (Ordinals)",
+ "NOPAIN": "No Pain No Gain",
"NOR": "Noir",
"NORA": "SnowCrash Token",
"NORD": "Nord Finance",
+ "NORDO": "Greenland Rare Bear",
"NORMIE": "Normie",
"NORMUS": "NORMUS",
"NOS": "Nosana",
@@ -9928,7 +10350,7 @@
"NRV": "Nerve Finance",
"NRVE": "Narrative",
"NRX": "Neironix",
- "NS": "NodeSynapse",
+ "NS": "SuiNS Token",
"NS2DRP": "New Silver Series 2 DROP",
"NSBT": "Neutrino Token",
"NSD": "Nasdacoin",
@@ -9993,6 +10415,7 @@
"NVC": "NovaCoin",
"NVDX": "Nodvix",
"NVG": "NightVerse Game",
+ "NVG8": "Navigate",
"NVIR": "NvirWorld",
"NVL": "Nevula",
"NVOY": "Envoy",
@@ -10047,11 +10470,13 @@
"OAK": "Acorn Collective",
"OAS": "Oasis City",
"OASC": "Oasis City",
+ "OASI": "Oasis Metaverse",
"OASIS": "Oasis",
"OAT": "OAT Network",
"OATH": "OATH Protocol",
"OAX": "Oax",
"OB1INCH": "1inch (OmniBridge)",
+ "OBABYTRUMP": "Official Baby Trump",
"OBEMA": "burek obema",
"OBI": "Orbofi AI",
"OBICOIN": "OBI Real Estate",
@@ -10080,6 +10505,7 @@
"OCICAT": "OciCat",
"OCL": "Oceanlab",
"OCN": "Odyssey",
+ "OCNEST": "OcNest AI",
"OCO": "Owners Casino Online",
"OCP": "Omni Consumer Protocols",
"OCPR": "OC Protocol",
@@ -10090,8 +10516,9 @@
"OCTAVUS": "Octavus Prime",
"OCTAX": "OctaX",
"OCTI": "Oction",
- "OCTO": "OctoFi",
+ "OCTO": "OctonetAI",
"OCTOCOIN": "Octocoin",
+ "OCTOF": "OctoFi",
"OCTOIN": "Octoin Coin",
"OCW": "Online Cold Wallet",
"OCX": "Original Crypto Coin",
@@ -10113,8 +10540,14 @@
"OFCR": "CryptoPolice",
"OFE": "Ofero",
"OFF": "BlastOff",
+ "OFFI": "Official Elon Coin",
+ "OFFIC": "OFFICIAL SIMPSON",
+ "OFFICI": "OFFICIAL BARRON",
+ "OFFICIA": "Official Elon Coin",
"OFN": "Openfabric AI",
+ "OFT": "ONFA",
"OG": "OG Fan Token",
+ "OGC": "OGCommunity",
"OGCINU": "The OG Cheems Inu",
"OGD": "OLYMPIC GAMES DOGE",
"OGGIE": "Oggie",
@@ -10139,6 +10572,8 @@
"OHNOGG": "OHNHO (ohno.gg)",
"OHO": "OHO",
"OICOIN": "Osmium Investment Coin",
+ "OIIAOIIA": "spinning cat",
+ "OIK": "Space Nation",
"OIL": "Oiler",
"OILD": "OilWellCoin",
"OILX": "OilX Token",
@@ -10182,6 +10617,7 @@
"OMD": "OneMillionDollars",
"OME": "o-mee",
"OMEGA": "OMEGA",
+ "OMEGAX": "OmegaX Health",
"OMG": "OMG Network",
"OMGC": "OmiseGO Classic",
"OMI": "ECOMI",
@@ -10209,6 +10645,7 @@
"ONC": "One Cash",
"ONCH": "OnchainPoints.xyz",
"ONDO": "Ondo",
+ "ONDOAI": "Ondo DeFAI",
"ONE": "Harmony",
"ONES": "OneSwap DAO",
"ONET": "ONE Token",
@@ -10229,6 +10666,7 @@
"ONS": "One Share",
"ONSTON": "Onston",
"ONT": "Ontology",
+ "ONTACT": "OnTact",
"ONUS": "ONUS",
"ONX": "Onix",
"OOE": "OpenOcean",
@@ -10283,15 +10721,18 @@
"OPTIG": "Catgirl Optimus",
"OPTIM": "Optimus X",
"OPTIMOUSE": "Optimouse",
+ "OPTIO": "Optio",
"OPTION": "OptionCoin",
"OPU": "Opu Coin",
"OPUL": "Opulous",
"OPUS": "Opus",
"OPV": "OpenLive NFT",
"OPXVEVELO": "OpenX Locked Velo",
- "ORA": "Oracolxor",
+ "ORA": "ORA Coin",
"ORACLE": "Oracle AI",
"ORACLECHAIN": "OracleChain",
+ "ORACLER": "Oracler",
+ "ORACOLXOR": "Oracolxor",
"ORACUL": "Oracul Ai",
"ORAI": "Oraichain Token",
"ORAIX": "OraiDEX",
@@ -10323,6 +10764,7 @@
"ORGT": "Organic Token",
"ORI": "Origami",
"ORIGIN": "Origin Foundation",
+ "ORIGINA": "Original Gangsters",
"ORION": "Orion Money",
"ORKL": "Orakler",
"ORLY": "OrlyCoin",
@@ -10358,6 +10800,7 @@
"OSL": "OSL AI",
"OSMI": "OSMI",
"OSMO": "Osmosis",
+ "OSOL": "OSOL",
"OSQTH": "Opyn Squeeth",
"OSS": "OSSChain",
"OST": "OST",
@@ -10476,6 +10919,7 @@
"PAPI": "Papi",
"PAPO": "PAPO NINJA",
"PAPPAY": "PAPPAY",
+ "PAPPLE": "Pineapple",
"PAPU": "Papu Token",
"PAPUSHA": "Papusha",
"PAR": "Parachute",
@@ -10504,6 +10948,7 @@
"PASG": "Passage",
"PASL": "Pascal Lite",
"PASS": "Blockpass",
+ "PASTERNAK": "Ben Pasternak",
"PAT": "PATRON",
"PATEK": "Silly Patek",
"PATEX": "Patex",
@@ -10605,6 +11050,7 @@
"PECL": "PECland",
"PED": "PEDRO",
"PEDRO": "Pedro The Raccoon",
+ "PEE": "peecoin",
"PEEL": "Meta Apes",
"PEENO": "Peeno",
"PEEP": "Peepo",
@@ -10620,7 +11066,8 @@
"PEGAMAGA": "Pepe Maga",
"PEGG": "PokPok Golden Egg",
"PEGS": "PegShares",
- "PEIPEI": "PEIPEI",
+ "PEIPEI": "PeiPei",
+ "PEIPEICN": "PEIPEI",
"PEKA": "PEKA",
"PEKC": "Peacock Coin",
"PEKINU": "PEKI INU",
@@ -10636,6 +11083,7 @@
"PENGCOIN": "PENG",
"PENGU": "Pudgy Penguins",
"PENGUI": "Penguiana",
+ "PENGUIN": "Penguin",
"PENGYX": "PengyX",
"PENIS": "PenisGrow",
"PENJ": "Penjamin Blinkerton",
@@ -10687,6 +11135,7 @@
"PEPEMAGA": "Trump Pepe",
"PEPEMO": "PepeMo",
"PEPEMOON": "PEPEMOON",
+ "PEPEMUSK": "pepemusk",
"PEPEOFSOL": "Pepe of Solana",
"PEPEPI": "PEPEPi",
"PEPER": "Baby Pepe",
@@ -10768,6 +11217,7 @@
"PHAE": "Phaeton",
"PHALA": "Phalanx",
"PHAME": "PHAME",
+ "PHAR": "Pharaoh",
"PHAUNTEM": "Phauntem",
"PHB": "Phoenix Global [v2]",
"PHBD": "Polygon HBD",
@@ -10792,21 +11242,25 @@
"PHRYG": "PHRYGES",
"PHRYGE": "PHRYGES",
"PHRYGES": "The Phryges",
+ "PHRZ": "Pharaohs",
"PHS": "PhilosophersStone",
"PHT": "Photon Token",
"PHTC": "Photochain",
"PHTR": "Phuture",
"PHUN": "PHUNWARE",
"PHV": "PATHHIVE",
- "PI": "Plian",
+ "PI": "Pi Network",
"PIA": "Olympia AI",
+ "PIAI": "Pi Network AI",
"PIAS": "PIAS",
"PIB": "Pibble",
- "PICA": "PicaArtMoney",
+ "PICA": "Picasso",
+ "PICAARTMONEY": "PicaArtMoney",
"PICKL": "PICKLE",
"PICKLE": "Pickle Finance",
"PICO": "PicoGo",
"PICOLO": "PICOLO",
+ "PIDOGE": "Pi Network Doge",
"PIE": "Persistent Information Exchange",
"PIERRE": "sacré bleu",
"PIF": "Pepe Wif Hat",
@@ -10825,12 +11279,15 @@
"PIKAM": "Pikamoon",
"PIKE": "Pike Token",
"PIKO": "Pinnako",
+ "PILLAR": "PillarFi",
"PILOT": "Unipilot",
"PIM": "PIM",
- "PIN": "Pin",
+ "PIN": "PinLink",
+ "PINCHAIN": "Pin",
"PINCHI": "Da Pinchi",
"PINE": "Pine",
"PINETWORKDEFI": "Pi Network DeFi",
+ "PINEYE": "PinEye",
"PING": "CryptoPing",
"PINK": "PINK - The Panther",
"PINKCOIN": "PinkCoin",
@@ -10861,6 +11318,7 @@
"PIST": "Pist Trust",
"PIT": "Pitbull",
"PITCH": "PITCH",
+ "PIUU": "PIXIU",
"PIVN": "PIVN",
"PIVX": "Private Instant Verified Transaction",
"PIX": "Lampix",
@@ -10891,6 +11349,7 @@
"PLANET": "PLANET",
"PLANETCOIN": "PlanetCoin",
"PLANETS": "PlanetWatch",
+ "PLANT": "Plant",
"PLASTIK": "Plastiks",
"PLAT": "BitGuild PLAT",
"PLATC": "PlatinCoin",
@@ -10918,6 +11377,7 @@
"PLG": "Pledgecamp",
"PLGR": "Pledge Finance",
"PLI": "Plugin",
+ "PLIAN": "Plian",
"PLINK": "Chainlink (Polygon Portal)",
"PLM": "Plasmonics",
"PLMC": "Polimec",
@@ -10941,6 +11401,7 @@
"PLU": "Pluton",
"PLUG": "PL^Gnet",
"PLUGCN": "Plug Chain",
+ "PLUME": "Plume",
"PLUP": "PoolUp",
"PLURA": "PluraCoin",
"PLUS1": "PlusOneCoin",
@@ -11006,6 +11467,7 @@
"POK": "Pokmonsters",
"POKEGROK": "PokeGROK",
"POKEM": "Pokemonio",
+ "POKEMO": "Pokemon",
"POKEMON": "Pokemon",
"POKER": "PokerCoin",
"POKERFI": "PokerFi",
@@ -11025,6 +11487,7 @@
"POLL": "Pollchain",
"POLLUK": "Jasse Polluk",
"POLLUX": "Pollux Coin",
+ "POLLY": "Polynetica",
"POLNX": "eToro Polish Zloty",
"POLO": "NftyPlay",
"POLS": "Polkastarter",
@@ -11071,6 +11534,7 @@
"POPE": "PopPepe",
"POPECOIN": "Popecoin",
"POPEPE": "POPEPE",
+ "POPG": "POPG",
"POPGOAT": "Goatseus Poppimus",
"POPK": "POPKON",
"POPO": "popo",
@@ -11102,6 +11566,8 @@
"POTATO": "Potato",
"POTS": "Moonpot",
"POTTER": "POTTER",
+ "POTUS": "President Trump",
+ "POTUS47": "Trump Coin",
"POU": "Pou",
"POUPE": "Poupe",
"POUW": "Pouwifhat",
@@ -11118,6 +11584,7 @@
"PPAY": "Plasma Finance",
"PPBLZ": "Pepemon Pepeballs",
"PPC": "PeerCoin",
+ "PPCOIN": "Project Plutus",
"PPFT": "Papparico Finance",
"PPI": "Primpy",
"PPIZZA": "P Pizza",
@@ -11249,6 +11716,7 @@
"PTD": "Pilot",
"PTERIA": "Pteria",
"PTF": "PowerTrade Fuel",
+ "PTGC": "The Grays Currency",
"PTH": "PlasticHero",
"PTI": "Paytomat",
"PTM": "Potentiam",
@@ -11285,6 +11753,9 @@
"PUMP": "PUMP",
"PUMPBTC": "pumpBTC",
"PUMPFUNBAN": "Pump Fun Ban",
+ "PUMPIT": "BOGDANOFF",
+ "PUMPTRUMP": "PUMP TRUMP",
+ "PUMPY": "WOW MOON LAMBO PUMPPPPPPY",
"PUN": "Punkko",
"PUNCH": "PUNCHWORD",
"PUNDIX": "Pundi X",
@@ -11306,7 +11777,9 @@
"PURA": "Pura",
"PURE": "Puriever",
"PUREALT": "Pure",
+ "PURP": "Purple Platform io",
"PURPE": "Purple Pepe",
+ "PURPLEBTC": "Purple Bitcoin",
"PURR": "Purr",
"PURRC": "Purrcoin",
"PURSE": "Pundi X PURSE",
@@ -11319,6 +11792,7 @@
"PUSSY": "Pussy Financial",
"PUSSYINBIO": "Pussy In Bio",
"PUT": "PutinCoin",
+ "PUTIN": "Putin Meme",
"PUUSH": "puush da button",
"PUX": "pukkamex",
"PVC": "PVC Meta",
@@ -11328,8 +11802,10 @@
"PVU": "Plant vs Undead Token",
"PWAR": "PolkaWar",
"PWC": "PixelWorldCoin",
+ "PWEASE": "Pwease",
"PWH": "pepewifhat",
"PWINGS": "JetSwap pWings",
+ "PWOG": "Purple Fwog",
"PWON": "Personal Wager",
"PWR": "MaxxChain",
"PWRC": "PWR Coin",
@@ -11360,6 +11836,7 @@
"PYRV1": "Vulcan Forged v1",
"PYT": "Payther",
"PYTH": "Pyth Network",
+ "PYTHIA": "Pythia",
"PYUSD": "PayPal USD",
"PZETH": "pzETH",
"PZM": "Prizm",
@@ -11475,6 +11952,7 @@
"QWAN": "The QWAN",
"QWARK": "Qwark",
"QWC": "Qwertycoin",
+ "QWEN": "Qwen AI",
"QWLA": "Qawalla",
"QWT": "QoWatt",
"QXC": "QuantumXC",
@@ -11505,7 +11983,7 @@
"RAFT": "Raft",
"RAGDOLL": "Ragdoll",
"RAGE": "Rage Fan",
- "RAI": "Rai Reflex Index",
+ "RAI": "Reploy",
"RAID": "Raid Token",
"RAIDER": "Crypto Raiders",
"RAIF": "RAI Finance",
@@ -11515,6 +11993,7 @@
"RAINC": "RainCheck",
"RAINCO": "Rain Coin",
"RAINI": "Rainicorn",
+ "RAIREFLEX": "Rai Reflex Index",
"RAISE": "Raise Token",
"RAIT": "Rabbitgame",
"RAIZER": "RAIZER",
@@ -11524,7 +12003,9 @@
"RALLY": "Trump Rally",
"RAM": "Ramifi Protocol",
"RAMA": "Ramestta",
+ "RAME": "Ramen",
"RAMEN": "RamenSwap",
+ "RAMON": "Ramon",
"RAMP": "RAMP",
"RANKER": "RankerDao",
"RAP": "Philosoraptor",
@@ -11544,6 +12025,7 @@
"RAVELOUS": "Ravelous",
"RAVEN": "Raven Protocol",
"RAVENCOINC": "Ravencoin Classic",
+ "RAWDOG": "RawDog",
"RAWG": "RAWG",
"RAY": "Raydium",
"RAYS": "Rays Network",
@@ -11607,12 +12089,17 @@
"RDX": "Redux Protocol",
"REA": "Realisto",
"REACH": "/Reach",
+ "REACT": "Reactive Network",
"REAL": "RealLink",
+ "REALESTATE": "RealEstate",
"REALM": "Realm",
"REALMS": "Realms of Ethernity",
"REALP": "Real Pepe",
"REALPLATFORM": "REAL",
"REALTRACT": "RealTract",
+ "REALUSD": "Real USD",
+ "REALUSDV1": "Real USD v1",
+ "REALUSDV2": "Real USD v2",
"REALY": "Realy Metaverse",
"REAP": "ReapChain",
"REAPER": "Grim Finance",
@@ -11637,6 +12124,7 @@
"REDLC": "Redlight Chain",
"REDLUNA": "Redluna",
"REDN": "Reden",
+ "REDNOTE": "RedNote Xiaohongshu",
"REDO": "Resistance Dog",
"REDP": "Red Ponzi Gud",
"REDPEPE": "Red Pepe",
@@ -11652,7 +12140,9 @@
"REFLECTO": "Reflecto",
"REFTOKEN": "RefToken",
"REFUND": "Refund",
+ "REG": "RealToken Ecosystem Governance",
"REGALCOIN": "Regalcoin",
+ "REGE": "Regent of the North Winds",
"REGEN": "Regen Network",
"REGENT": "REGENT COIN",
"REGI": "Resistance Girl",
@@ -11662,10 +12152,12 @@
"REIGN": "Reign of Terror",
"REINDEER": "Reindeer",
"REKT": "REKT",
- "REKT2": "REKT 2.0",
+ "REKTV2": "REKT 2.0",
+ "REKTV3": "REKT v3 (rekt.game)",
"REL": "Reliance",
"RELAY": "Relay Token",
"RELI": "Relite Finance",
+ "RELIGN": "RELIGN",
"RELOADED": "Doge Reloaded",
"RELVT": "Relevant",
"REM": "REMME",
@@ -11673,6 +12165,7 @@
"REME": "REME-Coin",
"REMILIA": " Remilia",
"REMIT": "BlockRemit",
+ "REMMETA": "Real Estate Metaverse",
"REN": "REN",
"RENA": "Warena",
"RENBTC": "renBTC",
@@ -11683,6 +12176,7 @@
"RENQ": "Renq Finance",
"RENS": "Rens",
"RENT": "Rent AI",
+ "RENTA": "Renta Network",
"RENTBE": "Rentberry",
"REP": "Augur",
"REPE": "Resistance Pepe",
@@ -11720,7 +12214,9 @@
"REVV": "REVV",
"REW": "Review.Network",
"REWARD": "Rewardable",
+ "REWARDS": "Solana Rewards",
"REX": "Imbrex",
+ "REXBT": "rexbt by VIRTUALS",
"REXHAT": "rexwifhat",
"REZ": "Renzo",
"RF": "Raido Financial",
@@ -11757,16 +12253,20 @@
"RIC": "Riecoin",
"RICE": "RiceFarm",
"RICECOIN": "RiceCoin",
- "RICH": "Richie",
+ "RICHIE": "Richie2.0",
+ "RICHIEV1": "Richie",
"RICHOFME": "Rich Of Memes",
"RICHR": "RichRabbit",
"RICK": "Infinite Ricks",
"RICKMORTY": "Rick And Morty",
"RIDE": "Holoride",
+ "RIDECHAIN": "Ride Chain Coin",
"RIDEMY": "Ride My Car",
"RIF": "RIF Token",
"RIF3": "MetaTariffv3",
+ "RIFA": "Rifampicin",
"RIFI": "Rikkei Finance",
+ "RIFT": "RIFT AI",
"RIGEL": "Rigel Finance",
"RIK": "RIKEZA",
"RIL": "Rilcoin",
@@ -11800,6 +12300,7 @@
"RITZ": "Ritz.Game",
"RIVUS": "RivusDAO",
"RIYA": "Etheriya",
+ "RIZ": "Rivalz Network",
"RIZE": "Rizespor Token",
"RIZO": "HahaYes",
"RIZOLOL": "Rizo",
@@ -11843,10 +12344,12 @@
"RNTB": "BitRent",
"RNX": "ROONEX",
"ROAD": "ROAD",
+ "ROAM": "Roam Token",
"ROAR": "Alpha DEX",
"ROARINGCAT": "Roaring Kitty",
"ROB": "ROB",
"ROBET": "RoBet",
+ "ROBI": "Robin Rug",
"ROBIN": "Robin of Da Hood",
"ROBINH": "ROBIN HOOD",
"ROBO": "RoboHero",
@@ -11889,6 +12392,9 @@
"ROOT": "The Root Network",
"ROOTCOIN": "RootCoin",
"ROOTS": "RootProject",
+ "ROP": "Redemption Of Pets",
+ "ROPE": "Rope Token",
+ "ROPELOL": "Rope",
"ROPIRITO": "Ropirito",
"ROS": "ROS Coin",
"ROSA": "Rosa Inu",
@@ -11900,6 +12406,7 @@
"ROSX": "Roseon",
"ROT": "Rotten",
"ROTTY": "ROTTYCOIN",
+ "ROUGE": "Rouge Studio",
"ROUND": "RoundCoin",
"ROUP": "Roup (Ordinals)",
"ROUSH": "Roush Fenway Racing Fan Token",
@@ -11952,6 +12459,7 @@
"RSUN": "RisingSun",
"RSUSHI": "Sushi (Rainbow Bridge)",
"RSV": "Reserve",
+ "RSVV1": "Reserve v1",
"RSWETH": "Restaked Swell Ethereum",
"RT2": "RotoCoin",
"RTB": "AB-CHAIN",
@@ -11999,6 +12507,8 @@
"RUX": "Gacrux NFT",
"RVC": "Revenue Coin",
"RVF": "RocketX exchange",
+ "RVFV1": "RocketX exchange v1",
+ "RVFV2": "RocketX exchange v2",
"RVL": "Revolotto",
"RVLNG": "RevolutionGames",
"RVLT": "Revolt 2 Earn",
@@ -12023,6 +12533,7 @@
"RXD": "Radiant",
"RXO": "RocketXRP Official",
"RXT": "RIMAUNANGIS",
+ "RYAN": "OFFICIAL RYAN",
"RYC": "RoyalCoin",
"RYCN": "RoyalCoin 2.0",
"RYD": "RYderOSHI",
@@ -12034,6 +12545,7 @@
"RYU": "The Blue Dragon",
"RYZ": "Anryze",
"RZR": "RazorCoin",
+ "RZUSD": "RZUSD",
"RedFlokiCEO": "Red Floki CEO",
"S": "Sonic Labs",
"S2K": "Sports 2K75",
@@ -12041,6 +12553,7 @@
"S4F": "S4FE",
"S8C": "S88 Coin",
"SA": "Superalgos",
+ "SAAS": "SaaSGo",
"SABAI": "Sabai Protocol",
"SABLE": "Sable Finance",
"SABR": "SABR Coin",
@@ -12170,7 +12683,7 @@
"SBTC": "Super Bitcoin",
"SC": "Siacoin",
"SC20": "Shine Chain",
- "SCA": "SiaClassic",
+ "SCA": "Scallop",
"SCALE": "Scalia Infrastructure",
"SCAM": "Scam Coin",
"SCAMP": "ScamPump",
@@ -12244,8 +12757,10 @@
"SDAO": "SingularityDAO",
"SDC": "ShadowCash",
"SDCRV": "Stake DAO CRV",
+ "SDEUSD": "Staked deUSD",
"SDEX": "SmarDex",
"SDL": "Saddle Finance",
+ "SDM": "Shieldeum",
"SDME": "SDME",
"SDN": "Shiden Network",
"SDO": "TheSolanDAO",
@@ -12300,7 +12815,7 @@
"SEN": "Sentaro",
"SENATE": "SENATE",
"SENC": "Sentinel Chain",
- "SEND": "Social Send",
+ "SEND": "Suilend",
"SENDOR": "Sendor",
"SENK": "Senk",
"SENNO": "SENNO",
@@ -12311,6 +12826,7 @@
"SENSOV1": "SENSO v1",
"SENSUS": "Sensus",
"SENT": "Sentinel",
+ "SENTAI": "SentAI",
"SENTI": "Sentinel Bot Ai",
"SENTR": "Sentre Protocol",
"SEON": "Seedon",
@@ -12320,6 +12836,7 @@
"SEPA": "Secure Pad",
"SEQ": "Sequence",
"SER": "Secretum",
+ "SERAPH": "Seraph",
"SERG": "Seiren Games Network",
"SERO": "Super Zero",
"SERP": "Shibarium Perpetuals",
@@ -12396,6 +12913,7 @@
"SHARK": "Sharky",
"SHARKC": "Shark Cat",
"SHARKI": "Sharki",
+ "SHARKYSH": "Sharky Sharkx",
"SHARP": "Sharp",
"SHARPE": "Sharpe Capital",
"SHAUN": "SHAUN INU",
@@ -12403,6 +12921,7 @@
"SHC": "School Hack Coin",
"SHD": "ShardingDAO",
"SHDW": "Shadow Token",
+ "SHDX": "Shido DEX",
"SHE": "Shine Chain",
"SHEB": "SHEBOSHIS",
"SHEEESH": "Secret Gem",
@@ -12410,7 +12929,8 @@
"SHEESHA": "Sheesha Finance",
"SHEGEN": "Aiwithdaddyissues",
"SHEI": "SheikhSolana",
- "SHELL": "Shell Token",
+ "SHELL": "MyShell",
+ "SHELLTOKEN": "Shell Token",
"SHEN": "Shen",
"SHEPE": "Shiba V Pepe",
"SHERA": "Shera Tokens",
@@ -12434,6 +12954,7 @@
"SHIBAMOM": "Shiba Mom",
"SHIBAR": "Shibarium Name Service",
"SHIBARMY": "Shib Army",
+ "SHIBAW": "Shiba $Wing",
"SHIBAY": "Shiba Inu Pay",
"SHIBAZILLA": "ShibaZilla2.0",
"SHIBCAT": "SHIBCAT",
@@ -12476,6 +12997,7 @@
"SHIP": "ShipChain",
"SHIR": "SHIRO",
"SHIRO": "Shiro Neko",
+ "SHIROSOL": "Shiro Neko (shirosol.online)",
"SHIRYOINU": "Shiryo-Inu",
"SHISHA": "Shisha Coin",
"SHIT": "I will poop it NFT",
@@ -12489,7 +13011,8 @@
"SHO": "Showcase Token",
"SHOE": "ShoeFy",
"SHOG": "SHOG",
- "SHOGGOTH": "Shoggoth",
+ "SHOGGOTH": "Shoggoth (shoggoth.monster)",
+ "SHOGGOTHAI": "Shoggoth",
"SHOKI": "Shoki",
"SHON": "ShonToken",
"SHOOT": "Mars Battle",
@@ -12517,13 +13040,16 @@
"SHUFFLE": "SHUFFLE!",
"SHVR": "Shivers",
"SHX": "Stronghold Token",
+ "SHY": "Shytoshi Kusama",
"SHYTCOIN": "ShytCoin",
"SI": "Siren",
+ "SIACLASSIC": "SiaClassic",
"SIB": "SibCoin",
"SIBA": "SibaInu",
"SIC": "Swisscoin",
"SID": "Sid",
"SIDE": "Side.xyz",
+ "SIDELINED": "Sidelined?",
"SIDESHIFT": "SideShift Token",
"SIDUS": "Sidus",
"SIERRA": "Sierracoin",
@@ -12549,9 +13075,11 @@
"SILVA": "Silva Token",
"SILVER": "SILVER",
"SILVERKRC": "Silver KRC-20",
+ "SILVERNOV": "Silvernova Token",
"SILVERSTAND": "Silver Standard",
"SILVERWAY": "Silverway",
"SIM": "Simpson",
+ "SIMBA": "SIMBA The Sloth",
"SIMP": "SO-COL",
"SIMPLE": "SimpleChain",
"SIMPS": "Simpson MAGA",
@@ -12577,6 +13105,7 @@
"SIPHER": "Sipher",
"SIPHON": "Siphon Life Spell",
"SIR": "Sir",
+ "SIREN": "siren",
"SIRIUS": "first reply",
"SIS": "Symbiosis Finance",
"SISA": "Strategic Investments in Significant Areas",
@@ -12587,11 +13116,13 @@
"SIUUU": "Crustieno Renaldo",
"SIV": "Sivasspor Token",
"SIX": "SIX Network",
+ "SIXP": "Sixpack Miner",
"SIXPACK": "SIXPACK",
"SIZ": "Sizlux",
"SIZE": "SIZE",
"SJCX": "StorjCoin",
"SKAI": "Skillful AI",
+ "SKAIN": "SKAINET",
"SKB": "SkullBuzz",
"SKBDI": "Skibidi Toilet",
"SKC": "Skeincoin",
@@ -12609,7 +13140,9 @@
"SKIN": "Skincoin",
"SKING": "Solo King",
"SKINS": "Coins & Skins",
+ "SKINUT": "Skimask Pnut",
"SKIPUP": "SKI MASK PUP",
+ "SKITTEN": "Ski Mask Kitten",
"SKL": "SKALE Network",
"SKLAY": "sKLAY",
"SKM": "Skrumble Network",
@@ -12638,7 +13171,7 @@
"SLAP": "CatSlap",
"SLAVI": "Slavi Coin",
"SLB": "Solberg",
- "SLC": "Solice",
+ "SLC": "Silencio",
"SLCL": "Solcial",
"SLEEP": "Sleep Ecosystem",
"SLEEPEE": "SleepFuture",
@@ -12672,7 +13205,7 @@
"SLRS": "Solrise Finance",
"SLS": "SaluS",
"SLST": "SmartLands",
- "SLT": "Social Lending Network",
+ "SLT": "Salute",
"SLUGDENG": "SLUG DENG",
"SLUMBO": "SLUMBO",
"SLVX": "eToro Silver",
@@ -12715,6 +13248,7 @@
"SMLY": "SmileyCoin",
"SMM": "TrendingTool.io",
"SMOG": "Smog",
+ "SMOK": "Smoking Chicken Fish",
"SMOKE": "Smoke",
"SMOL": "Smolcoin",
"SMOLE": "smolecoin",
@@ -12741,9 +13275,12 @@
"SNA": "SUKUYANA",
"SNAC": "SnackboxAI",
"SNACK": "Crypto Snack",
+ "SNAI": "SwarmNode.ai",
"SNAIL": "SnailBrook",
"SNAKE": "snake",
+ "SNAKEMOON": "Snakemoon",
"SNAKES": "Snakes Game",
+ "SNAKT": "Sna-King Trump",
"SNAP": "SnapEx",
"SNAPCAT": "Snapcat",
"SNB": "SynchroBitcoin",
@@ -12770,6 +13307,7 @@
"SNM": "SONM",
"SNMT": "Satoshi Nakamoto Token",
"SNN": "SeChain",
+ "SNO": "Snow Leopard",
"SNOB": "Snowball",
"SNOLEX": "Snolex",
"SNOOP": "SnoopDAO",
@@ -12807,6 +13345,8 @@
"SOCC": "SocialCoin",
"SOCCER": "SoccerInu",
"SOCIAL": "Phavercoin",
+ "SOCIALLT": "Social Lending Network",
+ "SOCIALSEND": "Social Send",
"SOCKS": "Unisocks",
"SOCOLA": "SOCOLA INU",
"SODA": "SODA Coin",
@@ -12817,7 +13357,8 @@
"SOFTCO": "SOFT COQ INU",
"SOH": "Stohn Coin",
"SOHOT": "SOHOTRN",
- "SOIL": "SoilCoin",
+ "SOIL": "Soil",
+ "SOILCOIN": "SoilCoin",
"SOJ": "Sojourn Coin",
"SOK": "shoki",
"SOKU": "Soku Swap",
@@ -12831,6 +13372,7 @@
"SOLAN": "Solana Beach",
"SOLANAP": "Solana Poker",
"SOLANAS": "Solana Swap",
+ "SOLANATREASURY": "Solana Treasury Machine",
"SOLAPE": "SolAPE Token",
"SOLAR": "Solar",
"SOLARA": "Solara",
@@ -12852,8 +13394,10 @@
"SOLETF": "SOL ETF",
"SOLEX": "Solex Launchpad",
"SOLFI": "SoliDefi",
+ "SOLFUN": "SolFun",
"SOLGOAT": "SOLGOAT",
"SOLGUN": "Solgun",
+ "SOLIC": "Solice",
"SOLID": "Solidified",
"SOLIDSEX": "SOLIDsex: Tokenized veSOLID",
"SOLITO": "SOLITO",
@@ -12877,9 +13421,12 @@
"SOLSPONGE": "Solsponge",
"SOLT": "Soltalk AI",
"SOLTR": "SolTrump",
+ "SOLV": "Solv Protocol",
"SOLVBTC": "Solv Protocol SolvBTC",
"SOLVBTCBBN": "Solv Protocol SolvBTC.BBN",
"SOLVBTCCORE": "Solv Protocol SolvBTC.CORE",
+ "SOLVBTCENA": "SolvBTC Ethena",
+ "SOLVBTCJUP": "SolvBTC Jupiter",
"SOLVE": "SOLVE",
"SOLWIF": "Solwif",
"SOLX": "SolarX",
@@ -12890,14 +13437,13 @@
"SOM": "Souls of Meta",
"SOMA": "Soma",
"SOMM": "Sommelier",
- "SOMNIUM": "Somnium Space CUBEs",
"SOMPS": "SompsOnKas",
"SON": "Simone",
"SONAR": "SonarWatch",
"SONG": "Song Coin",
"SONGOKU": "SONGOKU",
- "SONIC": "Sonic",
"SONICO": "Sonic",
+ "SONICSONIC": "Sonic",
"SONICWIF": "SonicWifHat",
"SONNE": "Sonne Finance",
"SONOF": "Son of Solana",
@@ -12915,6 +13461,7 @@
"SORAI": "Sora AI",
"SORAPORN": "Sora Porn",
"SOSNOVKINO": "Sosnovkino",
+ "SOSO": "SoSoValue",
"SOSWAP": "Solana Swap",
"SOT": "Soccer Crypto",
"SOTA": "SOTA Finance",
@@ -12938,6 +13485,7 @@
"SPACE": "Spacelens",
"SPACECOIN": "SpaceCoin",
"SPACED": "SPACE DRAGON",
+ "SPACEM": "Spacem Token",
"SPACEPI": "SpacePi",
"SPAD": "SolPad",
"SPAI": "Starship AI",
@@ -12971,6 +13519,7 @@
"SPENDC": "SpendCoin",
"SPENT": "Espento",
"SPEPE": "SolanaPepe",
+ "SPERG": "Bloomsperg Terminal",
"SPEX": "StepEx",
"SPF": "SportyCo",
"SPFC": "São Paulo FC Fan Token",
@@ -13023,6 +13572,7 @@
"SPRING": "Spring",
"SPRITZMOON": "SpritzMoon Crypto Token",
"SPRKL": "Sparkle Loyalty",
+ "SPROUT": "Sprout",
"SPRT": "Sportium",
"SPRTS": "Sprouts",
"SPRTZ": "SpritzCoin",
@@ -13040,6 +13590,7 @@
"SPY": "Smarty Pay",
"SPYRO": "SPYRO",
"SQAT": "Syndiqate",
+ "SQD": "SQD",
"SQG": "Squid Token",
"SQGROW": "SquidGrow",
"SQL": "Squall Coin",
@@ -13084,6 +13635,7 @@
"SSD": "Sonic Screw Driver Coin",
"SSDX": "SpunkySDX",
"SSE": "Soroosh Smart Ecosystem",
+ "SSEV1": "Soroosh Smart Ecosystem v1",
"SSG": "Surviving Soldiers",
"SSGT": "Safeswap",
"SSH": "StreamSpace",
@@ -13092,6 +13644,7 @@
"SSLX": "StarSlax",
"SSNC": "SatoshiSync",
"SSOL": "Solayer SOL",
+ "SSR": "SOL Strategic Reserve",
"SSS": "StarSharks",
"SSSSS": "Snake wif Hat",
"SST": "SIMBA Storage Token",
@@ -13120,10 +13673,12 @@
"STANDARD": "Stakeborg DAO",
"STAPT": "Ditto Staked Aptos",
"STAR": "FileStar",
+ "STAR10": "Ronaldinho Coin",
"STARAMBA": "Staramba",
"STARBASE": "Starbase",
"STARC": "StarChain",
"STARDOGE": "StarDOGE",
+ "STARGATEAI": "Stargate AI Agent",
"STARL": "StarLink",
"STARLAUNCH": "StarLaunch",
"STARLY": "Starly",
@@ -13191,6 +13746,7 @@
"STIMA": "STIMA",
"STING": "Sting",
"STINJ": "Stride Staked INJ",
+ "STITCH": "Stitch",
"STIX": "STIX",
"STJUNO": "Stride Staked JUNO",
"STK": "STK Token",
@@ -13212,6 +13768,7 @@
"STNK": "Stonks",
"STO": "Save The Ocean",
"STOC": "STO Cash",
+ "STOCK": "Digital Asset Stockpile",
"STOG": "Stooges",
"STOGE": "Stoner Doge Finance",
"STOIC": "stoicDAO",
@@ -13274,6 +13831,7 @@
"STUDENTC": "Student Coin",
"STUFF": "STUFF.io",
"STUMEE": "Stride Staked UMEE",
+ "STUPID": "StupidCoin",
"STUSDT": "Staked USDT",
"STV": "Sativa Coin",
"STWEMIX": "Staked WEMIX",
@@ -13285,22 +13843,26 @@
"STZETA": "ZetaEarn",
"STZU": "Shihtzu Exchange Token",
"SU": "Smol Su",
+ "SUAI": "SuiAI",
"SUB": "Subsocial",
"SUBAWU": "Subawu Token",
"SUBF": "Super Best Friends",
"SUBS": "Substratum Network",
"SUCR": "Sucre",
+ "SUD": "Sudo Labs",
"SUDO": "sudoswap",
"SUGAR": "Sugar Exchange",
"SUI": "Sui",
"SUIA": "SUIA",
"SUIAI": "SUI Agents",
"SUIB": "Suiba Inu",
+ "SUIDEPIN": "Sui DePIN",
"SUIJAK": "Suijak",
"SUILAMA": "Suilama",
"SUIMAN": "Suiman",
"SUIMON": "Sui Monster",
"SUIP": "SuiPad",
+ "SUIRWA": "Sui RWA",
"SUISHIB": "SuiShiba",
"SUITE": "Suite",
"SUKI": "SUKI",
@@ -13380,11 +13942,13 @@
"SWAGT": "Swag Token",
"SWAI": "Safe Water AI",
"SWAMP": "Swampy",
- "SWAN": "Black Swan",
+ "SWAN": "Swan Chain",
+ "SWANSOL": "Black Swan",
"SWAP": "Trustswap",
"SWAPP": "SWAPP Protocol",
"SWAPZ": "SWAPZ.app",
"SWARM": "SwarmCoin",
+ "SWARMS": "Swarms",
"SWASH": "Swash",
"SWAY": "Sway Social",
"SWBTC": "Swell Restaked BTC",
@@ -13425,6 +13989,7 @@
"SWPR": "Swapr",
"SWPRS": "Maid Sweepers",
"SWRV": "Swerve",
+ "SWRX": "SwissRx Coin",
"SWT": "Swarm City Token",
"SWTH": "Carbon",
"SWTS": "SWEETS",
@@ -13449,6 +14014,8 @@
"SYLO": "Sylo",
"SYLV": "Sylvester",
"SYM": "SymVerse",
+ "SYMM": "Symmio",
+ "SYMP": "Sympson AI",
"SYN": "Synapse",
"SYNC": "Syncus",
"SYNCC": "SyncCoin",
@@ -13462,7 +14029,8 @@
"SYNR": "MOBLAND",
"SYNT": "Synthetix Network",
"SYNTE": "Synternet",
- "SYNTH": "Synthswap",
+ "SYNTH": "SYNTHR",
+ "SYNTHSWAP": "Synthswap",
"SYNX": "Syndicate",
"SYPOOL": "Sypool",
"SYRUP": "Syrup",
@@ -13508,6 +14076,7 @@
"TANUKI": "Tanuki",
"TANUPAD": "Tanuki Launchpad",
"TAO": "Bittensor",
+ "TAOBOT": "tao.bot",
"TAONU": "TAO INU",
"TAOP": "TaoPad",
"TAOTOOLS": "TAOTools",
@@ -13515,6 +14084,7 @@
"TAPC": "Tap Coin",
"TAPPINGCOIN": "TappingCoin",
"TAPROOT": "Taproot Exchange",
+ "TAPS": "TapSwap",
"TAPT": "Tortuga Staked Aptos",
"TARA": "Taraxa",
"TARAL": "TARALITY",
@@ -13538,6 +14108,7 @@
"TAUR": "Marnotaur",
"TAVA": "ALTAVA",
"TAX": "MetaToll",
+ "TAXAD": "TAXAD",
"TAXI": "Robotaxi",
"TAXLESSTRUMP": "MAGA TAXLESS",
"TBAC": "BlockAura",
@@ -13554,6 +14125,7 @@
"TBFT": "Türkiye Basketbol Federasyon Token",
"TBIS": "TBIS token",
"TBL": "Tombola",
+ "TBR": "Tuebor",
"TBRIDGE": "tBridge Token",
"TBT": "T-BOT",
"TBTC": "tBTC",
@@ -13598,6 +14170,7 @@
"TEC": "TeCoin",
"TECAR": "Tesla Cars",
"TECH": "TechCoin",
+ "TECK": "Technet",
"TECRA": "TecraCoin",
"TED": "TED",
"TEDDY": "Teddy Doge v2",
@@ -13609,6 +14182,7 @@
"TEL": "Telcoin",
"TELE": "Miracle Tele",
"TELEBTC": "teleBTC",
+ "TELEPORT": "Teleport System Token",
"TELL": "Tellurion",
"TELLER": "Teller",
"TELO": "Telo Meme Coin",
@@ -13656,6 +14230,7 @@
"TETU": "TETU",
"TEVA": "Tevaera",
"TEW": "Trump in a memes world",
+ "TEX": "Terrax",
"TF47": "Trump Force 47",
"TFBX": "Truefeedback Token",
"TFC": "The Freedom Coin",
@@ -13679,7 +14254,7 @@
"THAVAGE": "Mike Tython",
"THC": "The Hempcoin",
"THD": "Trump Harris Debate",
- "THE": "The Protocol",
+ "THE369": "The 369 code",
"THE9": "THE9",
"THEAICOIN": "AI",
"THEB": "The Boys Club",
@@ -13689,7 +14264,9 @@
"THECAT": "THECAT",
"THECITADEL": "The Citadel",
"THEDAO": "The DAO",
+ "THEDOGE": "The Dogefather",
"THEF": "The Flash Currency",
+ "THEFACE": "FACE",
"THEG": "The GameHub",
"THEHARAMBE": "Harambe",
"THEM": "The Meta DAO",
@@ -13697,7 +14274,9 @@
"THEN": "THENA",
"THEO": "Theopetra",
"THEOS": "Theos",
+ "THEP": "The Protocol",
"THES": "The Standard Protocol (USDS)",
+ "THESTANDARD": "Standard Token",
"THETA": "Theta Network",
"THETAN": "Thetan Coin",
"THETRIBE": "The Tribe",
@@ -13731,6 +14310,7 @@
"TIA": "Celestia",
"TIANHE": "Tianhe",
"TIC": "TrueInvestmentCoin",
+ "TICO": "Tico",
"TIDAL": "Tidal Finance",
"TIDDIES": "TIDDIES",
"TIDE": "Tidalflats",
@@ -13747,13 +14327,17 @@
"TIIM": "TriipMiles",
"TIK": "ChronoBase",
"TIKI": "Tiki Token",
+ "TIKTOK": "Tiktok",
"TIKTOKEN": "TikToken",
"TIM": "TIMTIM GAMES",
"TIME": "Chrono.tech",
+ "TIMEFUN": "timefun",
"TIMES": "DARKTIMES",
- "TIMI": "Timicoin",
+ "TIMI": "This Is My Iguana",
+ "TIMICOIN": "Timicoin",
"TIN": "Token IN",
"TINC": "Tiny Coin",
+ "TIND": "Tinder Swindler",
"TINKU": "TinkuCoin",
"TINU": "Telegram Inu",
"TINY": "TinyBits",
@@ -13890,6 +14474,7 @@
"TORII": "Torii Finance",
"TORN": "Tornado Cash",
"TORO": "Toro Inoue",
+ "TOROSOL": "Toro",
"TORSY": "TORSY",
"TOS": "ThingsOperatingSystem",
"TOSA": "TosaInu BSC",
@@ -13937,6 +14522,7 @@
"TRADEBOT": "TradeBot",
"TRADECHAIN": "Trade Chain",
"TRADEX": "TradeX AI",
+ "TRAI": "Trackgood AI",
"TRAID": "Traid",
"TRAIMP": "TRUMP AI",
"TRAIN": "Trump Train",
@@ -13961,6 +14547,7 @@
"TRDS": "Traders Token",
"TRDT": "Trident",
"TREAT": "Treat",
+ "TREB": "Treble",
"TRECENTO": "Trecento Blockchain Capital",
"TREE": "Tree",
"TREEB": "Retreeb",
@@ -14023,12 +14610,13 @@
"TRUM": "TrumpBucks",
"TRUMAGA": "TrumpMAGA",
"TRUMATIC": "TruFin Staked MATIC",
- "TRUMP": "MAGA",
+ "TRUMP": "OFFICIAL TRUMP",
"TRUMP2": "Trump2024",
"TRUMP2024": "Donald Trump",
"TRUMP3": "Trump MP3",
"TRUMP47": "47th President of the United States",
"TRUMPA": "TRUMP AI",
+ "TRUMPAI": "Trump Maga AI",
"TRUMPAMANIA": "TRUMPAMANIA",
"TRUMPARMY": "Trump Army",
"TRUMPBASE": "MAGA (magatrumponbase.tech)",
@@ -14036,11 +14624,13 @@
"TRUMPC": "TrumpCat",
"TRUMPCA": "Trump Card",
"TRUMPCAT": "TRUMPCAT",
+ "TRUMPCATF": "Trump Cat Family",
"TRUMPCATS": "Trump Golden Cat",
"TRUMPCOIN": "TrumpCoin",
"TRUMPDAO": "TRUMP DAO",
"TRUMPDO": "TRUMP",
"TRUMPDOGE": "Trump Doge",
+ "TRUMPDOGECOIN": "DOGE",
"TRUMPE": "Trump Pepe",
"TRUMPEPE": "Trump Pepe",
"TRUMPER": "Trump Era",
@@ -14048,16 +14638,19 @@
"TRUMPHAT": "Trump Hat",
"TRUMPINU": "Trump Inu",
"TRUMPJ": "TRUMPJR",
- "TRUMPJR": "TrumpJr",
+ "TRUMPJR": "OFFICIAL TRUMP JR",
+ "TRUMPJRVIP": "TrumpJr",
"TRUMPM": "TRUMP MAGA PRESIDENT",
"TRUMPMA": "TRUMP MAGA SUPER",
"TRUMPMAGA": "President Trump MAGA",
"TRUMPONBASE": "TRUMP ON BASE",
+ "TRUMPPROJECT": "Trump Project 2025",
"TRUMPS": "Trump SOL",
"TRUMPSB": "TrumpsBags",
"TRUMPSFIGHT": "TrumpsFight",
"TRUMPSHIBA": "Trump Shiba",
"TRUMPTECH": "Trump Tech",
+ "TRUMPTESLA": "Trump Tesla",
"TRUMPTITANS": "TrumpTitans",
"TRUMPVANCE": "Trump Vance 2024",
"TRUMPX": "Trump X-Maga",
@@ -14065,6 +14658,7 @@
"TRUNK": "Elephant Money",
"TRUST": "TrustDAO",
"TRUSTNFT": "TrustNFT",
+ "TRUT": "Truth",
"TRUTH": "TruthGPT",
"TRUTHFI": "Truthfi",
"TRV": "TrustVerse",
@@ -14094,6 +14688,9 @@
"TSLT": "Tamkin",
"TSN": "Tsunami Exchange Token",
"TSR": "Tesra",
+ "TST": "Test",
+ "TSTAI": "Test AI",
+ "TSTS": "Test",
"TSUBASAUT": "TSUBASA Utility Token",
"TSUGT": "Captain Tsubasa",
"TSUJI": "Tsutsuji",
@@ -14161,6 +14758,7 @@
"TX20": "Trex20",
"TXA": "TXA",
"TXAG": "tSILVER",
+ "TXAI": "TrumpX Ai",
"TXAU": "tGOLD",
"TXBIT": "Txbit Token",
"TXC": "TEXITcoin",
@@ -14192,7 +14790,8 @@
"UAT": "UltrAlpha",
"UB": "UBit Token",
"UBA": "Unbox.Art",
- "UBC": "Ubcoin",
+ "UBC": "Universal Basic Compute",
+ "UBCOIN": "Ubcoin",
"UBDN": "UBD Network",
"UBEX": "Ubex",
"UBI": "Universal Basic Income",
@@ -14234,6 +14833,7 @@
"UETL": "Useless Eth Token Lite",
"UFARM": "UniFarm",
"UFC": "Union Fair Coin",
+ "UFD": "Unicorn Fart Dust",
"UFFYI": "Unlimited FiscusFYI",
"UFI": "PureFi",
"UFO": "UFO Gaming",
@@ -14306,19 +14906,26 @@
"UNICORN": "UNICORN Token",
"UNIDEXAI": "UniDexAI",
"UNIDX": "UniDex",
+ "UNIDXV1": "UniDex v1",
"UNIE": "Uniswap Protocol Token (Avalanche Bridge)",
"UNIETH": "Universal ETH",
+ "UNIFI": "Unifi",
"UNIFY": "Unify",
+ "UNIL": "UniLayer",
"UNIM": "Unicorn Milk",
"UNIO": "Unio Coin",
"UNIQ": "Uniqredit",
"UNIQUE": "Unique One",
+ "UNISD": "unified Stable Dollar",
+ "UNISDV1": "uniswap State Dollar",
"UNISTAKE": "Unistake",
"UNIT": "Universal Currency",
"UNIT0": "UNIT0",
"UNITARYSTATUS": "UnitaryStatus Dollar",
"UNITED": "UnitedCoins",
"UNITRADE": "UniTrade",
+ "UNITREEAI": "Unitree G1 AI",
+ "UNITREEDOG": "Unitree AI Robot Dog",
"UNITS": "GameUnits",
"UNITY": "SuperNET",
"UNIVRS": "Universe",
@@ -14376,6 +14983,7 @@
"USCC": "USC",
"USCOIN": "USCoin",
"USD0": "Usual",
+ "USD1": "USD1",
"USD3": "Web 3 Dollar",
"USDA": "USDA",
"USDAP": "Bond Appetite USD",
@@ -14393,6 +15001,7 @@
"USDCPO": "USD Coin (PoS) (Portal from Polygon)",
"USDCSO": "USD Coin (Portal from Solana)",
"USDD": "USDD",
+ "USDDV1": "USDD v1",
"USDE": "Ethena USDe",
"USDEBT": "USDEBT",
"USDEX": "eToro US Dollar",
@@ -14405,21 +15014,24 @@
"USDL": "Lift Dollar",
"USDM": "Mountain Protocol",
"USDMA": "USD mars",
- "USDN": "Neutrino USD",
+ "USDN": "Neutral AI",
"USDO": "USD Open Dollar",
"USDP": "Pax Dollar",
"USDPLUS": "Overnight.fi USD+",
- "USDQ": "USDQ",
- "USDR": "Real USD",
+ "USDQ": "Quantoz USDQ",
+ "USDQSTABLE": "USDQ",
+ "USDR": "StablR USD",
"USDS": "Sky Dollar",
"USDSB": "USDSB",
"USDSTABLY": "StableUSD",
"USDT": "Tether",
+ "USDT0": "USDT0",
"USDTBASE": "USDT (Base)",
"USDTV": "TetherTV",
"USDTZ": "USDtez",
"USDU": "Upper Dollar",
"USDV": "Verified USD",
+ "USDW": "USD DWIN",
"USDX": "USDX Stablecoin",
"USDY": "Ondo US Dollar Yield",
"USDZ": "Zedxion USDZ",
@@ -14445,8 +15057,10 @@
"USTBL": "Spiko US T-Bills Money Market Fund",
"USTC": "TerraClassicUSD",
"USTCW": "TerraClassicUSD Wormhole",
+ "USTREAM": "Ustream Coin",
"USTX": "UpStableToken",
"USUAL": "Usual",
+ "USUALX": "USUALx",
"USV": "Universal Store of Value",
"USX": "USX Quantum",
"USYC": "Hashnote USYC",
@@ -14455,6 +15069,7 @@
"UTC": "UltraCoin",
"UTG": "UltronGlow",
"UTH": "Uther",
+ "UTHR": "Utherverse Xaeon",
"UTHX": "Utherverse",
"UTI": "Unicorn Technology International",
"UTIL": "Utility Coin",
@@ -14466,7 +15081,9 @@
"UTT": "United Traders Token",
"UTU": "UTU Protocol",
"UTX": "UTIX",
+ "UTYA": "Utya",
"UTYAB": "Utya Black",
+ "UUC": "USA Unity Coin",
"UUSD": "Utopia USD",
"UUU": "U Network",
"UVT": "UvToken",
@@ -14499,6 +15116,7 @@
"VANA": "Vana",
"VANCAT": "Vancat",
"VANCE": "JD Vance",
+ "VANCEMEME": "Vance Meme",
"VANF": "Van Fwogh",
"VANRY": "Vanar Chain",
"VANT": "Vanta Network",
@@ -14508,6 +15126,7 @@
"VARA": "Vara Network",
"VARIUS": "Varius",
"VARK": "Aardvark",
+ "VATAN": "Vatan Token",
"VATO": "vanitis",
"VATR": "Vatra INU",
"VATRENI": "Croatian FF Fan Token",
@@ -14548,6 +15167,7 @@
"VEC": "VECTOR",
"VEC2": "VectorCoin 2.0",
"VECT": "Vectorium",
+ "VECTOR": "VectorChat.ai",
"VEE": "BLOCKv",
"VEED": "VEED",
"VEEN": "LIVEEN",
@@ -14606,13 +15226,15 @@
"VGX": "Voyager Token",
"VHC": "Vault Hill City",
"VI": "Vid",
- "VIA": "ViaCoin",
+ "VIA": "Octavia AI",
+ "VIAC": "ViaCoin",
"VIB": "Viberate",
"VIBE": "VIBEHub",
"VIBEA": "Vibe AI",
"VIBLO": "VIBLO",
"VIC": "Viction",
"VICA": "ViCA Token",
+ "VICE": "VICE Token",
"VICEX": "ViceToken",
"VICS": "RoboF",
"VICT": "Victory Impact Coin",
@@ -14621,6 +15243,7 @@
"VIDA": "Vidiachange",
"VIDEO": "Videocoin by Drakula",
"VIDT": "VIDT Datalink",
+ "VIDTV1": "VIDT Datalink",
"VIDY": "Vidy",
"VIDYA": "Vidya",
"VIDYX": "VidyX",
@@ -14630,9 +15253,11 @@
"VIK": "VIKTAMA",
"VIKITA": "VIKITA",
"VIKKY": "VikkyToken",
+ "VILADY": "Vitalik Milady",
"VIM": "VicMove",
"VIN": "VinChain",
"VINCI": "VINCI",
+ "VINE": "Vine Coin",
"VINU": "Vita Inu",
"VIOR": "ViorCoin",
"VIP": "VIP Tokens",
@@ -14656,6 +15281,7 @@
"VITAFAST": "Molecules of Korolchuk IP-NFT",
"VITAL": "Vital Network",
"VITALI": "Vitalik's Casper",
+ "VITALIK": "OFFICIAL VITALIK",
"VITAMINS": "Vitamins",
"VITARNA": "VitaRNA",
"VITE": "VITE",
@@ -14708,6 +15334,7 @@
"VOCO": "Provoco",
"VODCAT": "VODKA CAT",
"VODKA": "Vodka Token",
+ "VOID": "Nothing",
"VOIP": "Voip Finance",
"VOISE": "Voise",
"VOL": "Volume Network",
@@ -14719,6 +15346,7 @@
"VOLTOLD": "Volt Inu (Old)",
"VOLTV1": "Volt Inu v1",
"VOLTV2": "Volt Inu v2",
+ "VOLTX": "VolatilityX",
"VOLTZ": "Voltz",
"VOLX": "VolumeX",
"VONE": "Vone",
@@ -14799,6 +15427,7 @@
"VVAIFU": "Dasha",
"VVI": "VV Coin",
"VVS": "VVS Finance",
+ "VVV": "Venice Token",
"VX": "ViteX Coin",
"VXL": "Voxel X Network",
"VXR": "Vox Royale",
@@ -14810,6 +15439,8 @@
"VYFI": "VyFinance",
"VYNC": "VYNK Chain",
"VYPER": "VYPER.WIN",
+ "VYVO": "Vyvo AI",
+ "VZ": "Vault Zero",
"VZT": "Vezt",
"W": "Wormhole",
"W1": "W1",
@@ -14835,6 +15466,7 @@
"WAGG": "Waggle Network",
"WAGIE": "Wagie",
"WAGIEBOT": "Wagie Bot",
+ "WAGM": "WAGMI",
"WAGMI": "Wagmi Coin",
"WAGMIGAMES": "WAGMI Game",
"WAGMIT": "Wagmi",
@@ -14889,11 +15521,13 @@
"WAXS": "Axie Infinity Shards (Wormhole)",
"WAY": "WayCoin",
"WAZ": "MikeAI",
+ "WBAI": "Wrapped Balance AI",
"WBAN": "Wrapped Banano",
"WBB": "Wild Beast Coin",
"WBBC": "Wibcoin",
"WBC": "WorldBrain Coin",
"WBCH": "Wrapped Bitcoin Cash",
+ "WBERA": "Wrapped Bera",
"WBESC": "Wrapped BESC",
"WBET": "Wavesbet",
"WBETH": "Wrapped Beacon ETH",
@@ -14980,6 +15614,7 @@
"WENLAMBO": "Wenlambo",
"WEOS": "Wrapped EOS",
"WEPC": "World Earn & Play Community",
+ "WEPE": "Wall Street Pepe",
"WERK": "Werk Family",
"WEST": "Waves Enterprise",
"WET": "WeShow Token",
@@ -15035,6 +15670,7 @@
"WHITE": "Whiteheart",
"WHL": "WhaleCoin",
"WHO": "Truwho",
+ "WHOLE": "Whole Network",
"WHOREN": "elizabath whoren",
"WHT": "Wrapped Huobi Token",
"WHTETGRMOON": "WHITE TIGER MOON",
@@ -15082,7 +15718,9 @@
"WINT": "WinToken",
"WINTER": "Winter",
"WINU": "Walter Inu",
+ "WINX": "WinX.io",
"WIOTX": "Wrapped IoTeX",
+ "WIRE": "717ai by Virtuals",
"WIRTUAL": "Wirtual",
"WIS": "Experty Wisdom Token",
"WISC": "WisdomCoin",
@@ -15104,6 +15742,7 @@
"WKAVA": "Wrapped Kava",
"WKC": "Wiki Cat",
"WKD": "Wakanda Inu",
+ "WKEYDAO": "WebKey DAO",
"WLD": "Worldcoin",
"WLF": "Wolfs Group",
"WLFI": "World Liberty Financial",
@@ -15111,13 +15750,15 @@
"WLK": "Wolk",
"WLKN": "Walken",
"WLO": "WOLLO",
+ "WLSC": "WESTLAND SMART CITY",
"WLTH": "Common Wealth",
"WLUNA": "Wrapped LUNA Token",
"WLUNC": "Wrapped LUNA Classic",
"WLXT": "Wallex Token",
"WMATIC": "Wrapped Matic",
"WMB": "WatermelonBlock",
- "WMC": "WMCoin",
+ "WMC": "Wrapped MistCoin",
+ "WMCOIN": "WMCoin",
"WMEMO": "Wonderful Memories",
"WMETIS": "Wrapped Metis",
"WMF": "Whale Maker Fund",
@@ -15156,9 +15797,10 @@
"WOJAK2": "Wojak 2.0 Coin",
"WOJAKC": "Wojak Coin",
"WOKB": "Wrapped OKB",
+ "WOKIE": "Wokie Plumpkin by Virtuals",
"WOKT": "Wrapped OKT",
"WOL": "World of Legends",
- "WOLF": "LANDWOLF (AVAX)",
+ "WOLF": "Landwolf 0x67",
"WOLFILAND": "Wolfiland",
"WOLFOF": "Wolf of Wall Street",
"WOLFP": "Wolfpack Coin",
@@ -15194,6 +15836,7 @@
"WORX": "Worx",
"WOS": "Wolf Of Solana",
"WOT": "World Of Trump",
+ "WOULD": "would",
"WOW": "WOWswap",
"WOWS": "Wolves of Wall Street",
"WOZX": "Efforce",
@@ -15210,6 +15853,7 @@
"WPR": "WePower",
"WQT": "Work Quest",
"WRC": "Worldcore",
+ "WREACT": "Wrapped REACT",
"WRK": "BlockWRK",
"WRKX": "NFT Workx",
"WRLD": "NFT Worlds",
@@ -15241,6 +15885,7 @@
"WSTORV1": "StorageChain v1",
"WSTR": "Wrapped Star",
"WSTUSDT": "wstUSDT",
+ "WSTUSR": "Resolv wstUSR",
"WSX": "WeAreSatoshi",
"WT": "WeToken",
"WTAO": "Wrapped TAO",
@@ -15260,6 +15905,7 @@
"WUF": "WUFFI",
"WUK": "WUKONG",
"WUKONG": "Sun Wukong",
+ "WULFY": "Wulfy",
"WUSD": "Worldwide USD",
"WUST": "Wrapped UST Token",
"WVG0": "Wrapped Virgin Gen-0 CryptoKittties",
@@ -15309,6 +15955,7 @@
"XAGX": "Silver Token",
"XAH": "Xahau",
"XAI": "Xai",
+ "XAIGAME": "xAI Game Studio",
"XALGO": "Wrapped ALGO",
"XALPHA": "XAlpha AI",
"XAMP": "Antiample",
@@ -15439,6 +16086,7 @@
"XHP": "XHYPE",
"XHPV1": "XHYPE v1",
"XHT": "HollaEx",
+ "XHUNT": "CryptoHunter World",
"XHV": "Haven Protocol",
"XI": "Xi",
"XIASI": "Xiasi Inu",
@@ -15513,6 +16161,7 @@
"XOV": "XOVBank",
"XOX": "XOX Labs",
"XOXNO": "XOXNO",
+ "XOXO": "XO Protocol",
"XP": "Experience Points",
"XPA": "XPA",
"XPAT": "Bitnation Pangea",
@@ -15524,6 +16173,7 @@
"XPET": "XPET token",
"XPH": "PharmaCoin",
"XPHX": "PhoenixCo Token",
+ "XPI": "XPi",
"XPL": "Exclusive Platform",
"XPLA": "XPLA",
"XPLL": "ParallelChain",
@@ -15604,6 +16254,7 @@
"XTECH": "X-TECH",
"XTK": "xToken",
"XTM": "TORUM",
+ "XTN": "Neutrino Index Token",
"XTO": "Tao",
"XTP": "Tap",
"XTR": "Xtreme",
@@ -15680,14 +16331,18 @@
"YDF": "Yieldification",
"YDOGE": "Yorkie Doge",
"YDR": "YDragon",
+ "YE": "Kanye West",
+ "YEAI": "YE AI Agent",
"YEARN": "YearnTogether",
"YEC": "Ycash",
"YEE": "Yeeco",
"YEED": "Yggdrash",
"YEEHAW": "YEEHAW",
+ "YEET": "Yeet",
"YEETI": "YEETI 液体",
"YEFI": "YeFi",
"YEL": "Yel.Finance",
+ "YELP": "Yelpro",
"YEON": "Yeon",
"YEPE": "Yellow Pepe",
"YES": "YES Money",
@@ -15695,6 +16350,7 @@
"YESP": "Yesports",
"YESW": "Yes World",
"YETI": "Yeti Finance",
+ "YETIUSD": "YUSD Stablecoin",
"YETU": "Yetucoin",
"YFARM": "YFARM Token",
"YFBETA": "yfBeta",
@@ -15721,6 +16377,8 @@
"YIELD": "Yield Protocol",
"YIELDX": "Yield Finance",
"YIKES": "Yikes Dog",
+ "YILONG": "Yi Long Ma",
+ "YILONGMA": "Chinese Elon Musk",
"YIN": "YIN Finance",
"YINBI": "Yinbi",
"YLAY": "Yelay",
@@ -15729,12 +16387,14 @@
"YLDY": "Yieldly",
"YMC": "YamahaCoin",
"YMS": "Yeni Malatyaspor Token",
+ "YNE": "yesnoerror",
"YNETH": "YieldNest Restaked ETH",
"YO": "Yobit Token",
"YOBASE": "All Your Base",
"YOC": "YoCoin",
"YOCO": "YocoinYOCO",
"YOD": "Year of the Dragon",
+ "YODA": "YODA",
"YODE": "YodeSwap",
"YOLO": "YoloNolo",
"YOM": "YOM",
@@ -15761,6 +16421,7 @@
"YSEC": "Yearn Secure",
"YSR": "Ystar",
"YTA": "YottaChain",
+ "YTC": "Yachtscoin",
"YTJIA": "Jia Yueting",
"YTN": "YENTEN",
"YTS": "YetiSwap",
@@ -15775,12 +16436,13 @@
"YUKIE": "Yukie",
"YUKKY": "YUKKY",
"YUKO": "YUKO",
+ "YULI": "Yuliverse",
"YUM": "Yumerium",
"YUMMI": "Yummi Universe",
"YUMMY": "Yummy",
"YUP": "Crowdholding",
"YURI": "YURI",
- "YUSD": "YUSD Stablecoin",
+ "YUSD": "YieldFi yToken",
"YUSE": "Yuse Token",
"YUSRA": "YUSRA",
"YUSUF": "Yusuf Dikec Meme",
@@ -15792,6 +16454,7 @@
"YYE": "YYE Energy",
"YYFI": "YYFI.Protocol",
"YYOLO": "yYOLO",
+ "YZY": "Yeezy",
"Z3": "Z-Cubed",
"ZABAKU": "Zabaku Inu",
"ZACK": "Zack Morris",
@@ -15817,6 +16480,7 @@
"ZBCN": "Zebec Network",
"ZBIT": "zbit",
"ZBU": "Zeebu",
+ "ZBUV1": "ZEEBU v1",
"ZCC": "ZCC Coin",
"ZCC1": "ZeroCarbon",
"ZCD": "ZChains",
@@ -15890,6 +16554,7 @@
"ZGEM": "GemSwap",
"ZHC": "ZHC : Zero Hour Cash",
"ZHOA": "Chengpang Zhoa",
+ "ZHOUKING": "ZhouKing",
"ZIBU": "Zibu",
"ZIG": "Zignaly",
"ZIGAP": "ZIGAP",
@@ -15920,6 +16585,7 @@
"ZKDX": "ZKDX",
"ZKE": "zkEra Finance",
"ZKEVM": "zkEVMChain (BSC)",
+ "ZKEX": "zkExchange",
"ZKF": "ZKFair",
"ZKGROK": "ZKGROK",
"ZKGUN": "zkGUN",
diff --git a/apps/api/src/assets/sitemap.xml b/apps/api/src/assets/sitemap.xml
index 5a49f6710..fc1e89dba 100644
--- a/apps/api/src/assets/sitemap.xml
+++ b/apps/api/src/assets/sitemap.xml
@@ -92,6 +92,10 @@
https://ghostfol.io/de/ueber-uns/oss-friends
${currentDate}T00:00:00+00:00
+
+ https://ghostfol.io/development/storybook
+ ${currentDate}T00:00:00+00:00
+
https://ghostfol.io/en
${currentDate}T00:00:00+00:00
@@ -504,12 +508,10 @@
https://ghostfol.io/pl/o-ghostfolio
${currentDate}T00:00:00+00:00
-
+
+ https://ghostfol.io/pl/open
+ ${currentDate}T00:00:00+00:00
+
https://ghostfol.io/pl/rynki
${currentDate}T00:00:00+00:00
@@ -583,6 +585,12 @@
${currentDate}T00:00:00+00:00
+
+
- Dutch, French, German, Italian,
-
- Portuguese, Spanish and Turkish are currently supported.
+ Dutch, French, German, Italian, Polish, Portuguese, Spanish
+ and Turkish
+
+ are currently supported.
diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
index 91254e002..5f5f7cea9 100644
--- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
@@ -125,7 +125,10 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
this.dataSource = new MatTableDataSource(activities);
this.totalItems = count;
- if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
+ if (
+ this.hasPermissionToCreateActivity &&
+ this.user?.activitiesCount === 0
+ ) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
@@ -160,6 +163,11 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe();
+
this.fetchActivities();
});
}
@@ -169,6 +177,11 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
.deleteActivity(aId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe();
+
this.fetchActivities();
});
}
@@ -230,6 +243,11 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe();
+
this.fetchActivities();
});
}
@@ -248,6 +266,11 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe();
+
this.fetchActivities();
});
}
@@ -333,6 +356,11 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
if (transaction) {
this.dataService.postOrder(transaction).subscribe({
next: () => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe();
+
this.fetchActivities();
}
});
diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html
index c06f7dd75..80ad71b79 100644
--- a/apps/client/src/app/pages/portfolio/activities/activities-page.html
+++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html
@@ -6,6 +6,7 @@
[baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="deviceType"
+ [hasActivities]="user?.activitiesCount > 0"
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
[hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity"
[hasPermissionToExportActivities]="!hasImpersonationId"
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
index 271a5cd53..555fbc7aa 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
@@ -3,24 +3,20 @@ import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n';
-import { COMMA, ENTER } from '@angular/cdk/keycodes';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- ElementRef,
Inject,
- OnDestroy,
- ViewChild
+ OnDestroy
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
import { isAfter, isToday } from 'date-fns';
-import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs';
-import { catchError, delay, map, startWith, takeUntil } from 'rxjs/operators';
+import { EMPTY, Subject, lastValueFrom } from 'rxjs';
+import { catchError, delay, takeUntil } from 'rxjs/operators';
import { DataService } from '../../../../services/data.service';
import { validateObjectForForm } from '../../../../util/form.util';
@@ -35,9 +31,6 @@ import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
standalone: false
})
export class CreateOrUpdateActivityDialog implements OnDestroy {
- @ViewChild('symbolAutocomplete') symbolAutocomplete;
- @ViewChild('tagInput') tagInput: ElementRef;
-
public activityForm: FormGroup;
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { id: assetClass, label: translate(assetClass) };
@@ -48,12 +41,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
public currencies: string[] = [];
public currentMarketPrice = null;
public defaultDateFormat: string;
- public filteredTagsObservable: Observable = of([]);
public isLoading = false;
public isToday = isToday;
public mode: 'create' | 'update';
public platforms: { id: string; name: string }[];
- public separatorKeysCodes: number[] = [COMMA, ENTER];
public tagsAvailable: Tag[] = [];
public total = 0;
public typesTranslationMap = new Map();
@@ -284,15 +275,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.changeDetectorRef.markForCheck();
});
- this.filteredTagsObservable = this.activityForm.controls[
- 'tags'
- ].valueChanges.pipe(
- startWith(this.activityForm.get('tags').value),
- map((aTags: Tag[] | null) => {
- return aTags ? this.filterTags(aTags) : this.tagsAvailable.slice();
- })
- );
-
this.activityForm
.get('type')
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
@@ -440,29 +422,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
return isAfter(aDate, new Date(0));
}
- public onAddTag(event: MatAutocompleteSelectedEvent) {
- this.activityForm.get('tags').setValue([
- ...(this.activityForm.get('tags').value ?? []),
- this.tagsAvailable.find(({ id }) => {
- return id === event.option.value;
- })
- ]);
-
- this.tagInput.nativeElement.value = '';
- }
-
public onCancel() {
this.dialogRef.close();
}
- public onRemoveTag(aTag: Tag) {
- this.activityForm.get('tags').setValue(
- this.activityForm.get('tags').value.filter(({ id }) => {
- return id !== aTag.id;
- })
- );
- }
-
public async onSubmit() {
const activity: CreateOrderDto | UpdateOrderDto = {
accountId: this.activityForm.get('accountId').value,
@@ -518,21 +481,15 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
}
}
+ public onTagsChanged(tags: Tag[]) {
+ this.activityForm.get('tags').setValue(tags);
+ }
+
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
- private filterTags(aTags: Tag[]) {
- const tagIds = aTags.map(({ id }) => {
- return id;
- });
-
- return this.tagsAvailable.filter(({ id }) => {
- return !tagIds.includes(id);
- });
- }
-
private updateSymbol() {
this.isLoading = true;
this.changeDetectorRef.markForCheck();
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
index 7795688c0..85fcf5a94 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
@@ -379,38 +379,11 @@
-
- Tags
-
- @for (tag of activityForm.get('tags')?.value; track tag.id) {
-
- {{ tag.name }}
-
-
- }
-
-
-
- @for (tag of filteredTagsObservable | async; track tag.id) {
-
- {{ tag.name }}
-
- }
-
-
+
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
index a4d28d0e0..8fb2c1bed 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
@@ -1,14 +1,13 @@
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
+import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
-import { MatChipsModule } from '@angular/material/chips';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -24,11 +23,10 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
FormsModule,
GfAssetProfileIconComponent,
GfSymbolAutocompleteComponent,
+ GfTagsSelectorComponent,
GfValueComponent,
- MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
- MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
index 2f5ead47a..82e78a180 100644
--- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
@@ -23,6 +23,7 @@ import { MatStepper } from '@angular/material/stepper';
import { MatTableDataSource } from '@angular/material/table';
import { AssetClass } from '@prisma/client';
import { isArray, sortBy } from 'lodash';
+import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, takeUntil } from 'rxjs';
@@ -133,7 +134,7 @@ export class ImportActivitiesDialog implements OnDestroy {
'✅ ' + $localize`Import has been completed`,
undefined,
{
- duration: 3000
+ duration: ms('3 seconds')
}
);
} catch (error) {
@@ -142,7 +143,9 @@ export class ImportActivitiesDialog implements OnDestroy {
' ' +
$localize`Please try again later.`,
$localize`Okay`,
- { duration: 3000 }
+ {
+ duration: ms('3 seconds')
+ }
);
} finally {
this.dialogRef.close();
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
index 41961edd3..54ea88548 100644
--- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
@@ -260,6 +260,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.platforms = {};
this.portfolioDetails = {
accounts: {},
+ createdAt: undefined,
holdings: {},
platforms: {},
summary: undefined
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
index 7e27a05f9..2bd3096d4 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
@@ -12,14 +12,22 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
-import { GroupBy } from '@ghostfolio/common/types';
+import type { AiPromptMode, GroupBy } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
import { Clipboard } from '@angular/cdk/clipboard';
-import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import {
+ ChangeDetectorRef,
+ Component,
+ OnDestroy,
+ OnInit,
+ ViewChild
+} from '@angular/core';
+import { MatMenuTrigger } from '@angular/material/menu';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SymbolProfile } from '@prisma/client';
import { isNumber, sortBy } from 'lodash';
+import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -31,6 +39,8 @@ import { takeUntil } from 'rxjs/operators';
standalone: false
})
export class AnalysisPageComponent implements OnDestroy, OnInit {
+ @ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger;
+
public benchmark: Partial
;
public benchmarkDataItems: HistoricalDataItem[] = [];
public benchmarks: Partial[];
@@ -45,10 +55,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public investments: InvestmentItem[];
public investmentTimelineDataLabel = $localize`Investment`;
public investmentsByGroup: InvestmentItem[];
+ public isLoadingAnalysisPrompt: boolean;
public isLoadingBenchmarkComparator: boolean;
public isLoadingDividendTimelineChart: boolean;
public isLoadingInvestmentChart: boolean;
public isLoadingInvestmentTimelineChart: boolean;
+ public isLoadingPortfolioPrompt: boolean;
public mode: GroupBy = 'month';
public modeOptions: ToggleOption[] = [
{ label: $localize`Monthly`, value: 'month' },
@@ -141,18 +153,45 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.fetchDividendsAndInvestments();
}
- public onCopyPromptToClipboard() {
- this.dataService.fetchPrompt().subscribe(({ prompt }) => {
- this.clipboard.copy(prompt);
+ public onCopyPromptToClipboard(mode: AiPromptMode) {
+ if (mode === 'analysis') {
+ this.isLoadingAnalysisPrompt = true;
+ } else if (mode === 'portfolio') {
+ this.isLoadingPortfolioPrompt = true;
+ }
+
+ this.dataService
+ .fetchPrompt({
+ mode,
+ filters: this.userService.getFilters()
+ })
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(({ prompt }) => {
+ this.clipboard.copy(prompt);
- this.snackBar.open(
- '✅ ' + $localize`AI prompt has been copied to the clipboard`,
- undefined,
- {
- duration: 3000
+ const snackBarRef = this.snackBar.open(
+ '✅ ' + $localize`AI prompt has been copied to the clipboard`,
+ $localize`Open Duck.ai` + ' →',
+ {
+ duration: ms('7 seconds')
+ }
+ );
+
+ snackBarRef
+ .onAction()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(() => {
+ window.open('https://duck.ai', '_blank');
+ });
+
+ this.actionsMenuButton.closeMenu();
+
+ if (mode === 'analysis') {
+ this.isLoadingAnalysisPrompt = false;
+ } else if (mode === 'portfolio') {
+ this.isLoadingPortfolioPrompt = false;
}
- );
- });
+ });
}
public ngOnDestroy() {
@@ -310,6 +349,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
.fetchBenchmarkForUser({
dataSource,
symbol,
+ filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange,
startDate: this.firstOrderDate
})
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
index 07ffa705d..56c95e40f 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
@@ -5,6 +5,7 @@
-
-
+
+
+
+
+
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
index fb39b2ab9..e02e15ec8 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
@@ -10,6 +10,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
@@ -29,6 +30,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
MatButtonModule,
MatCardModule,
MatMenuModule,
+ MatProgressSpinnerModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
index ceba5f52c..24ca0f474 100644
--- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
@@ -7,23 +7,58 @@
risks in your portfolio. Adjust the rules below and set custom
thresholds to align with your personal investment strategy.
-
+
@if (isLoading) {
-
+
+
+
+
} @else {
- {{ statistics?.rulesFulfilledCount }}
-
out of
- {{ statistics?.rulesActiveCount }}
-
rules align with your portfolio.
+
+ @if (
+ statistics?.rulesActiveCount === 0 ||
+ statistics?.rulesFulfilledCount === 0
+ ) {
+
+ } @else if (
+ statistics?.rulesFulfilledCount === statistics?.rulesActiveCount
+ ) {
+
+ } @else {
+
+ }
+
+
+ {{ statistics?.rulesFulfilledCount }}
+ out of
+ {{ statistics?.rulesActiveCount }}
+ rules align with your portfolio.
+
}
-
+
+
+
+ Regional Market Cluster Risks
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
{
+ return isActive;
+ }) ?? null;
+
this.isLoading = false;
this.changeDetectorRef.markForCheck();
diff --git a/apps/client/src/app/pages/register/register-page.component.ts b/apps/client/src/app/pages/register/register-page.component.ts
index 86490688b..66a5f4beb 100644
--- a/apps/client/src/app/pages/register/register-page.component.ts
+++ b/apps/client/src/app/pages/register/register-page.component.ts
@@ -7,7 +7,6 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
-import { Role } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -59,43 +58,28 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
);
}
- public async createAccount() {
- this.dataService
- .postUser()
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe(({ accessToken, authToken, role }) => {
- this.openShowAccessTokenDialog(accessToken, authToken, role);
- });
- }
-
public async onLoginWithInternetIdentity() {
try {
const { authToken } = await this.internetIdentityService.login();
+
this.tokenStorageService.saveToken(authToken);
- this.router.navigate(['/']);
+
+ await this.router.navigate(['/']);
} catch {}
}
- public openShowAccessTokenDialog(
- accessToken: string,
- authToken: string,
- role: Role
- ) {
+ public openShowAccessTokenDialog() {
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
- data: {
- accessToken,
- authToken,
- role
- },
disableClose: true,
- width: '30rem'
+ height: this.deviceType === 'mobile' ? '98vh' : undefined,
+ width: this.deviceType === 'mobile' ? '100vw' : '30rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
- .subscribe((data) => {
- if (data?.authToken) {
+ .subscribe((authToken) => {
+ if (authToken) {
this.tokenStorageService.saveToken(authToken, true);
this.router.navigate(['/']);
diff --git a/apps/client/src/app/pages/register/register-page.html b/apps/client/src/app/pages/register/register-page.html
index 903aedccf..eee49083a 100644
--- a/apps/client/src/app/pages/register/register-page.html
+++ b/apps/client/src/app/pages/register/register-page.html
@@ -22,7 +22,7 @@
class="d-inline-block"
color="primary"
mat-flat-button
- (click)="createAccount()"
+ (click)="openShowAccessTokenDialog()"
>
Create Account
diff --git a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.component.ts b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.component.ts
index 5aacbd457..c6535bf48 100644
--- a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.component.ts
+++ b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.component.ts
@@ -1,19 +1,58 @@
-import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
-import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { DataService } from '@ghostfolio/client/services/data.service';
+
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ViewChild
+} from '@angular/core';
+import { MatStepper } from '@angular/material/stepper';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
@Component({
- selector: 'gf-show-access-token-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'gf-show-access-token-dialog',
+ standalone: false,
styleUrls: ['./show-access-token-dialog.scss'],
- templateUrl: 'show-access-token-dialog.html',
- standalone: false
+ templateUrl: 'show-access-token-dialog.html'
})
export class ShowAccessTokenDialog {
- public isAgreeButtonDisabled = true;
+ @ViewChild(MatStepper) stepper!: MatStepper;
+
+ public accessToken: string;
+ public authToken: string;
+ public isCreateAccountButtonDisabled = true;
+ public isDisclaimerChecked = false;
+ public role: string;
+
+ private unsubscribeSubject = new Subject
();
+
+ public constructor(
+ private changeDetectorRef: ChangeDetectorRef,
+ private dataService: DataService
+ ) {}
- public constructor(@Inject(MAT_DIALOG_DATA) public data: any) {}
+ public createAccount() {
+ this.dataService
+ .postUser()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(({ accessToken, authToken, role }) => {
+ this.accessToken = accessToken;
+ this.authToken = authToken;
+ this.role = role;
+
+ this.stepper.next();
+
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+
+ public enableCreateAccountButton() {
+ this.isCreateAccountButtonDisabled = false;
+ }
- public enableAgreeButton() {
- this.isAgreeButtonDisabled = false;
+ public onChangeDislaimerChecked() {
+ this.isDisclaimerChecked = !this.isDisclaimerChecked;
}
}
diff --git a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
index 0a6a2e5e3..737d32679 100644
--- a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+++ b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
@@ -1,48 +1,93 @@
Create Account
- @if (data.role === 'ADMIN') {
- {{ data.role }}
+ @if (role === 'ADMIN') {
+ {{ role }}
}
-
-
-
- Security Token
-
-
-
-
-
Cancel
-
- Agree and continue
-
-
+
+
+ Security Token
+
+ Here is your security token. It is only visible once, please store
+ and keep it in a safe place.
+
+
+ Security Token
+
+
+
+
+ Copy to clipboard
+
+
+
+
+
+
+ Create Account
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.module.ts b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.module.ts
index d537fbe7f..117c1da00 100644
--- a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.module.ts
+++ b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.module.ts
@@ -4,9 +4,11 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
+import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
+import { MatStepperModule } from '@angular/material/stepper';
import { ShowAccessTokenDialog } from './show-access-token-dialog.component';
@@ -17,9 +19,11 @@ import { ShowAccessTokenDialog } from './show-access-token-dialog.component';
CommonModule,
FormsModule,
MatButtonModule,
+ MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
+ MatStepperModule,
ReactiveFormsModule,
TextFieldModule
],
diff --git a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.scss b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.scss
index dc9093b45..777c8c854 100644
--- a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.scss
+++ b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.scss
@@ -1,2 +1,6 @@
:host {
+ .mat-mdc-dialog-actions {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ }
}
diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
index 3a0ec4ffb..6a8543e71 100644
--- a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
+++ b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
@@ -3,14 +3,13 @@ import { Product } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { translate } from '@ghostfolio/ui/i18n';
-import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router';
@Component({
host: { class: 'page' },
- imports: [CommonModule, MatButtonModule, RouterModule],
+ imports: [MatButtonModule, RouterModule],
selector: 'gf-product-page',
styleUrls: ['./product-page.scss'],
templateUrl: './product-page.html'
diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts
index ec0605bee..fea3924e9 100644
--- a/apps/client/src/app/services/admin.service.ts
+++ b/apps/client/src/app/services/admin.service.ts
@@ -1,8 +1,6 @@
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
-import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
-import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import {
HEADER_KEY_SKIP_INTERCEPTOR,
@@ -10,7 +8,6 @@ import {
PROPERTY_API_KEY_GHOSTFOLIO
} from '@ghostfolio/common/config';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
-import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
AdminData,
@@ -25,9 +22,8 @@ import {
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
-import { DataSource, MarketData, Platform, Tag } from '@prisma/client';
+import { DataSource, MarketData, Platform } from '@prisma/client';
import { JobStatus } from 'bull';
-import { format } from 'date-fns';
import { switchMap } from 'rxjs';
import { environment } from '../../environments/environment';
@@ -75,10 +71,6 @@ export class AdminService {
);
}
- public deleteTag(aId: string) {
- return this.http.delete(`/api/v1/tag/${aId}`);
- }
-
public executeJob(aId: string) {
return this.http.get(`/api/v1/admin/queue/job/${aId}/execute`);
}
@@ -155,10 +147,6 @@ export class AdminService {
return this.http.get('/api/v1/platform');
}
- public fetchTags() {
- return this.http.get('/api/v1/tag');
- }
-
public fetchUsers({
skip,
take = DEFAULT_PAGE_SIZE
@@ -196,19 +184,8 @@ export class AdminService {
);
}
- public gatherSymbol({
- dataSource,
- date,
- symbol
- }: AssetProfileIdentifier & {
- date?: Date;
- }) {
- let url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
-
- if (date) {
- url = `${url}/${format(date, DATE_FORMAT)}`;
- }
-
+ public gatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
+ const url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
return this.http.post(url, {});
}
@@ -261,10 +238,6 @@ export class AdminService {
return this.http.post(`/api/v1/platform`, aPlatform);
}
- public postTag(aTag: CreateTagDto) {
- return this.http.post(`/api/v1/tag`, aTag);
- }
-
public putPlatform(aPlatform: UpdatePlatformDto) {
return this.http.put(
`/api/v1/platform/${aPlatform.id}`,
@@ -272,10 +245,6 @@ export class AdminService {
);
}
- public putTag(aTag: UpdateTagDto) {
- return this.http.put(`/api/v1/tag/${aTag.id}`, aTag);
- }
-
public testMarketData({
dataSource,
scraperConfiguration,
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index 4a57d5878..4eba3fffa 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -4,6 +4,8 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
+import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
+import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import {
Activities,
@@ -44,12 +46,18 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
-import { AccountWithValue, DateRange, GroupBy } from '@ghostfolio/common/types';
+import type {
+ AccountWithValue,
+ AiPromptMode,
+ DateRange,
+ GroupBy
+} from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
+import { utc } from '@date-fns/utc';
import {
AccountBalance,
DataSource,
@@ -274,7 +282,7 @@ export class DataService {
symbol: string;
}) {
return this.http.get(
- `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT)}`
+ `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}`
);
}
@@ -301,13 +309,17 @@ export class DataService {
}
public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
- return this.http.delete(`/api/v1/benchmark/${dataSource}/${symbol}`);
+ return this.http.delete(`/api/v1/benchmarks/${dataSource}/${symbol}`);
}
public deleteOwnUser(aData: DeleteOwnUserDto) {
return this.http.delete(`/api/v1/user`, { body: aData });
}
+ public deleteTag(aId: string) {
+ return this.http.delete(`/api/v1/tags/${aId}`);
+ }
+
public deleteUser(aId: string) {
return this.http.delete(`/api/v1/user/${aId}`);
}
@@ -332,30 +344,33 @@ export class DataService {
public fetchBenchmarkForUser({
dataSource,
+ filters,
range,
startDate,
- symbol
+ symbol,
+ withExcludedAccounts
}: {
+ filters?: Filter[];
range: DateRange;
startDate: Date;
+ withExcludedAccounts?: boolean;
} & AssetProfileIdentifier): Observable {
- let params = new HttpParams();
+ let params = this.buildFiltersAsQueryParams({ filters });
- if (range) {
- params = params.append('range', range);
+ params = params.append('range', range);
+
+ if (withExcludedAccounts) {
+ params = params.append('withExcludedAccounts', withExcludedAccounts);
}
return this.http.get(
- `/api/v1/benchmark/${dataSource}/${symbol}/${format(
- startDate,
- DATE_FORMAT
- )}`,
+ `/api/v1/benchmarks/${dataSource}/${symbol}/${format(startDate, DATE_FORMAT, { in: utc })}`,
{ params }
);
}
public fetchBenchmarks() {
- return this.http.get('/api/v1/benchmark');
+ return this.http.get('/api/v1/benchmarks');
}
public fetchExport({
@@ -638,8 +653,18 @@ export class DataService {
return this.http.get('/api/v1/portfolio/report');
}
- public fetchPrompt() {
- return this.http.get('/api/v1/ai/prompt');
+ public fetchPrompt({
+ filters,
+ mode
+ }: {
+ filters?: Filter[];
+ mode: AiPromptMode;
+ }) {
+ const params = this.buildFiltersAsQueryParams({ filters });
+
+ return this.http.get(`/api/v1/ai/prompt/${mode}`, {
+ params
+ });
}
public fetchPublicPortfolio(aAccessId: string) {
@@ -662,6 +687,10 @@ export class DataService {
);
}
+ public fetchTags() {
+ return this.http.get('/api/v1/tags');
+ }
+
public loginAnonymous(accessToken: string) {
return this.http.post('/api/v1/auth/anonymous', {
accessToken
@@ -688,7 +717,7 @@ export class DataService {
}
public postBenchmark(benchmark: AssetProfileIdentifier) {
- return this.http.post('/api/v1/benchmark', benchmark);
+ return this.http.post('/api/v1/benchmarks', benchmark);
}
public postMarketData({
@@ -709,6 +738,10 @@ export class DataService {
return this.http.post('/api/v1/order', aOrder);
}
+ public postTag(aTag: CreateTagDto) {
+ return this.http.post(`/api/v1/tags`, aTag);
+ }
+
public postUser() {
return this.http.post('/api/v1/user', {});
}
@@ -736,6 +769,10 @@ export class DataService {
return this.http.put(`/api/v1/order/${aOrder.id}`, aOrder);
}
+ public putTag(aTag: UpdateTagDto) {
+ return this.http.put(`/api/v1/tags/${aTag.id}`, aTag);
+ }
+
public putUserSetting(aData: UpdateUserSettingDto) {
return this.http.put('/api/v1/user/setting', aData);
}
diff --git a/apps/client/src/app/services/web-authn.service.ts b/apps/client/src/app/services/web-authn.service.ts
index c5e186362..76352cb7b 100644
--- a/apps/client/src/app/services/web-authn.service.ts
+++ b/apps/client/src/app/services/web-authn.service.ts
@@ -45,7 +45,7 @@ export class WebAuthnService {
return of(null);
}),
switchMap((attOps) => {
- return startRegistration(attOps);
+ return startRegistration({ optionsJSON: attOps });
}),
switchMap((credential) => {
return this.http.post(
@@ -89,8 +89,8 @@ export class WebAuthnService {
{ deviceId }
)
.pipe(
- switchMap((requestOptionsJSON) => {
- return startAuthentication(requestOptionsJSON);
+ switchMap((optionsJSON) => {
+ return startAuthentication({ optionsJSON });
}),
switchMap((credential) => {
return this.http.post<{ authToken: string }>(
diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf
index 1b7d0cbaf..52a1a1dac 100644
--- a/apps/client/src/locales/messages.ca.xlf
+++ b/apps/client/src/locales/messages.ca.xlf
@@ -52,6 +52,10 @@
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
2
+
+ apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+ 81
+
Personal Finance
@@ -250,7 +254,7 @@
apps/client/src/app/pages/about/overview/about-overview-page.html
- 146
+ 154
@@ -334,19 +338,23 @@
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 105
+ 104
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 110
+ 108
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 114
+ 112
+
+
+ apps/client/src/app/components/user-account-settings/user-account-settings.html
+ 117
apps/client/src/app/pages/features/features-page.html
- 259
+ 260
@@ -354,7 +362,7 @@
El risc d’assumir pèrdues en les inversions és substancial. No és recomanable invertir diners que pugui necessitar a curt termini.
apps/client/src/app/app.component.html
- 200
+ 214
@@ -443,7 +451,7 @@
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 23
+ 22
@@ -575,11 +583,11 @@
apps/client/src/app/pages/pricing/pricing-page.component.ts
- 42
+ 43
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 24
+ 23
@@ -649,11 +657,11 @@
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 43
+ 39
apps/client/src/app/core/http-response.interceptor.ts
- 72
+ 77
apps/client/src/app/core/paths.ts
@@ -734,7 +742,7 @@
apps/client/src/app/pages/pricing/pricing-page.component.ts
- 43
+ 44
@@ -799,7 +807,7 @@
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 26
+ 25
@@ -879,7 +887,7 @@
Realment vol revocar aquest accés?
apps/client/src/app/components/access-table/access-table.component.ts
- 68
+ 79
@@ -1177,10 +1185,6 @@
apps/client/src/app/components/admin-market-data/admin-market-data.html
231
-
- apps/client/src/app/components/admin-overview/admin-overview.html
- 78
-
apps/client/src/app/components/admin-platform/admin-platform.component.html
92
@@ -1211,11 +1215,7 @@
apps/client/src/app/components/admin-overview/admin-overview.html
- 89
-
-
- apps/client/src/app/components/admin-overview/admin-overview.html
- 206
+ 129
apps/client/src/app/components/admin-platform/admin-platform.component.html
@@ -1451,7 +1451,7 @@
Cancel·lar
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 357
+ 432
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html
@@ -1479,11 +1479,11 @@
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
- 427
+ 400
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
- 38
+ 29
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html
@@ -1495,7 +1495,7 @@
Guardar
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 364
+ 439
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html
@@ -1523,7 +1523,7 @@
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
- 434
+ 407
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html
@@ -1719,7 +1719,7 @@
Realment vol eliminar el perfil d’aquest actiu?
apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
- 36
+ 37
@@ -1727,7 +1727,7 @@
Realment vol eliminar aquests perfils?
apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
- 67
+ 68
@@ -1735,7 +1735,7 @@
Oooh! No s’han pogut eliminar els perfils
apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
- 55
+ 56
@@ -1751,7 +1751,7 @@
El preu de mercat actual és
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
- 326
+ 399
@@ -1783,7 +1783,7 @@
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html
- 70
+ 71
@@ -1823,7 +1823,7 @@
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 308
+ 383
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
@@ -1843,7 +1843,7 @@
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 319
+ 394
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
@@ -1871,7 +1871,7 @@
Configuració del Proveïdor de Dades
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 283
+ 295
@@ -1879,7 +1879,7 @@
Prova
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 301
+ 372
@@ -1887,7 +1887,11 @@
Url
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 331
+ 354
+
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+ 406
apps/client/src/app/components/admin-platform/admin-platform.component.html
@@ -1903,7 +1907,7 @@
Notes
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
- 344
+ 419
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
@@ -1950,36 +1954,12 @@
124
-
- Please add a currency:
- Si us plau, afegiu una divisa:
-
- apps/client/src/app/components/admin-overview/admin-overview.component.ts
- 126
-
-
-
- is an invalid currency!
- no és una divisa vàlida!
-
- apps/client/src/app/components/admin-overview/admin-overview.component.ts
- 137
-
-
Do you really want to delete this coupon?
Està segur qeu vol eliminar aquest cupó?
apps/client/src/app/components/admin-overview/admin-overview.component.ts
- 156
-
-
-
- Do you really want to delete this currency?
- Està segur que vol eliminar aquesta divisa?
-
- apps/client/src/app/components/admin-overview/admin-overview.component.ts
- 169
+ 134
@@ -1987,7 +1967,7 @@
Està segur que vol eliminar aquest missatge del sistema?
apps/client/src/app/components/admin-overview/admin-overview.component.ts
- 182
+ 147
@@ -1995,7 +1975,7 @@
Està segur que vol depurar el cache?
apps/client/src/app/components/admin-overview/admin-overview.component.ts
- 206
+ 171
@@ -2003,7 +1983,7 @@
Si us plau, afegeixi el seu missatge del sistema:
apps/client/src/app/components/admin-overview/admin-overview.component.ts
- 226
+ 191
@@ -2038,14 +2018,6 @@
28
-
- Exchange Rates
- Tipus de Canvi
-
- apps/client/src/app/components/admin-overview/admin-overview.html
- 34
-
-
Add Currency
Afegir Divisa
@@ -2053,17 +2025,13 @@
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html
22
-
- apps/client/src/app/components/admin-overview/admin-overview.html
- 105
-
User Signup
Registrar Usuari
apps/client/src/app/components/admin-overview/admin-overview.html
- 111
+ 34
@@ -2071,7 +2039,7 @@
Mode Només Lecutra
apps/client/src/app/components/admin-overview/admin-overview.html
- 125
+ 48
@@ -2079,7 +2047,7 @@
Recollida de Dades
apps/client/src/app/components/admin-overview/admin-overview.html
- 137
+ 60
@@ -2087,7 +2055,7 @@
Missatge del Sistema
apps/client/src/app/components/admin-overview/admin-overview.html
- 149
+ 72
@@ -2095,7 +2063,7 @@
Estableix el Missatge
apps/client/src/app/components/admin-overview/admin-overview.html
- 171
+ 94
@@ -2103,7 +2071,7 @@
Coupons
apps/client/src/app/components/admin-overview/admin-overview.html
- 179
+ 102
@@ -2111,7 +2079,7 @@
Afegir
apps/client/src/app/components/admin-overview/admin-overview.html
- 239
+ 162
libs/ui/src/lib/account-balances/account-balances.component.html
@@ -2123,7 +2091,7 @@
Ordre
apps/client/src/app/components/admin-overview/admin-overview.html
- 247
+ 170
@@ -2131,7 +2099,7 @@
Depurar el Cache
apps/client/src/app/components/admin-overview/admin-overview.html
- 251
+ 174
@@ -2199,7 +2167,7 @@
Plataformes
apps/client/src/app/components/admin-settings/admin-settings.component.html
- 79
+ 81
@@ -2207,19 +2175,15 @@
Etiquetes
apps/client/src/app/components/admin-settings/admin-settings.component.html
- 85
-
-
- apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
- 377
+ 87
- apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
- 414
+ libs/ui/src/lib/tags-selector/tags-selector.component.html
+ 4
- apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
- 383
+ libs/ui/src/lib/tags-selector/tags-selector.component.html
+ 16
@@ -2235,7 +2199,7 @@
Està segur que vol eliminar aquesta etiqueta?
apps/client/src/app/components/admin-tag/admin-tag.component.ts
- 87
+ 85
@@ -2351,7 +2315,7 @@
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 94
+ 93
@@ -2423,7 +2387,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 288
+ 293
@@ -2439,7 +2403,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 294
+ 299
@@ -2511,7 +2475,7 @@
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 158
+ 159
@@ -2619,7 +2583,7 @@
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 154
+ 213
@@ -2635,7 +2599,7 @@
Informar d’un Problema amb les Dades
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
- 433
+ 409
@@ -2675,7 +2639,7 @@
Gestionar Activitats
apps/client/src/app/components/home-holdings/home-holdings.html
- 63
+ 62
@@ -2687,7 +2651,7 @@
libs/ui/src/lib/i18n.ts
- 98
+ 101
@@ -2699,7 +2663,7 @@
libs/ui/src/lib/i18n.ts
- 99
+ 102
@@ -2827,11 +2791,15 @@
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 251
+ 255
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
- 10
+ 44
+
+
+ apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+ 52
@@ -2991,7 +2959,7 @@
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 34
+ 69
@@ -3099,7 +3067,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 213
+ 218
@@ -3119,7 +3087,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 217
+ 222
@@ -3135,7 +3103,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 221
+ 226
@@ -3151,7 +3119,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 225
+ 230
@@ -3163,7 +3131,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 240
+ 245
@@ -3179,7 +3147,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 252
+ 257
@@ -3299,7 +3267,7 @@
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 224
+ 228
@@ -3315,7 +3283,7 @@
Please enter your coupon code.
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 213
+ 208
@@ -3323,7 +3291,7 @@
Could not redeem coupon code
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 177
+ 172
@@ -3331,7 +3299,7 @@
Coupon code has been redeemed
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 190
+ 185
@@ -3339,7 +3307,7 @@
Reload
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 191
+ 186
@@ -3351,7 +3319,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 274
+ 279
@@ -3383,7 +3351,7 @@
Do you really want to close your Ghostfolio account?
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 173
+ 174
@@ -3391,7 +3359,7 @@
Do you really want to remove this sign in method?
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 247
+ 248
@@ -3399,7 +3367,7 @@
Oops! There was an error setting up biometric authentication.
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 301
+ 302
@@ -3453,9 +3421,13 @@
Locale
Locale
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+ 322
+
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 123
+ 127
@@ -3463,7 +3435,7 @@
Date and number format
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 125
+ 129
@@ -3471,7 +3443,7 @@
Appearance
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 148
+ 152
@@ -3479,7 +3451,7 @@
Auto
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 162
+ 166
@@ -3487,7 +3459,7 @@
Light
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 163
+ 167
@@ -3495,7 +3467,7 @@
Dark
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 164
+ 168
@@ -3503,7 +3475,7 @@
Zen Mode
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 173
+ 177
apps/client/src/app/pages/features/features-page.html
@@ -3515,7 +3487,7 @@
Distraction-free experience for turbulent times
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 174
+ 178
@@ -3523,7 +3495,7 @@
Biometric Authentication
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 190
+ 194
@@ -3531,7 +3503,7 @@
Sign in with fingerprint
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 191
+ 195
@@ -3539,7 +3511,7 @@
Experimental Features
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 207
+ 211
@@ -3547,7 +3519,7 @@
Sneak peek at upcoming functionality
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 208
+ 212
@@ -3555,7 +3527,7 @@
Export Data
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 232
+ 236
@@ -3563,7 +3535,7 @@
Danger Zone
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 244
+ 248
@@ -3571,7 +3543,7 @@
Close Account
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 279
+ 283
@@ -3579,7 +3551,7 @@
This feature is currently unavailable.
apps/client/src/app/core/http-response.interceptor.ts
- 53
+ 54
@@ -3587,15 +3559,15 @@
Please try again later.
apps/client/src/app/core/http-response.interceptor.ts
- 55
+ 56
apps/client/src/app/core/http-response.interceptor.ts
- 80
+ 85
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 143
+ 144
@@ -3603,7 +3575,7 @@
This action is not allowed.
apps/client/src/app/core/http-response.interceptor.ts
- 61
+ 64
@@ -3611,11 +3583,11 @@
Oops! Something went wrong.
apps/client/src/app/core/http-response.interceptor.ts
- 78
+ 83
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 141
+ 142
@@ -3623,15 +3595,15 @@
Okay
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 152
+ 147
apps/client/src/app/core/http-response.interceptor.ts
- 81
+ 86
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 144
+ 145
@@ -3639,7 +3611,7 @@
Oops! It looks like you’re making too many requests. Please slow down a bit.
apps/client/src/app/core/http-response.interceptor.ts
- 96
+ 103
@@ -4083,7 +4055,7 @@
Open Source Software
apps/client/src/app/pages/features/features-page.html
- 278
+ 279
@@ -4091,7 +4063,7 @@
Get Started
apps/client/src/app/pages/features/features-page.html
- 303
+ 304
apps/client/src/app/pages/public/public-page.html
@@ -4211,7 +4183,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 324
+ 329
@@ -4743,7 +4715,7 @@
Import Activities
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 46
+ 47
@@ -4751,7 +4723,7 @@
Import Dividends
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 87
+ 88
@@ -4759,7 +4731,7 @@
Importing data...
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 125
+ 126
@@ -4767,7 +4739,7 @@
Import has been completed
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 133
+ 134
@@ -4775,7 +4747,7 @@
Validating data...
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 239
+ 242
@@ -5067,7 +5039,7 @@
Dividend
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 41
+ 51
libs/ui/src/lib/i18n.ts
@@ -5079,15 +5051,15 @@
Investment
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 46
+ 56
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 60
+ 72
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 88
+ 87
@@ -5095,7 +5067,7 @@
Monthly
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 54
+ 66
@@ -5103,7 +5075,7 @@
Yearly
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 55
+ 67
@@ -5119,7 +5091,7 @@
Absolute Asset Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 60
+ 102
@@ -5127,7 +5099,7 @@
Asset Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 81
+ 123
@@ -5135,7 +5107,7 @@
Absolute Currency Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 103
+ 145
@@ -5143,7 +5115,7 @@
Currency Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 127
+ 169
@@ -5151,7 +5123,7 @@
Absolute Net Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 150
+ 192
@@ -5159,7 +5131,7 @@
Net Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 169
+ 211
@@ -5167,7 +5139,7 @@
Top
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 197
+ 239
@@ -5175,7 +5147,7 @@
Bottom
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 246
+ 288
@@ -5183,7 +5155,7 @@
Portfolio Evolution
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 299
+ 341
@@ -5191,7 +5163,7 @@
Investment Timeline
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 326
+ 368
@@ -5199,7 +5171,7 @@
Current Streak
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 347
+ 389
@@ -5207,7 +5179,7 @@
Longest Streak
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 356
+ 398
@@ -5215,7 +5187,7 @@
Dividend Timeline
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 383
+ 425
@@ -5255,7 +5227,7 @@
Currency Cluster Risks
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 58
+ 93
@@ -5263,7 +5235,7 @@
Account Cluster Risks
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 106
+ 141
@@ -5319,7 +5291,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 201
+ 206
@@ -5335,7 +5307,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 205
+ 210
@@ -5351,7 +5323,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 209
+ 214
@@ -5367,7 +5339,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 229
+ 234
@@ -5415,7 +5387,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 261
+ 266
@@ -5423,7 +5395,7 @@
For ambitious investors who need the full picture of their financial assets.
apps/client/src/app/pages/pricing/pricing-page.html
- 194
+ 199
@@ -5431,7 +5403,7 @@
Email and Chat Support
apps/client/src/app/pages/pricing/pricing-page.html
- 257
+ 262
@@ -5439,7 +5411,7 @@
One-time payment, no auto-renewal.
apps/client/src/app/pages/pricing/pricing-page.html
- 298
+ 303
@@ -5447,7 +5419,7 @@
It’s free.
apps/client/src/app/pages/pricing/pricing-page.html
- 327
+ 332
@@ -5519,23 +5491,7 @@
Copy to clipboard
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
- 26
-
-
-
- I agree to have stored my Security Token from above in a secure place. If I lose it, I cannot get my account back.
- I agree to have stored my Security Token from above in a secure place. If I lose it, I cannot get my account back.
-
- apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
- 32
-
-
-
- Agree and continue
- Agree and continue
-
- apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
- 45
+ 68
@@ -5603,11 +5559,11 @@
Switzerland
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 59
+ 58
libs/ui/src/lib/i18n.ts
- 90
+ 93
@@ -5615,7 +5571,7 @@
Global
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 60
+ 59
libs/ui/src/lib/i18n.ts
@@ -6035,7 +5991,7 @@
Do you really want to delete these activities?
libs/ui/src/lib/activities-table/activities-table.component.ts
- 218
+ 219
@@ -6043,7 +5999,7 @@
Do you really want to delete this activity?
libs/ui/src/lib/activities-table/activities-table.component.ts
- 228
+ 229
@@ -6403,7 +6359,7 @@
Japan
libs/ui/src/lib/i18n.ts
- 84
+ 86
@@ -6695,7 +6651,7 @@
Extreme Fear
libs/ui/src/lib/i18n.ts
- 96
+ 99
@@ -6703,7 +6659,7 @@
Extreme Greed
libs/ui/src/lib/i18n.ts
- 97
+ 100
@@ -6711,7 +6667,7 @@
Neutral
libs/ui/src/lib/i18n.ts
- 100
+ 103
@@ -6727,7 +6683,7 @@
Valid until
apps/client/src/app/components/admin-settings/admin-settings.component.html
- 26
+ 28
libs/ui/src/lib/membership-card/membership-card.component.html
@@ -6767,7 +6723,7 @@
Alternative
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 82
+ 81
@@ -6775,7 +6731,7 @@
App
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 83
+ 82
@@ -6783,7 +6739,7 @@
Budgeting
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 84
+ 83
@@ -6791,7 +6747,7 @@
Community
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 85
+ 84
@@ -6799,7 +6755,7 @@
Family Office
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 86
+ 85
@@ -6807,7 +6763,7 @@
Investor
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 89
+ 88
@@ -6815,7 +6771,7 @@
Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 90
+ 89
@@ -6823,7 +6779,7 @@
Personal Finance
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 92
+ 91
@@ -6831,7 +6787,7 @@
Privacy
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 93
+ 92
@@ -6839,7 +6795,7 @@
Software
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 95
+ 94
@@ -6847,7 +6803,7 @@
Tool
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 96
+ 95
@@ -6855,7 +6811,7 @@
User Experience
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 97
+ 96
@@ -6863,7 +6819,7 @@
Wealth
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 98
+ 97
@@ -6871,7 +6827,7 @@
Wealth Management
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
- 99
+ 98
@@ -6879,7 +6835,7 @@
Australia
libs/ui/src/lib/i18n.ts
- 73
+ 74
@@ -6887,7 +6843,7 @@
Austria
libs/ui/src/lib/i18n.ts
- 74
+ 75
@@ -6895,7 +6851,7 @@
Belgium
libs/ui/src/lib/i18n.ts
- 75
+ 76
@@ -6903,7 +6859,7 @@
Bulgaria
libs/ui/src/lib/i18n.ts
- 76
+ 78
@@ -6911,7 +6867,7 @@
Canada
libs/ui/src/lib/i18n.ts
- 77
+ 79
@@ -6919,7 +6875,7 @@
Czech Republic
libs/ui/src/lib/i18n.ts
- 78
+ 80
@@ -6927,7 +6883,7 @@
Finland
libs/ui/src/lib/i18n.ts
- 79
+ 81
@@ -6935,7 +6891,7 @@
France
libs/ui/src/lib/i18n.ts
- 80
+ 82
@@ -6943,7 +6899,7 @@
Germany
libs/ui/src/lib/i18n.ts
- 81
+ 83
@@ -6951,7 +6907,7 @@
India
libs/ui/src/lib/i18n.ts
- 82
+ 84
@@ -6959,7 +6915,7 @@
Italy
libs/ui/src/lib/i18n.ts
- 83
+ 85
@@ -6967,7 +6923,7 @@
Netherlands
libs/ui/src/lib/i18n.ts
- 85
+ 87
@@ -6975,7 +6931,7 @@
New Zealand
libs/ui/src/lib/i18n.ts
- 86
+ 88
@@ -6983,7 +6939,7 @@
Poland
libs/ui/src/lib/i18n.ts
- 87
+ 89
@@ -6991,7 +6947,7 @@
Romania
libs/ui/src/lib/i18n.ts
- 88
+ 90
@@ -6999,7 +6955,7 @@
South Africa
libs/ui/src/lib/i18n.ts
- 89
+ 92
@@ -7007,7 +6963,7 @@
Thailand
libs/ui/src/lib/i18n.ts
- 91
+ 94
@@ -7015,7 +6971,7 @@
United States
libs/ui/src/lib/i18n.ts
- 93
+ 96
@@ -7023,7 +6979,7 @@
Error
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
- 317
+ 390
@@ -7047,7 +7003,7 @@
Inactive
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 173
+ 232
@@ -7091,16 +7047,16 @@
- Change with currency effect Change
- Change with currency effect Change
+ Change with currency effect Change
+ Change with currency effect Change
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
50
- Performance with currency effect Performance
- Performance with currency effect Performance
+ Performance with currency effect Performance
+ Performance with currency effect Performance
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
69
@@ -7330,20 +7286,12 @@
4
-
- NEW
- NEW
-
- apps/client/src/app/components/admin-settings/admin-settings.component.html
- 15
-
-
Set API key
Set API key
apps/client/src/app/components/admin-settings/admin-settings.component.html
- 68
+ 70
@@ -7354,9 +7302,9 @@
23
-
- Get access to 100’000+ tickers from over 50 exchanges
- Get access to 100’000+ tickers from over 50 exchanges
+
+ Get access to 80’000+ tickers from over 50 exchanges
+ Get access to 80’000+ tickers from over 50 exchanges
libs/ui/src/lib/i18n.ts
24
@@ -7367,7 +7315,7 @@
Ukraine
libs/ui/src/lib/i18n.ts
- 92
+ 95
@@ -7457,7 +7405,7 @@
Economic Market Cluster Risks
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 130
+ 165
@@ -7465,7 +7413,7 @@
of
apps/client/src/app/components/admin-settings/admin-settings.component.html
- 40
+ 42
@@ -7473,7 +7421,7 @@
daily requests
apps/client/src/app/components/admin-settings/admin-settings.component.html
- 42
+ 44
@@ -7481,7 +7429,7 @@
Remove API key
apps/client/src/app/components/admin-settings/admin-settings.component.html
- 56
+ 58
@@ -7529,7 +7477,7 @@
Could not generate an API key
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 139
+ 134
@@ -7537,7 +7485,7 @@
Set this API key in your self-hosted environment:
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 154
+ 149
@@ -7545,7 +7493,7 @@
Ghostfolio Premium Data Provider API Key
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 157
+ 152
@@ -7553,7 +7501,7 @@
Do you really want to generate a new API key?
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 162
+ 157
@@ -7585,7 +7533,7 @@
out of
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 22
+ 56
@@ -7593,7 +7541,7 @@
rules align with your portfolio.
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 24
+ 58
@@ -7609,7 +7557,7 @@
Asset Class Cluster Risks
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
- 82
+ 117
@@ -7633,7 +7581,7 @@
Please enter your Ghostfolio API key.
apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts
- 59
+ 57
@@ -7641,15 +7589,219 @@
AI prompt has been copied to the clipboard
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 149
+ 173
+
+
+
+ Link has been copied to the clipboard
+ Link has been copied to the clipboard
+
+ apps/client/src/app/components/access-table/access-table.component.ts
+ 65
+
+
+
+ Early Access
+ Early Access
+
+ apps/client/src/app/components/admin-settings/admin-settings.component.html
+ 16
+
+
+
+ Regional Market Cluster Risks
+ Regional Market Cluster Risks
+
+ apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
+ 189
+
+
+
+ Lazy
+ Lazy
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+ 93
+
+
+
+ Instant
+ Instant
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+ 97
+
+
+
+ Default Market Price
+ Default Market Price
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+ 300
+
+
+
+ Mode
+ Mode
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+ 328
+
+
+
+ Selector
+ Selector
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+ 341
+
+
+
+ HTTP Request Headers
+ HTTP Request Headers
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+ 310
+
+
+
+ end of day
+ end of day
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+ 93
-
- Copy AI prompt to clipboard
- Copy AI prompt to clipboard
+
+ real-time
+ real-time
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+ 97
+
+
+
+ Open Duck.ai
+ Open Duck.ai
+
+ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+ 174
+
+
+
+ Create
+ Create
+
+ libs/ui/src/lib/tags-selector/tags-selector.component.html
+ 50
+
+
+
+ Market Data
+ Market Data
+
+ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
+ 374
+
+
+
+ Change
+ Change
+
+ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
+ 365
+
+
+
+ Performance
+ Performance
+
+ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
+ 365
+
+
+ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
+ 378
+
+
+
+ Copy portfolio data to clipboard for AI prompt
+ Copy portfolio data to clipboard for AI prompt
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 27
+ 42
+
+
+
+ Copy AI prompt to clipboard for analysis
+ Copy AI prompt to clipboard for analysis
+
+ apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+ 67
+
+
+
+ Armenia
+ Armenia
+
+ libs/ui/src/lib/i18n.ts
+ 73
+
+
+
+ British Virgin Islands
+ British Virgin Islands
+
+ libs/ui/src/lib/i18n.ts
+ 77
+
+
+
+ Singapore
+ Singapore
+
+ libs/ui/src/lib/i18n.ts
+ 91
+
+
+
+ Terms and Conditions
+ Terms and Conditions
+
+ apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+ 10
+
+
+
+ Please keep your security token safe. If you lose it, you will not be able to recover your account.
+ Please keep your security token safe. If you lose it, you will not be able to recover your account.
+
+ apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+ 13
+
+
+
+ I understand that if I lose my security token, I cannot recover my account.
+ I understand that if I lose my security token, I cannot recover my account.
+
+ apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+ 23
+
+
+
+ Continue
+ Continue
+
+ apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+ 38
+
+
+
+ Here is your security token. It is only visible once, please store and keep it in a safe place.
+ Here is your security token. It is only visible once, please store and keep it in a safe place.
+
+ apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
+ 47