Compare commits
130 Commits
Author | SHA1 | Date | |
---|---|---|---|
31f0056a2d | |||
550e646079 | |||
37ff7acf04 | |||
8236091477 | |||
2a71cb66de | |||
e60fe48fdd | |||
d40bc5070a | |||
fda4e0ea7d | |||
08d696ce33 | |||
46614a7c24 | |||
02b433eb1e | |||
25112a450b | |||
727340748b | |||
8ad6492477 | |||
4af76f6f6d | |||
10940214a5 | |||
d9a6c22e1e | |||
692309988c | |||
42a54263f9 | |||
4fb88859b2 | |||
aa24b5e8c6 | |||
90e18338f6 | |||
ad5ae938ef | |||
c9a8dd4958 | |||
f1ec5e704e | |||
f40f0653c2 | |||
5f7a230fd3 | |||
71feb531e8 | |||
ec3552d7f6 | |||
41875e70d6 | |||
5fa0540936 | |||
5b69dee246 | |||
19b0fe04a6 | |||
19ea4479ff | |||
0b2f6a312c | |||
f79d60014b | |||
5b7409d08e | |||
6230aa87e2 | |||
8b615d2f56 | |||
4100446cac | |||
ad3e6d637c | |||
aa87262954 | |||
01b6bb5b99 | |||
884b7f4de7 | |||
3f8a2b47f9 | |||
e2e4c9be3c | |||
0f7c6ff0fe | |||
703a96f4db | |||
42c0560422 | |||
eb63802d01 | |||
6d9191a46f | |||
6744245d8b | |||
8f64a77a9d | |||
0d5fc7655b | |||
c511ec7e33 | |||
b12349a148 | |||
f7e3a4c727 | |||
5f276469b7 | |||
69e1d92ed3 | |||
ef2849aa6c | |||
c668d7b456 | |||
e23bf62859 | |||
54c5746d21 | |||
7130ac7565 | |||
1851ae137f | |||
6f6ff94979 | |||
7f25066f0f | |||
fc795aaa8c | |||
d0112968e8 | |||
522025ffa0 | |||
27bf662281 | |||
93c27277c6 | |||
5e6adfcef5 | |||
ab691bb27a | |||
8fc5676443 | |||
1fe1e2fe0c | |||
921d38a706 | |||
6161d5e77c | |||
369386f976 | |||
41437636b1 | |||
b21884eb66 | |||
1c5437e1fd | |||
58278ba5e6 | |||
921f3e9807 | |||
75ca125a70 | |||
a1fd4e7a38 | |||
0d5a8eb33e | |||
b088df2fa3 | |||
f45d8f616a | |||
d8300502ce | |||
502d51ad29 | |||
bc33e5f147 | |||
48ba8f936b | |||
05ec4cce05 | |||
d74f283707 | |||
0f8bc7db32 | |||
431500f28a | |||
9672de174e | |||
c6aa06b933 | |||
1f46a6b6f3 | |||
1bed940bc0 | |||
f9eb3cc3c5 | |||
2519c3ffb0 | |||
91013d1d10 | |||
6deefb9c43 | |||
d0744e07df | |||
93e1ee3ba7 | |||
dceaa55a6c | |||
8b4d55925d | |||
754b49e50f | |||
6ccbda8169 | |||
b0fb986208 | |||
0b59fc639d | |||
7ddd6f27b5 | |||
c5d56f4b47 | |||
2f2b712999 | |||
c2fd31f5e5 | |||
f2d70f9070 | |||
f41dd9cd8e | |||
7d238b4935 | |||
da6591fca0 | |||
1f9b9e9998 | |||
49c4ea306d | |||
ccb5c664ef | |||
97e165ff69 | |||
45aefb6a45 | |||
2435535975 | |||
bd3d43bf05 | |||
02dc7c52b1 | |||
ff59fd4196 |
@ -11,6 +11,5 @@ POSTGRES_USER=user
|
|||||||
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
|
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
|
||||||
|
|
||||||
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
|
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
|
||||||
ALPHA_VANTAGE_API_KEY=
|
|
||||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
|
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
|
||||||
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
|
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
],
|
],
|
||||||
"attributeSort": "ASC",
|
"attributeSort": "ASC",
|
||||||
"endOfLine": "auto",
|
"endOfLine": "auto",
|
||||||
|
"plugins": ["prettier-plugin-organize-attributes"],
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
|
220
CHANGELOG.md
220
CHANGELOG.md
@ -5,6 +5,222 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 2.9.0 - 2023-10-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to search for a holding by `isin`, `name` and `symbol` (experimental)
|
||||||
|
- Added support for notes in the activities import
|
||||||
|
- Added support to search in the platform selector of the create or update account dialog
|
||||||
|
- Added support for a search query in the portfolio position endpoint
|
||||||
|
- Added the application version to the endpoint `GET api/v1/admin`
|
||||||
|
- Introduced a carousel component for the testimonial section on the landing page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Displayed the link to the markets overview on the home page without any permission
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the style of the active features page in the navigation on desktop
|
||||||
|
|
||||||
|
## 2.8.0 - 2023-10-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Supported enter key press to submit the form of the create or update account dialog
|
||||||
|
- Added the application version to the admin control panel
|
||||||
|
- Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/order`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Harmonized the settings icon of the user account page
|
||||||
|
- Improved the usability to set an asset profile as a benchmark
|
||||||
|
- Reload platforms after making a change in the admin control panel
|
||||||
|
- Reload tags after making a change in the admin control panel
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the sidebar navigation on the user account page
|
||||||
|
|
||||||
|
## 2.7.0 - 2023-09-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a new static portfolio analysis rule: Emergency fund setup
|
||||||
|
- Added tabs to the user account page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Set up the _Inter_ font family
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.7.0` to `2.8.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a link on the features page
|
||||||
|
|
||||||
|
## 2.6.0 - 2023-09-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the management of tags in the admin control panel
|
||||||
|
- Added a blog post: _Hacktoberfest 2023_
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `prettier` from version `3.0.2` to `3.0.3`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.5.0` to `2.7.0`
|
||||||
|
|
||||||
|
## 2.5.0 - 2023-09-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for translated activity types in the activities table
|
||||||
|
- Added support for dates in `DD.MM.YYYY` format in the activities import
|
||||||
|
- Set up the language localization for Türkçe (`tr`)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Skipped creating queue jobs for asset profiles with `MANUAL` data source on creating a new activity
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the cash position in the holdings table
|
||||||
|
|
||||||
|
## 2.4.0 - 2023-09-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for interest on account level (experimental)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the preselected currency based on the account's currency in the create or edit activity dialog
|
||||||
|
- Unlocked the experimental features setting for all users
|
||||||
|
- Upgraded `prisma` from version `5.2.0` to `5.3.1`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a memory leak related to the server's timezone (behind UTC) in the data gathering
|
||||||
|
|
||||||
|
## 2.3.0 - 2023-09-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for fees on account level (experimental)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the export functionality for liabilities
|
||||||
|
|
||||||
|
## 2.2.0 - 2023-09-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduced a sidebar navigation on desktop
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the style of the system message
|
||||||
|
- Upgraded _Postgres_ from version `12` to `15` in the `docker-compose` files
|
||||||
|
|
||||||
|
## 2.1.0 - 2023-09-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to drop a file in the import activities dialog
|
||||||
|
- Added a timeout to all data source requests
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Harmonized the style of the user interface for granting and revoking public access to share the portfolio
|
||||||
|
- Removed the account type from the user interface as a preparation to remove it from the `Account` database schema
|
||||||
|
- Improved the logger output of the info service
|
||||||
|
- Harmonized the logger output: `<symbol> (<dataSource>)`
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Improved the language localization for Italian (`it`)
|
||||||
|
- Improved the language localization for Dutch (`nl`)
|
||||||
|
- Improved the read-only mode
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the timeout in _EOD Historical Data_ requests
|
||||||
|
- Fixed an issue with the portfolio summary caused by the language localization for Dutch (`nl`)
|
||||||
|
|
||||||
|
## 2.0.0 - 2023-09-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for the cryptocurrency _CyberConnect_
|
||||||
|
- Added a blog post: _Announcing Ghostfolio 2.0_
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Breaking Change**: Removed the deprecated environment variable `BASE_CURRENCY`
|
||||||
|
- Improved the validation in the activities import
|
||||||
|
- Deactivated _Internet Identity_ as a social login provider for the account registration
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Refreshed the cryptocurrencies list
|
||||||
|
- Changed the version in the `docker-compose` files from `3.7` to `3.9`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.4.4` to `2.5.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the _Yahoo Finance_ data enhancer where countries and sectors have been removed
|
||||||
|
|
||||||
|
## 1.305.0 - 2023-09-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added _Hacker News_ to the _As seen in_ section on the landing page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Shortened the page titles
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `prisma` from version `4.16.2` to `5.2.0`
|
||||||
|
- Upgraded `replace-in-file` from version `6.3.5` to `7.0.1`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.4.3` to `2.4.4`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the alignment in the header navigation
|
||||||
|
- Fixed the alignment in the menu of the impersonation mode
|
||||||
|
|
||||||
|
## 1.304.0 - 2023-08-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added health check endpoints for data enhancers
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `Nx` from version `16.7.2` to `16.7.4`
|
||||||
|
- Upgraded `prettier` from version `2.8.4` to `3.0.2`
|
||||||
|
|
||||||
|
## 1.303.0 - 2023-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a blog post: _Ghostfolio joins OSS Friends_
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refreshed the cryptocurrencies list
|
||||||
|
- Improved the _OSS Friends_ page
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the _Trackinsight_ data enhancer for asset profile data
|
||||||
|
|
||||||
|
## 1.302.0 - 2023-08-20
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `angular` from version `16.1.8` to `16.2.1`
|
||||||
|
- Upgraded `Nx` from version `16.6.0` to `16.7.2`
|
||||||
|
|
||||||
## 1.301.1 - 2023-08-19
|
## 1.301.1 - 2023-08-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -1435,7 +1651,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Set up the language localization for Italiano (`it`)
|
- Set up the language localization for Italian (`it`)
|
||||||
- Extended the landing page
|
- Extended the landing page
|
||||||
|
|
||||||
## 1.195.0 - 20.09.2022
|
## 1.195.0 - 20.09.2022
|
||||||
@ -2858,7 +3074,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Supported the management of additional currencies in the admin control panel
|
- Supported the management of additional currencies in the admin control panel
|
||||||
- Introduced the system message
|
- Introduced the system message
|
||||||
- Introduced the read only mode
|
- Introduced the read-only mode
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -18,6 +18,12 @@
|
|||||||
|
|
||||||
### Prisma
|
### Prisma
|
||||||
|
|
||||||
|
#### Access database via GUI
|
||||||
|
|
||||||
|
Run `yarn database:gui`
|
||||||
|
|
||||||
|
https://www.prisma.io/studio
|
||||||
|
|
||||||
#### Synchronize schema with database for prototyping
|
#### Synchronize schema with database for prototyping
|
||||||
|
|
||||||
Run `yarn database:push`
|
Run `yarn database:push`
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
[](#contributing)
|
[](#contributing)
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||||
|
|
||||||
|
New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
|
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
|
||||||
@ -25,7 +27,7 @@
|
|||||||
|
|
||||||
## Ghostfolio Premium
|
## Ghostfolio Premium
|
||||||
|
|
||||||
Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. Revenue is used to cover the costs of the hosting infrastructure and to fund ongoing development.
|
||||||
|
|
||||||
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
|
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
|
||||||
|
|
||||||
@ -136,9 +138,9 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
|||||||
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
||||||
At each start, the container will automatically apply the database schema migrations if needed.
|
At each start, the container will automatically apply the database schema migrations if needed.
|
||||||
|
|
||||||
### Run with _Unraid_ (Community)
|
### Home Server Systems (Community)
|
||||||
|
|
||||||
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
|
Ghostfolio is available for various home server systems, including [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -10,8 +10,9 @@ import {
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
export class CreateAccountDto {
|
export class CreateAccountDto {
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
accountType: AccountType;
|
accountType?: AccountType;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
balance: number;
|
balance: number;
|
||||||
|
12
apps/api/src/app/account/transfer-balance.dto.ts
Normal file
12
apps/api/src/app/account/transfer-balance.dto.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class TransferBalanceDto {
|
||||||
|
@IsString()
|
||||||
|
accountIdFrom: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
accountIdTo: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
balance: number;
|
||||||
|
}
|
@ -10,8 +10,9 @@ import {
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
export class UpdateAccountDto {
|
export class UpdateAccountDto {
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
accountType: AccountType;
|
accountType?: AccountType;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
balance: number;
|
balance: number;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -6,7 +7,12 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
PROPERTY_CURRENCIES,
|
||||||
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
|
PROPERTY_IS_USER_SIGNUP_ENABLED
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
@ -23,8 +29,6 @@ import { groupBy } from 'lodash';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
private baseCurrency: string;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
@ -34,9 +38,7 @@ export class AdminService {
|
|||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly subscriptionService: SubscriptionService,
|
private readonly subscriptionService: SubscriptionService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {
|
) {}
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addAssetProfile({
|
public async addAssetProfile({
|
||||||
dataSource,
|
dataSource,
|
||||||
@ -80,15 +82,15 @@ export class AdminService {
|
|||||||
exchangeRates: this.exchangeRateDataService
|
exchangeRates: this.exchangeRateDataService
|
||||||
.getCurrencies()
|
.getCurrencies()
|
||||||
.filter((currency) => {
|
.filter((currency) => {
|
||||||
return currency !== this.baseCurrency;
|
return currency !== DEFAULT_CURRENCY;
|
||||||
})
|
})
|
||||||
.map((currency) => {
|
.map((currency) => {
|
||||||
return {
|
return {
|
||||||
label1: this.baseCurrency,
|
label1: DEFAULT_CURRENCY,
|
||||||
label2: currency,
|
label2: currency,
|
||||||
value: this.exchangeRateDataService.toCurrency(
|
value: this.exchangeRateDataService.toCurrency(
|
||||||
1,
|
1,
|
||||||
this.baseCurrency,
|
DEFAULT_CURRENCY,
|
||||||
currency
|
currency
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@ -96,7 +98,8 @@ export class AdminService {
|
|||||||
settings: await this.propertyService.get(),
|
settings: await this.propertyService.get(),
|
||||||
transactionCount: await this.prismaService.order.count(),
|
transactionCount: await this.prismaService.order.count(),
|
||||||
userCount: await this.prismaService.user.count(),
|
userCount: await this.prismaService.user.count(),
|
||||||
users: await this.getUsersWithAnalytics()
|
users: await this.getUsersWithAnalytics(),
|
||||||
|
version: environment.version
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,7 +309,9 @@ export class AdminService {
|
|||||||
response = await this.propertyService.delete({ key });
|
response = await this.propertyService.delete({ key });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === PROPERTY_CURRENCIES) {
|
if (key === PROPERTY_IS_READ_ONLY_MODE && value === 'true') {
|
||||||
|
await this.putSetting(PROPERTY_IS_USER_SIGNUP_ENABLED, 'false');
|
||||||
|
} else if (key === PROPERTY_CURRENCIES) {
|
||||||
await this.exchangeRateDataService.initialize();
|
await this.exchangeRateDataService.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
|||||||
import { SitemapModule } from './sitemap/sitemap.module';
|
import { SitemapModule } from './sitemap/sitemap.module';
|
||||||
import { SubscriptionModule } from './subscription/subscription.module';
|
import { SubscriptionModule } from './subscription/subscription.module';
|
||||||
import { SymbolModule } from './symbol/symbol.module';
|
import { SymbolModule } from './symbol/symbol.module';
|
||||||
|
import { TagModule } from './tag/tag.module';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -101,6 +102,7 @@ import { UserModule } from './user/user.module';
|
|||||||
SitemapModule,
|
SitemapModule,
|
||||||
SubscriptionModule,
|
SubscriptionModule,
|
||||||
SymbolModule,
|
SymbolModule,
|
||||||
|
TagModule,
|
||||||
TwitterBotModule,
|
TwitterBotModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
|
@ -41,9 +41,8 @@ export class AuthController {
|
|||||||
@Param('accessToken') accessToken: string
|
@Param('accessToken') accessToken: string
|
||||||
): Promise<OAuthResponse> {
|
): Promise<OAuthResponse> {
|
||||||
try {
|
try {
|
||||||
const authToken = await this.authService.validateAnonymousLogin(
|
const authToken =
|
||||||
accessToken
|
await this.authService.validateAnonymousLogin(accessToken);
|
||||||
);
|
|
||||||
return { authToken };
|
return { authToken };
|
||||||
} catch {
|
} catch {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
|
@ -55,7 +55,7 @@ export class AuthService {
|
|||||||
const isUserSignupEnabled =
|
const isUserSignupEnabled =
|
||||||
await this.propertyService.isUserSignupEnabled();
|
await this.propertyService.isUserSignupEnabled();
|
||||||
|
|
||||||
if (!isUserSignupEnabled) {
|
if (!isUserSignupEnabled || true) {
|
||||||
throw new Error('Sign up forbidden');
|
throw new Error('Sign up forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
@ -32,32 +33,6 @@ export class BenchmarkController {
|
|||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
|
||||||
public async getBenchmark(): Promise<BenchmarkResponse> {
|
|
||||||
return {
|
|
||||||
benchmarks: await this.benchmarkService.getBenchmarks()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':dataSource/:symbol/:startDateString')
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
|
||||||
public async getBenchmarkMarketDataBySymbol(
|
|
||||||
@Param('dataSource') dataSource: DataSource,
|
|
||||||
@Param('startDateString') startDateString: string,
|
|
||||||
@Param('symbol') symbol: string
|
|
||||||
): Promise<BenchmarkMarketDataDetails> {
|
|
||||||
const startDate = new Date(startDateString);
|
|
||||||
|
|
||||||
return this.benchmarkService.getMarketDataBySymbol({
|
|
||||||
dataSource,
|
|
||||||
startDate,
|
|
||||||
symbol
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
|
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
|
||||||
@ -94,4 +69,70 @@ export class BenchmarkController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete(':dataSource/:symbol')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteBenchmark(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
this.request.user.permissions,
|
||||||
|
permissions.accessAdminControl
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const benchmark = await this.benchmarkService.deleteBenchmark({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!benchmark) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return benchmark;
|
||||||
|
} catch {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||||
|
StatusCodes.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async getBenchmark(): Promise<BenchmarkResponse> {
|
||||||
|
return {
|
||||||
|
benchmarks: await this.benchmarkService.getBenchmarks()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':dataSource/:symbol/:startDateString')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
public async getBenchmarkMarketDataBySymbol(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('startDateString') startDateString: string,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<BenchmarkMarketDataDetails> {
|
||||||
|
const startDate = new Date(startDateString);
|
||||||
|
|
||||||
|
return this.benchmarkService.getMarketDataBySymbol({
|
||||||
|
dataSource,
|
||||||
|
startDate,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,6 +245,43 @@ export class BenchmarkService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteBenchmark({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
|
||||||
|
const assetProfile = await this.prismaService.symbolProfile.findFirst({
|
||||||
|
where: {
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!assetProfile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let benchmarks =
|
||||||
|
((await this.propertyService.getByKey(
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
|
)) as BenchmarkProperty[]) ?? [];
|
||||||
|
|
||||||
|
benchmarks = benchmarks.filter(({ symbolProfileId }) => {
|
||||||
|
return symbolProfileId !== assetProfile.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.propertyService.put({
|
||||||
|
key: PROPERTY_BENCHMARKS,
|
||||||
|
value: JSON.stringify(benchmarks)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
id: assetProfile.id,
|
||||||
|
name: assetProfile.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private getMarketCondition(aPerformanceInPercent: number) {
|
private getMarketCondition(aPerformanceInPercent: number) {
|
||||||
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@ import {
|
|||||||
UseGuards
|
UseGuards
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { ExchangeRateService } from './exchange-rate.service';
|
import { ExchangeRateService } from './exchange-rate.service';
|
||||||
import { parseISO } from 'date-fns';
|
|
||||||
|
|
||||||
@Controller('exchange-rate')
|
@Controller('exchange-rate')
|
||||||
export class ExchangeRateController {
|
export class ExchangeRateController {
|
||||||
|
@ -26,18 +26,8 @@ export class ExportService {
|
|||||||
where: { userId }
|
where: { userId }
|
||||||
})
|
})
|
||||||
).map(
|
).map(
|
||||||
({
|
({ balance, comment, currency, id, isExcluded, name, platformId }) => {
|
||||||
accountType,
|
|
||||||
balance,
|
|
||||||
comment,
|
|
||||||
currency,
|
|
||||||
id,
|
|
||||||
isExcluded,
|
|
||||||
name,
|
|
||||||
platformId
|
|
||||||
}) => {
|
|
||||||
return {
|
return {
|
||||||
accountType,
|
|
||||||
balance,
|
balance,
|
||||||
comment,
|
comment,
|
||||||
currency,
|
currency,
|
||||||
@ -87,7 +77,13 @@ export class ExportService {
|
|||||||
currency: SymbolProfile.currency,
|
currency: SymbolProfile.currency,
|
||||||
dataSource: SymbolProfile.dataSource,
|
dataSource: SymbolProfile.dataSource,
|
||||||
date: date.toISOString(),
|
date: date.toISOString(),
|
||||||
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
|
symbol:
|
||||||
|
type === 'FEE' ||
|
||||||
|
type === 'INTEREST' ||
|
||||||
|
type === 'ITEM' ||
|
||||||
|
type === 'LIABILITY'
|
||||||
|
? SymbolProfile.name
|
||||||
|
: SymbolProfile.symbol
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -18,6 +18,19 @@ export class HealthController {
|
|||||||
@Get()
|
@Get()
|
||||||
public async getHealth() {}
|
public async getHealth() {}
|
||||||
|
|
||||||
|
@Get('data-enhancer/:name')
|
||||||
|
public async getHealthOfDataEnhancer(@Param('name') name: string) {
|
||||||
|
const hasResponse =
|
||||||
|
await this.healthService.hasResponseFromDataEnhancer(name);
|
||||||
|
|
||||||
|
if (hasResponse !== true) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE),
|
||||||
|
StatusCodes.SERVICE_UNAVAILABLE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('data-provider/:dataSource')
|
@Get('data-provider/:dataSource')
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
public async getHealthOfDataProvider(
|
public async getHealthOfDataProvider(
|
||||||
@ -30,9 +43,8 @@ export class HealthController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasResponse = await this.healthService.hasResponseFromDataProvider(
|
const hasResponse =
|
||||||
dataSource
|
await this.healthService.hasResponseFromDataProvider(dataSource);
|
||||||
);
|
|
||||||
|
|
||||||
if (hasResponse !== true) {
|
if (hasResponse !== true) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
|
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ import { HealthService } from './health.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
imports: [ConfigurationModule, DataProviderModule],
|
imports: [ConfigurationModule, DataEnhancerModule, DataProviderModule],
|
||||||
providers: [HealthService]
|
providers: [HealthService]
|
||||||
})
|
})
|
||||||
export class HealthModule {}
|
export class HealthModule {}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
@ -5,9 +6,14 @@ import { DataSource } from '@prisma/client';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class HealthService {
|
export class HealthService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly dataEnhancerService: DataEnhancerService,
|
||||||
private readonly dataProviderService: DataProviderService
|
private readonly dataProviderService: DataProviderService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async hasResponseFromDataEnhancer(aName: string) {
|
||||||
|
return this.dataEnhancerService.enhance(aName);
|
||||||
|
}
|
||||||
|
|
||||||
public async hasResponseFromDataProvider(aDataSource: DataSource) {
|
public async hasResponseFromDataProvider(aDataSource: DataSource) {
|
||||||
return this.dataProviderService.checkQuote(aDataSource);
|
return this.dataProviderService.checkQuote(aDataSource);
|
||||||
}
|
}
|
||||||
|
@ -410,7 +410,7 @@ export class ImportService {
|
|||||||
currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
//@ts-ignore
|
// @ts-ignore
|
||||||
SymbolProfile: assetProfile,
|
SymbolProfile: assetProfile,
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
value,
|
||||||
@ -566,7 +566,7 @@ export class ImportService {
|
|||||||
])
|
])
|
||||||
)?.[symbol];
|
)?.[symbol];
|
||||||
|
|
||||||
if (!assetProfile) {
|
if (!assetProfile?.name) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
@ -28,11 +29,11 @@ import { InfoService } from './info.service';
|
|||||||
signOptions: { expiresIn: '30 days' }
|
signOptions: { expiresIn: '30 days' }
|
||||||
}),
|
}),
|
||||||
PlatformModule,
|
PlatformModule,
|
||||||
PrismaModule,
|
|
||||||
PropertyModule,
|
PropertyModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
TagModule
|
TagModule,
|
||||||
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [InfoService]
|
providers: [InfoService]
|
||||||
})
|
})
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
DEFAULT_REQUEST_TIMEOUT,
|
||||||
PROPERTY_BETTER_UPTIME_MONITOR_ID,
|
PROPERTY_BETTER_UPTIME_MONITOR_ID,
|
||||||
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
|
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
|
||||||
PROPERTY_DEMO_USER_ID,
|
PROPERTY_DEMO_USER_ID,
|
||||||
@ -44,21 +46,17 @@ export class InfoService {
|
|||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly platformService: PlatformService,
|
private readonly platformService: PlatformService,
|
||||||
private readonly prismaService: PrismaService,
|
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly redisCacheService: RedisCacheService,
|
||||||
private readonly tagService: TagService
|
private readonly tagService: TagService,
|
||||||
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(): Promise<InfoItem> {
|
public async get(): Promise<InfoItem> {
|
||||||
const info: Partial<InfoItem> = {};
|
const info: Partial<InfoItem> = {};
|
||||||
let isReadOnlyMode: boolean;
|
let isReadOnlyMode: boolean;
|
||||||
const platforms = (
|
const platforms = await this.platformService.getPlatforms({
|
||||||
await this.platformService.getPlatforms({
|
orderBy: { name: 'asc' }
|
||||||
orderBy: { name: 'asc' }
|
|
||||||
})
|
|
||||||
).map(({ id, name }) => {
|
|
||||||
return { id, name };
|
|
||||||
});
|
});
|
||||||
let systemMessage: string;
|
let systemMessage: string;
|
||||||
|
|
||||||
@ -139,18 +137,13 @@ export class InfoService {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
systemMessage,
|
systemMessage,
|
||||||
tags,
|
tags,
|
||||||
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
baseCurrency: DEFAULT_CURRENCY,
|
||||||
currencies: this.exchangeRateDataService.getCurrencies()
|
currencies: this.exchangeRateDataService.getCurrencies()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async countActiveUsers(aDays: number) {
|
private async countActiveUsers(aDays: number) {
|
||||||
return await this.prismaService.user.count({
|
return this.userService.count({
|
||||||
orderBy: {
|
|
||||||
Analytics: {
|
|
||||||
updatedAt: 'desc'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
{
|
{
|
||||||
@ -172,16 +165,24 @@ export class InfoService {
|
|||||||
|
|
||||||
private async countDockerHubPulls(): Promise<number> {
|
private async countDockerHubPulls(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const { pull_count } = await got(
|
const { pull_count } = await got(
|
||||||
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
|
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
|
||||||
{
|
{
|
||||||
headers: { 'User-Agent': 'request' }
|
headers: { 'User-Agent': 'request' },
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
return pull_count;
|
return pull_count;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'InfoService');
|
Logger.error(error, 'InfoService - DockerHub');
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -189,7 +190,16 @@ export class InfoService {
|
|||||||
|
|
||||||
private async countGitHubContributors(): Promise<number> {
|
private async countGitHubContributors(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const { body } = await got('https://github.com/ghostfolio/ghostfolio');
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
|
const { body } = await got('https://github.com/ghostfolio/ghostfolio', {
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
});
|
||||||
|
|
||||||
const $ = cheerio.load(body);
|
const $ = cheerio.load(body);
|
||||||
|
|
||||||
@ -199,7 +209,7 @@ export class InfoService {
|
|||||||
).text()
|
).text()
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'InfoService');
|
Logger.error(error, 'InfoService - GitHub');
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -207,26 +217,31 @@ export class InfoService {
|
|||||||
|
|
||||||
private async countGitHubStargazers(): Promise<number> {
|
private async countGitHubStargazers(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const { stargazers_count } = await got(
|
const { stargazers_count } = await got(
|
||||||
`https://api.github.com/repos/ghostfolio/ghostfolio`,
|
`https://api.github.com/repos/ghostfolio/ghostfolio`,
|
||||||
{
|
{
|
||||||
headers: { 'User-Agent': 'request' }
|
headers: { 'User-Agent': 'request' },
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
return stargazers_count;
|
return stargazers_count;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'InfoService');
|
Logger.error(error, 'InfoService - GitHub');
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async countNewUsers(aDays: number) {
|
private async countNewUsers(aDays: number) {
|
||||||
return await this.prismaService.user.count({
|
return this.userService.count({
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc'
|
|
||||||
},
|
|
||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
{
|
{
|
||||||
@ -317,11 +332,10 @@ export class InfoService {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripeConfig = (await this.prismaService.property.findUnique({
|
return (
|
||||||
where: { key: PROPERTY_STRIPE_CONFIG }
|
((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
|
||||||
})) ?? { value: '{}' };
|
{}
|
||||||
|
);
|
||||||
return JSON.parse(stripeConfig.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUptime(): Promise<number> {
|
private async getUptime(): Promise<number> {
|
||||||
@ -331,24 +345,31 @@ export class InfoService {
|
|||||||
PROPERTY_BETTER_UPTIME_MONITOR_ID
|
PROPERTY_BETTER_UPTIME_MONITOR_ID
|
||||||
)) as string;
|
)) as string;
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const { data } = await got(
|
const { data } = await got(
|
||||||
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
||||||
subDays(new Date(), 90),
|
subDays(new Date(), 90),
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)}&to${format(new Date(), DATE_FORMAT)}`,
|
)}&to${format(new Date(), DATE_FORMAT)}`,
|
||||||
|
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.configurationService.get(
|
Authorization: `Bearer ${this.configurationService.get(
|
||||||
'BETTER_UPTIME_API_KEY'
|
'BETTER_UPTIME_API_KEY'
|
||||||
)}`
|
)}`
|
||||||
}
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
return data.attributes.availability / 100;
|
return data.attributes.availability / 100;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'InfoService');
|
Logger.error(error, 'InfoService - Better Stack');
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
|
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { HttpException, Injectable } from '@nestjs/common';
|
import { HttpException, Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
@ -41,10 +42,18 @@ export class LogoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getBuffer(aUrl: string) {
|
private getBuffer(aUrl: string) {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
return got(
|
return got(
|
||||||
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
|
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
|
||||||
{
|
{
|
||||||
headers: { 'User-Agent': 'request' }
|
headers: { 'User-Agent': 'request' },
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
}
|
}
|
||||||
).buffer();
|
).buffer();
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,9 @@ export class OrderController {
|
|||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
||||||
@Query('accounts') filterByAccounts?: string,
|
@Query('accounts') filterByAccounts?: string,
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('tags') filterByTags?: string
|
@Query('skip') skip?: number,
|
||||||
|
@Query('tags') filterByTags?: string,
|
||||||
|
@Query('take') take?: number
|
||||||
): Promise<Activities> {
|
): Promise<Activities> {
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
@ -105,6 +107,8 @@ export class OrderController {
|
|||||||
filters,
|
filters,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
includeDrafts: true,
|
includeDrafts: true,
|
||||||
|
skip: isNaN(skip) ? undefined : skip,
|
||||||
|
take: isNaN(take) ? undefined : take,
|
||||||
userId: impersonationUserId || this.request.user.id,
|
userId: impersonationUserId || this.request.user.id,
|
||||||
withExcludedAccounts: true
|
withExcludedAccounts: true
|
||||||
});
|
});
|
||||||
@ -147,8 +151,9 @@ export class OrderController {
|
|||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!order.isDraft) {
|
if (data.dataSource && !order.isDraft) {
|
||||||
// Gather symbol data in the background, if not draft
|
// Gather symbol data in the background, if data source is set
|
||||||
|
// (not MANUAL) and not draft
|
||||||
this.dataGatheringService.gatherSymbols([
|
this.dataGatheringService.gatherSymbols([
|
||||||
{
|
{
|
||||||
dataSource: data.dataSource,
|
dataSource: data.dataSource,
|
||||||
|
@ -97,7 +97,12 @@ export class OrderService {
|
|||||||
const updateAccountBalance = data.updateAccountBalance ?? false;
|
const updateAccountBalance = data.updateAccountBalance ?? false;
|
||||||
const userId = data.userId;
|
const userId = data.userId;
|
||||||
|
|
||||||
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
|
if (
|
||||||
|
data.type === 'FEE' ||
|
||||||
|
data.type === 'INTEREST' ||
|
||||||
|
data.type === 'ITEM' ||
|
||||||
|
data.type === 'LIABILITY'
|
||||||
|
) {
|
||||||
const assetClass = data.assetClass;
|
const assetClass = data.assetClass;
|
||||||
const assetSubClass = data.assetSubClass;
|
const assetSubClass = data.assetSubClass;
|
||||||
currency = data.SymbolProfile.connectOrCreate.create.currency;
|
currency = data.SymbolProfile.connectOrCreate.create.currency;
|
||||||
@ -118,20 +123,22 @@ export class OrderService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataGatheringService.addJobToQueue({
|
if (data.SymbolProfile.connectOrCreate.create.dataSource !== 'MANUAL') {
|
||||||
data: {
|
this.dataGatheringService.addJobToQueue({
|
||||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
data: {
|
||||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
|
||||||
},
|
|
||||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
|
||||||
opts: {
|
|
||||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
|
||||||
jobId: getAssetProfileIdentifier({
|
|
||||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||||
})
|
},
|
||||||
}
|
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||||
});
|
opts: {
|
||||||
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
|
jobId: getAssetProfileIdentifier({
|
||||||
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||||
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
delete data.accountId;
|
delete data.accountId;
|
||||||
delete data.assetClass;
|
delete data.assetClass;
|
||||||
@ -151,6 +158,9 @@ export class OrderService {
|
|||||||
const orderData: Prisma.OrderCreateInput = data;
|
const orderData: Prisma.OrderCreateInput = data;
|
||||||
|
|
||||||
const isDraft =
|
const isDraft =
|
||||||
|
data.type === 'FEE' ||
|
||||||
|
data.type === 'INTEREST' ||
|
||||||
|
data.type === 'ITEM' ||
|
||||||
data.type === 'LIABILITY'
|
data.type === 'LIABILITY'
|
||||||
? false
|
? false
|
||||||
: isAfter(data.date as Date, endOfToday());
|
: isAfter(data.date as Date, endOfToday());
|
||||||
@ -197,7 +207,12 @@ export class OrderService {
|
|||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
if (order.type === 'ITEM' || order.type === 'LIABILITY') {
|
if (
|
||||||
|
order.type === 'FEE' ||
|
||||||
|
order.type === 'INTEREST' ||
|
||||||
|
order.type === 'ITEM' ||
|
||||||
|
order.type === 'LIABILITY'
|
||||||
|
) {
|
||||||
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,6 +230,8 @@ export class OrderService {
|
|||||||
public async getOrders({
|
public async getOrders({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
|
skip,
|
||||||
|
take = Number.MAX_SAFE_INTEGER,
|
||||||
types,
|
types,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
@ -222,6 +239,8 @@ export class OrderService {
|
|||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
types?: TypeOfOrder[];
|
types?: TypeOfOrder[];
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -300,6 +319,8 @@ export class OrderService {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
await this.orders({
|
await this.orders({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -368,7 +389,12 @@ export class OrderService {
|
|||||||
|
|
||||||
let isDraft = false;
|
let isDraft = false;
|
||||||
|
|
||||||
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
|
if (
|
||||||
|
data.type === 'FEE' ||
|
||||||
|
data.type === 'INTEREST' ||
|
||||||
|
data.type === 'ITEM' ||
|
||||||
|
data.type === 'LIABILITY'
|
||||||
|
) {
|
||||||
delete data.SymbolProfile.connect;
|
delete data.SymbolProfile.connect;
|
||||||
} else {
|
} else {
|
||||||
delete data.SymbolProfile.update;
|
delete data.SymbolProfile.update;
|
||||||
|
@ -47,6 +47,7 @@ export class PlatformController {
|
|||||||
StatusCodes.FORBIDDEN
|
StatusCodes.FORBIDDEN
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.platformService.createPlatform(data);
|
return this.platformService.createPlatform(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,18 @@ import { Platform, Prisma } from '@prisma/client';
|
|||||||
export class PlatformService {
|
export class PlatformService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async createPlatform(data: Prisma.PlatformCreateInput) {
|
||||||
|
return this.prismaService.platform.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deletePlatform(
|
||||||
|
where: Prisma.PlatformWhereUniqueInput
|
||||||
|
): Promise<Platform> {
|
||||||
|
return this.prismaService.platform.delete({ where });
|
||||||
|
}
|
||||||
|
|
||||||
public async getPlatform(
|
public async getPlatform(
|
||||||
platformWhereUniqueInput: Prisma.PlatformWhereUniqueInput
|
platformWhereUniqueInput: Prisma.PlatformWhereUniqueInput
|
||||||
): Promise<Platform> {
|
): Promise<Platform> {
|
||||||
@ -56,12 +68,6 @@ export class PlatformService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createPlatform(data: Prisma.PlatformCreateInput) {
|
|
||||||
return this.prismaService.platform.create({
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updatePlatform({
|
public async updatePlatform({
|
||||||
data,
|
data,
|
||||||
where
|
where
|
||||||
@ -74,10 +80,4 @@ export class PlatformService {
|
|||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deletePlatform(
|
|
||||||
where: Prisma.PlatformWhereUniqueInput
|
|
||||||
): Promise<Platform> {
|
|
||||||
return this.prismaService.platform.delete({ where });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,6 @@ describe('CurrentRateService', () => {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
marketDataService = new MarketDataService(null);
|
marketDataService = new MarketDataService(null);
|
||||||
|
@ -784,7 +784,7 @@ export class PortfolioCalculator {
|
|||||||
);
|
);
|
||||||
} else if (!currentPosition.quantity.eq(0)) {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
`Missing historical market data for symbol ${currentPosition.symbol}`,
|
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
|
||||||
'PortfolioCalculator'
|
'PortfolioCalculator'
|
||||||
);
|
);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
|
@ -10,7 +10,10 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
|
|||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
HEADER_KEY_IMPERSONATION
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioDividends,
|
PortfolioDividends,
|
||||||
@ -47,8 +50,6 @@ import { PortfolioService } from './portfolio.service';
|
|||||||
|
|
||||||
@Controller('portfolio')
|
@Controller('portfolio')
|
||||||
export class PortfolioController {
|
export class PortfolioController {
|
||||||
private baseCurrency: string;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accessService: AccessService,
|
private readonly accessService: AccessService,
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
@ -57,9 +58,7 @@ export class PortfolioController {
|
|||||||
private readonly portfolioService: PortfolioService,
|
private readonly portfolioService: PortfolioService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {
|
) {}
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('details')
|
@Get('details')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
@ -174,8 +173,14 @@ export class PortfolioController {
|
|||||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||||
holdings[symbol] = {
|
holdings[symbol] = {
|
||||||
...portfolioPosition,
|
...portfolioPosition,
|
||||||
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
|
assetClass:
|
||||||
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
|
hasDetails || portfolioPosition.assetClass === 'CASH'
|
||||||
|
? portfolioPosition.assetClass
|
||||||
|
: undefined,
|
||||||
|
assetSubClass:
|
||||||
|
hasDetails || portfolioPosition.assetSubClass === 'CASH'
|
||||||
|
? portfolioPosition.assetSubClass
|
||||||
|
: undefined,
|
||||||
countries: hasDetails ? portfolioPosition.countries : [],
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||||
@ -386,12 +391,14 @@ export class PortfolioController {
|
|||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||||
@Query('accounts') filterByAccounts?: string,
|
@Query('accounts') filterByAccounts?: string,
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('query') filterBySearchQuery?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<PortfolioPositions> {
|
): Promise<PortfolioPositions> {
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
|
filterBySearchQuery,
|
||||||
filterByTags
|
filterByTags
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -442,8 +449,7 @@ export class PortfolioController {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||||
portfolioPosition.currency,
|
portfolioPosition.currency,
|
||||||
this.request.user?.Settings?.settings.baseCurrency ??
|
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY
|
||||||
this.baseCurrency
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
@ -10,13 +10,14 @@ import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rule
|
|||||||
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
||||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
|
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
|
||||||
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
|
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
|
||||||
|
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 { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
EMERGENCY_FUND_TAG_ID,
|
EMERGENCY_FUND_TAG_ID,
|
||||||
MAX_CHART_ITEMS,
|
MAX_CHART_ITEMS,
|
||||||
UNKNOWN_KEY
|
UNKNOWN_KEY
|
||||||
@ -50,18 +51,17 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
|
Type as ActivityType,
|
||||||
AssetClass,
|
AssetClass,
|
||||||
DataSource,
|
DataSource,
|
||||||
Order,
|
Order,
|
||||||
Platform,
|
Platform,
|
||||||
Prisma,
|
Prisma,
|
||||||
Tag,
|
Tag
|
||||||
Type as TypeOfOrder
|
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
endOfToday,
|
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
isBefore,
|
isBefore,
|
||||||
@ -90,11 +90,8 @@ const europeMarkets = require('../../assets/countries/europe-markets.json');
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortfolioService {
|
export class PortfolioService {
|
||||||
private baseCurrency: string;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly configurationService: ConfigurationService,
|
|
||||||
private readonly currentRateService: CurrentRateService,
|
private readonly currentRateService: CurrentRateService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
@ -104,9 +101,7 @@ export class PortfolioService {
|
|||||||
private readonly rulesService: RulesService,
|
private readonly rulesService: RulesService,
|
||||||
private readonly symbolProfileService: SymbolProfileService,
|
private readonly symbolProfileService: SymbolProfileService,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {
|
) {}
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAccounts({
|
public async getAccounts({
|
||||||
filters,
|
filters,
|
||||||
@ -470,9 +465,8 @@ export class PortfolioService {
|
|||||||
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
||||||
);
|
);
|
||||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions =
|
||||||
startDate
|
await portfolioCalculator.getCurrentPositions(startDate);
|
||||||
);
|
|
||||||
|
|
||||||
const cashDetails = await this.accountService.getCashDetails({
|
const cashDetails = await this.accountService.getCashDetails({
|
||||||
filters,
|
filters,
|
||||||
@ -810,9 +804,8 @@ export class PortfolioService {
|
|||||||
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions =
|
||||||
portfolioStart
|
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
||||||
);
|
|
||||||
|
|
||||||
const position = currentPositions.positions.find(
|
const position = currentPositions.positions.find(
|
||||||
(item) => item.symbol === aSymbol
|
(item) => item.symbol === aSymbol
|
||||||
@ -1021,6 +1014,9 @@ export class PortfolioService {
|
|||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
}): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
}): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
||||||
|
const searchQuery = filters.find(({ type }) => {
|
||||||
|
return type === 'SEARCH_QUERY';
|
||||||
|
})?.id;
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { portfolioOrders, transactionPoints } =
|
||||||
@ -1046,13 +1042,12 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions =
|
||||||
startDate
|
await portfolioCalculator.getCurrentPositions(startDate);
|
||||||
);
|
|
||||||
|
|
||||||
const positions = currentPositions.positions.filter(
|
let positions = currentPositions.positions.filter(({ quantity }) => {
|
||||||
(item) => !item.quantity.eq(0)
|
return !quantity.eq(0);
|
||||||
);
|
});
|
||||||
|
|
||||||
const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
|
const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
|
||||||
return {
|
return {
|
||||||
@ -1075,6 +1070,18 @@ export class PortfolioService {
|
|||||||
symbolProfileMap[symbolProfile.symbol] = symbolProfile;
|
symbolProfileMap[symbolProfile.symbol] = symbolProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
positions = positions.filter(({ symbol }) => {
|
||||||
|
const enhancedSymbolProfile = symbolProfileMap[symbol];
|
||||||
|
|
||||||
|
return (
|
||||||
|
enhancedSymbolProfile.isin?.toLowerCase().startsWith(searchQuery) ||
|
||||||
|
enhancedSymbolProfile.name?.toLowerCase().startsWith(searchQuery) ||
|
||||||
|
enhancedSymbolProfile.symbol?.toLowerCase().startsWith(searchQuery)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors,
|
hasErrors: currentPositions.hasErrors,
|
||||||
positions: positions.map((position) => {
|
positions: positions.map((position) => {
|
||||||
@ -1223,12 +1230,6 @@ export class PortfolioService {
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isEmpty(orders)) {
|
|
||||||
return {
|
|
||||||
rules: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
@ -1237,10 +1238,11 @@ export class PortfolioService {
|
|||||||
|
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const portfolioStart = parseDate(
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
||||||
portfolioStart
|
|
||||||
);
|
);
|
||||||
|
const currentPositions =
|
||||||
|
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
||||||
|
|
||||||
const positions = currentPositions.positions.filter(
|
const positions = currentPositions.positions.filter(
|
||||||
(item) => !item.quantity.eq(0)
|
(item) => !item.quantity.eq(0)
|
||||||
@ -1259,33 +1261,48 @@ export class PortfolioService {
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userSettings = <UserSettings>this.request.user.Settings.settings;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
accountClusterRisk: await this.rulesService.evaluate(
|
accountClusterRisk: isEmpty(orders)
|
||||||
[
|
? undefined
|
||||||
new AccountClusterRiskCurrentInvestment(
|
: await this.rulesService.evaluate(
|
||||||
this.exchangeRateDataService,
|
[
|
||||||
accounts
|
new AccountClusterRiskCurrentInvestment(
|
||||||
|
this.exchangeRateDataService,
|
||||||
|
accounts
|
||||||
|
),
|
||||||
|
new AccountClusterRiskSingleAccount(
|
||||||
|
this.exchangeRateDataService,
|
||||||
|
accounts
|
||||||
|
)
|
||||||
|
],
|
||||||
|
userSettings
|
||||||
),
|
),
|
||||||
new AccountClusterRiskSingleAccount(
|
currencyClusterRisk: isEmpty(orders)
|
||||||
|
? undefined
|
||||||
|
: await this.rulesService.evaluate(
|
||||||
|
[
|
||||||
|
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
|
||||||
|
this.exchangeRateDataService,
|
||||||
|
positions
|
||||||
|
),
|
||||||
|
new CurrencyClusterRiskCurrentInvestment(
|
||||||
|
this.exchangeRateDataService,
|
||||||
|
positions
|
||||||
|
)
|
||||||
|
],
|
||||||
|
userSettings
|
||||||
|
),
|
||||||
|
emergencyFund: await this.rulesService.evaluate(
|
||||||
|
[
|
||||||
|
new EmergencyFundSetup(
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
accounts
|
userSettings.emergencyFund
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
<UserSettings>this.request.user.Settings.settings
|
userSettings
|
||||||
),
|
|
||||||
currencyClusterRisk: await this.rulesService.evaluate(
|
|
||||||
[
|
|
||||||
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
|
|
||||||
this.exchangeRateDataService,
|
|
||||||
positions
|
|
||||||
),
|
|
||||||
new CurrencyClusterRiskCurrentInvestment(
|
|
||||||
this.exchangeRateDataService,
|
|
||||||
positions
|
|
||||||
)
|
|
||||||
],
|
|
||||||
<UserSettings>this.request.user.Settings.settings
|
|
||||||
),
|
),
|
||||||
fees: await this.rulesService.evaluate(
|
fees: await this.rulesService.evaluate(
|
||||||
[
|
[
|
||||||
@ -1295,7 +1312,7 @@ export class PortfolioService {
|
|||||||
this.getFees({ userCurrency, activities: orders }).toNumber()
|
this.getFees({ userCurrency, activities: orders }).toNumber()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
<UserSettings>this.request.user.Settings.settings
|
userSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1351,36 +1368,6 @@ export class PortfolioService {
|
|||||||
return cashPositions;
|
return cashPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDividend({
|
|
||||||
activities,
|
|
||||||
date = new Date(0),
|
|
||||||
userCurrency
|
|
||||||
}: {
|
|
||||||
activities: OrderWithAccount[];
|
|
||||||
date?: Date;
|
|
||||||
userCurrency: string;
|
|
||||||
}) {
|
|
||||||
return activities
|
|
||||||
.filter((activity) => {
|
|
||||||
// Filter out all activities before given date (drafts) and type dividend
|
|
||||||
return (
|
|
||||||
isBefore(date, new Date(activity.date)) &&
|
|
||||||
activity.type === TypeOfOrder.DIVIDEND
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
|
||||||
return this.exchangeRateDataService.toCurrency(
|
|
||||||
new Big(quantity).mul(unitPrice).toNumber(),
|
|
||||||
SymbolProfile.currency,
|
|
||||||
userCurrency
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce(
|
|
||||||
(previous, current) => new Big(previous).plus(current),
|
|
||||||
new Big(0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDividendsByGroup({
|
private getDividendsByGroup({
|
||||||
dividends,
|
dividends,
|
||||||
groupBy
|
groupBy
|
||||||
@ -1525,52 +1512,6 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getItems(activities: OrderWithAccount[], date = new Date(0)) {
|
|
||||||
return activities
|
|
||||||
.filter((activity) => {
|
|
||||||
// Filter out all activities before given date (drafts) and type item
|
|
||||||
return (
|
|
||||||
isBefore(date, new Date(activity.date)) &&
|
|
||||||
activity.type === TypeOfOrder.ITEM
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
|
||||||
return this.exchangeRateDataService.toCurrency(
|
|
||||||
new Big(quantity).mul(unitPrice).toNumber(),
|
|
||||||
SymbolProfile.currency,
|
|
||||||
this.request.user.Settings.settings.baseCurrency
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce(
|
|
||||||
(previous, current) => new Big(previous).plus(current),
|
|
||||||
new Big(0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLiabilities({
|
|
||||||
activities,
|
|
||||||
userCurrency
|
|
||||||
}: {
|
|
||||||
activities: OrderWithAccount[];
|
|
||||||
userCurrency: string;
|
|
||||||
}) {
|
|
||||||
return activities
|
|
||||||
.filter(({ type }) => {
|
|
||||||
return type === TypeOfOrder.LIABILITY;
|
|
||||||
})
|
|
||||||
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
|
||||||
return this.exchangeRateDataService.toCurrency(
|
|
||||||
new Big(quantity).mul(unitPrice).toNumber(),
|
|
||||||
SymbolProfile.currency,
|
|
||||||
userCurrency
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce(
|
|
||||||
(previous, current) => new Big(previous).plus(current),
|
|
||||||
new Big(0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
|
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
|
||||||
switch (aDateRange) {
|
switch (aDateRange) {
|
||||||
case '1d':
|
case '1d':
|
||||||
@ -1659,9 +1600,10 @@ export class PortfolioService {
|
|||||||
return account?.isExcluded ?? false;
|
return account?.isExcluded ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const dividend = this.getDividend({
|
const dividend = this.getSumOfActivityType({
|
||||||
activities,
|
activities,
|
||||||
userCurrency
|
userCurrency,
|
||||||
|
activityType: 'DIVIDEND'
|
||||||
}).toNumber();
|
}).toNumber();
|
||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
Math.max(
|
Math.max(
|
||||||
@ -1671,23 +1613,49 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
||||||
const firstOrderDate = activities[0]?.date;
|
const firstOrderDate = activities[0]?.date;
|
||||||
const items = this.getItems(activities).toNumber();
|
const interest = this.getSumOfActivityType({
|
||||||
const liabilities = this.getLiabilities({
|
|
||||||
activities,
|
activities,
|
||||||
userCurrency
|
userCurrency,
|
||||||
|
activityType: 'INTEREST'
|
||||||
|
}).toNumber();
|
||||||
|
const items = this.getSumOfActivityType({
|
||||||
|
activities,
|
||||||
|
userCurrency,
|
||||||
|
activityType: 'ITEM'
|
||||||
|
}).toNumber();
|
||||||
|
const liabilities = this.getSumOfActivityType({
|
||||||
|
activities,
|
||||||
|
userCurrency,
|
||||||
|
activityType: 'LIABILITY'
|
||||||
}).toNumber();
|
}).toNumber();
|
||||||
|
|
||||||
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY');
|
const totalBuy = this.getSumOfActivityType({
|
||||||
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
|
activities,
|
||||||
|
userCurrency,
|
||||||
|
activityType: 'BUY'
|
||||||
|
}).toNumber();
|
||||||
|
const totalSell = this.getSumOfActivityType({
|
||||||
|
activities,
|
||||||
|
userCurrency,
|
||||||
|
activityType: 'SELL'
|
||||||
|
}).toNumber();
|
||||||
|
|
||||||
const cash = new Big(balanceInBaseCurrency)
|
const cash = new Big(balanceInBaseCurrency)
|
||||||
.minus(emergencyFund)
|
.minus(emergencyFund)
|
||||||
.plus(emergencyFundPositionsValueInBaseCurrency)
|
.plus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
const committedFunds = new Big(totalBuy).minus(totalSell);
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
||||||
const totalOfExcludedActivities = new Big(
|
const totalOfExcludedActivities = this.getSumOfActivityType({
|
||||||
this.getTotalByType(excludedActivities, userCurrency, 'BUY')
|
userCurrency,
|
||||||
).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL'));
|
activities: excludedActivities,
|
||||||
|
activityType: 'BUY'
|
||||||
|
}).minus(
|
||||||
|
this.getSumOfActivityType({
|
||||||
|
userCurrency,
|
||||||
|
activities: excludedActivities,
|
||||||
|
activityType: 'SELL'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const cashDetailsWithExcludedAccounts =
|
const cashDetailsWithExcludedAccounts =
|
||||||
await this.accountService.getCashDetails({
|
await this.accountService.getCashDetails({
|
||||||
@ -1734,6 +1702,7 @@ export class PortfolioService {
|
|||||||
excludedAccountsAndActivities,
|
excludedAccountsAndActivities,
|
||||||
fees,
|
fees,
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
|
interest,
|
||||||
items,
|
items,
|
||||||
liabilities,
|
liabilities,
|
||||||
netWorth,
|
netWorth,
|
||||||
@ -1756,6 +1725,39 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSumOfActivityType({
|
||||||
|
activities,
|
||||||
|
activityType,
|
||||||
|
date = new Date(0),
|
||||||
|
userCurrency
|
||||||
|
}: {
|
||||||
|
activities: OrderWithAccount[];
|
||||||
|
activityType: ActivityType;
|
||||||
|
date?: Date;
|
||||||
|
userCurrency: string;
|
||||||
|
}) {
|
||||||
|
return activities
|
||||||
|
.filter((activity) => {
|
||||||
|
// Filter out all activities before given date (drafts) and
|
||||||
|
// activity type
|
||||||
|
return (
|
||||||
|
isBefore(date, new Date(activity.date)) &&
|
||||||
|
activity.type === activityType
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
||||||
|
return this.exchangeRateDataService.toCurrency(
|
||||||
|
new Big(quantity).mul(unitPrice).toNumber(),
|
||||||
|
SymbolProfile.currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(previous, current) => new Big(previous).plus(current),
|
||||||
|
new Big(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async getTransactionPoints({
|
private async getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
@ -1772,7 +1774,7 @@ export class PortfolioService {
|
|||||||
portfolioOrders: PortfolioOrder[];
|
portfolioOrders: PortfolioOrder[];
|
||||||
}> {
|
}> {
|
||||||
const userCurrency =
|
const userCurrency =
|
||||||
this.request.user?.Settings?.settings.baseCurrency ?? this.baseCurrency;
|
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
|
||||||
|
|
||||||
const orders = await this.orderService.getOrders({
|
const orders = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
@ -1827,6 +1829,21 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUserCurrency(aUser: UserWithSettings) {
|
||||||
|
return (
|
||||||
|
aUser.Settings?.settings.baseCurrency ??
|
||||||
|
this.request.user?.Settings?.settings.baseCurrency ??
|
||||||
|
DEFAULT_CURRENCY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUserId(aImpersonationId: string, aUserId: string) {
|
||||||
|
const impersonationUserId =
|
||||||
|
await this.impersonationService.validateImpersonationId(aImpersonationId);
|
||||||
|
|
||||||
|
return impersonationUserId || aUserId;
|
||||||
|
}
|
||||||
|
|
||||||
private async getValueOfAccountsAndPlatforms({
|
private async getValueOfAccountsAndPlatforms({
|
||||||
filters = [],
|
filters = [],
|
||||||
orders,
|
orders,
|
||||||
@ -1970,38 +1987,4 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return { accounts, platforms };
|
return { accounts, platforms };
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTotalByType(
|
|
||||||
orders: OrderWithAccount[],
|
|
||||||
currency: string,
|
|
||||||
type: TypeOfOrder
|
|
||||||
) {
|
|
||||||
return orders
|
|
||||||
.filter(
|
|
||||||
(order) => !isAfter(order.date, endOfToday()) && order.type === type
|
|
||||||
)
|
|
||||||
.map((order) => {
|
|
||||||
return this.exchangeRateDataService.toCurrency(
|
|
||||||
order.quantity * order.unitPrice,
|
|
||||||
order.SymbolProfile.currency,
|
|
||||||
currency
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce((previous, current) => previous + current, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUserCurrency(aUser: UserWithSettings) {
|
|
||||||
return (
|
|
||||||
aUser.Settings?.settings.baseCurrency ??
|
|
||||||
this.request.user?.Settings?.settings.baseCurrency ??
|
|
||||||
this.baseCurrency
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getUserId(aImpersonationId: string, aUserId: string) {
|
|
||||||
const impersonationUserId =
|
|
||||||
await this.impersonationService.validateImpersonationId(aImpersonationId);
|
|
||||||
|
|
||||||
return impersonationUserId || aUserId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -93,9 +93,8 @@ export class SubscriptionService {
|
|||||||
|
|
||||||
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
|
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
|
||||||
try {
|
try {
|
||||||
const session = await this.stripe.checkout.sessions.retrieve(
|
const session =
|
||||||
aCheckoutSessionId
|
await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId);
|
||||||
);
|
|
||||||
|
|
||||||
await this.createSubscription({
|
await this.createSubscription({
|
||||||
price: session.amount_total / 100,
|
price: session.amount_total / 100,
|
||||||
|
@ -15,13 +15,13 @@ import {
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
import { isDate, isEmpty } from 'lodash';
|
import { isDate, isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||||
import { SymbolItem } from './interfaces/symbol-item.interface';
|
import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||||
import { SymbolService } from './symbol.service';
|
import { SymbolService } from './symbol.service';
|
||||||
import { parseISO } from 'date-fns';
|
|
||||||
|
|
||||||
@Controller('symbol')
|
@Controller('symbol')
|
||||||
export class SymbolController {
|
export class SymbolController {
|
||||||
|
6
apps/api/src/app/tag/create-tag.dto.ts
Normal file
6
apps/api/src/app/tag/create-tag.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateTagDto {
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
}
|
104
apps/api/src/app/tag/tag.controller.ts
Normal file
104
apps/api/src/app/tag/tag.controller.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Tag } from '@prisma/client';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
import { CreateTagDto } from './create-tag.dto';
|
||||||
|
import { TagService } from './tag.service';
|
||||||
|
import { UpdateTagDto } from './update-tag.dto';
|
||||||
|
|
||||||
|
@Controller('tag')
|
||||||
|
export class TagController {
|
||||||
|
public constructor(
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
|
private readonly tagService: TagService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getTags() {
|
||||||
|
return this.tagService.getTagsWithActivityCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async createTag(@Body() data: CreateTagDto): Promise<Tag> {
|
||||||
|
if (!hasPermission(this.request.user.permissions, permissions.createTag)) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tagService.createTag(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) {
|
||||||
|
if (!hasPermission(this.request.user.permissions, permissions.updateTag)) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalTag = await this.tagService.getTag({
|
||||||
|
id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!originalTag) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tagService.updateTag({
|
||||||
|
data: {
|
||||||
|
...data
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteTag(@Param('id') id: string) {
|
||||||
|
if (!hasPermission(this.request.user.permissions, permissions.deleteTag)) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalTag = await this.tagService.getTag({
|
||||||
|
id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!originalTag) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tagService.deleteTag({ id });
|
||||||
|
}
|
||||||
|
}
|
13
apps/api/src/app/tag/tag.module.ts
Normal file
13
apps/api/src/app/tag/tag.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TagController } from './tag.controller';
|
||||||
|
import { TagService } from './tag.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [TagController],
|
||||||
|
exports: [TagService],
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [TagService]
|
||||||
|
})
|
||||||
|
export class TagModule {}
|
79
apps/api/src/app/tag/tag.service.ts
Normal file
79
apps/api/src/app/tag/tag.service.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma, Tag } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TagService {
|
||||||
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async createTag(data: Prisma.TagCreateInput) {
|
||||||
|
return this.prismaService.tag.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> {
|
||||||
|
return this.prismaService.tag.delete({ where });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTag(
|
||||||
|
tagWhereUniqueInput: Prisma.TagWhereUniqueInput
|
||||||
|
): Promise<Tag> {
|
||||||
|
return this.prismaService.tag.findUnique({
|
||||||
|
where: tagWhereUniqueInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTags({
|
||||||
|
cursor,
|
||||||
|
orderBy,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
where
|
||||||
|
}: {
|
||||||
|
cursor?: Prisma.TagWhereUniqueInput;
|
||||||
|
orderBy?: Prisma.TagOrderByWithRelationInput;
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
where?: Prisma.TagWhereInput;
|
||||||
|
} = {}) {
|
||||||
|
return this.prismaService.tag.findMany({
|
||||||
|
cursor,
|
||||||
|
orderBy,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTagsWithActivityCount() {
|
||||||
|
const tagsWithOrderCount = await this.prismaService.tag.findMany({
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: { orders: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tagsWithOrderCount.map(({ _count, id, name }) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
activityCount: _count.orders
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateTag({
|
||||||
|
data,
|
||||||
|
where
|
||||||
|
}: {
|
||||||
|
data: Prisma.TagUpdateInput;
|
||||||
|
where: Prisma.TagWhereUniqueInput;
|
||||||
|
}): Promise<Tag> {
|
||||||
|
return this.prismaService.tag.update({
|
||||||
|
data,
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
9
apps/api/src/app/tag/update-tag.dto.ts
Normal file
9
apps/api/src/app/tag/update-tag.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateTagDto {
|
||||||
|
@IsString()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
}
|
@ -19,22 +19,22 @@ import { UserWithSettings } from '@ghostfolio/common/types';
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma, Role, User } from '@prisma/client';
|
import { Prisma, Role, User } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy, without } from 'lodash';
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private baseCurrency: string;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly subscriptionService: SubscriptionService,
|
private readonly subscriptionService: SubscriptionService,
|
||||||
private readonly tagService: TagService
|
private readonly tagService: TagService
|
||||||
) {
|
) {}
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
|
public async count(args?: Prisma.UserCountArgs) {
|
||||||
|
return this.prismaService.user.count(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUser(
|
public async getUser(
|
||||||
@ -163,6 +163,13 @@ export class UserService {
|
|||||||
|
|
||||||
let currentPermissions = getPermissions(user.role);
|
let currentPermissions = getPermissions(user.role);
|
||||||
|
|
||||||
|
if (!(user.Settings.settings as UserSettings).isExperimentalFeatures) {
|
||||||
|
currentPermissions = without(
|
||||||
|
currentPermissions,
|
||||||
|
permissions.accessAssistant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
user.subscription =
|
user.subscription =
|
||||||
this.subscriptionService.getSubscription(Subscription);
|
this.subscriptionService.getSubscription(Subscription);
|
||||||
@ -188,6 +195,11 @@ export class UserService {
|
|||||||
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPermissions = without(
|
||||||
|
currentPermissions,
|
||||||
|
permissions.createAccess
|
||||||
|
);
|
||||||
|
|
||||||
// Reset benchmark
|
// Reset benchmark
|
||||||
user.Settings.settings.benchmark = undefined;
|
user.Settings.settings.benchmark = undefined;
|
||||||
}
|
}
|
||||||
@ -267,7 +279,7 @@ export class UserService {
|
|||||||
...data,
|
...data,
|
||||||
Account: {
|
Account: {
|
||||||
create: {
|
create: {
|
||||||
currency: this.baseCurrency,
|
currency: DEFAULT_CURRENCY,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
name: 'Default Account'
|
name: 'Default Account'
|
||||||
}
|
}
|
||||||
@ -275,7 +287,7 @@ export class UserService {
|
|||||||
Settings: {
|
Settings: {
|
||||||
create: {
|
create: {
|
||||||
settings: {
|
settings: {
|
||||||
currency: this.baseCurrency
|
currency: DEFAULT_CURRENCY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,9 @@
|
|||||||
"3FT": "ThreeFold Token",
|
"3FT": "ThreeFold Token",
|
||||||
"3ULL": "3ULL Coin",
|
"3ULL": "3ULL Coin",
|
||||||
"3XD": "3DChain",
|
"3XD": "3DChain",
|
||||||
|
"420CHAN": "420chan",
|
||||||
"4ART": "4ART Coin",
|
"4ART": "4ART Coin",
|
||||||
|
"4CHAN": "4Chan",
|
||||||
"4JNET": "4JNET",
|
"4JNET": "4JNET",
|
||||||
"77G": "GraphenTech",
|
"77G": "GraphenTech",
|
||||||
"7E": "7ELEVEN",
|
"7E": "7ELEVEN",
|
||||||
@ -60,6 +62,7 @@
|
|||||||
"8BT": "8 Circuit Studios",
|
"8BT": "8 Circuit Studios",
|
||||||
"8PAY": "8Pay",
|
"8PAY": "8Pay",
|
||||||
"8X8": "8X8 Protocol",
|
"8X8": "8X8 Protocol",
|
||||||
|
"9GAG": "9GAG",
|
||||||
"A5T": "Alpha5",
|
"A5T": "Alpha5",
|
||||||
"AAA": "Moon Rabbit",
|
"AAA": "Moon Rabbit",
|
||||||
"AAB": "AAX Token",
|
"AAB": "AAX Token",
|
||||||
@ -101,6 +104,7 @@
|
|||||||
"ACN": "AvonCoin",
|
"ACN": "AvonCoin",
|
||||||
"ACOIN": "ACoin",
|
"ACOIN": "ACoin",
|
||||||
"ACP": "Anarchists Prime",
|
"ACP": "Anarchists Prime",
|
||||||
|
"ACQ": "Acquire.Fi",
|
||||||
"ACS": "Access Protocol",
|
"ACS": "Access Protocol",
|
||||||
"ACT": "Achain",
|
"ACT": "Achain",
|
||||||
"ACTIN": "Actinium",
|
"ACTIN": "Actinium",
|
||||||
@ -180,7 +184,7 @@
|
|||||||
"AGX": "Agricoin",
|
"AGX": "Agricoin",
|
||||||
"AHOO": "Ahoolee",
|
"AHOO": "Ahoolee",
|
||||||
"AHT": "AhaToken",
|
"AHT": "AhaToken",
|
||||||
"AI": "Multiverse",
|
"AI": "AiDoge",
|
||||||
"AIB": "AdvancedInternetBlock",
|
"AIB": "AdvancedInternetBlock",
|
||||||
"AIBB": "AiBB",
|
"AIBB": "AiBB",
|
||||||
"AIBK": "AIB Utility Token",
|
"AIBK": "AIB Utility Token",
|
||||||
@ -213,6 +217,7 @@
|
|||||||
"AKA": "Akroma",
|
"AKA": "Akroma",
|
||||||
"AKITA": "Akita Inu",
|
"AKITA": "Akita Inu",
|
||||||
"AKN": "Akoin",
|
"AKN": "Akoin",
|
||||||
|
"AKNC": "Aave KNC v1",
|
||||||
"AKRO": "Akropolis",
|
"AKRO": "Akropolis",
|
||||||
"AKT": "Akash Network",
|
"AKT": "Akash Network",
|
||||||
"AKTIO": "AKTIO Coin",
|
"AKTIO": "AKTIO Coin",
|
||||||
@ -237,12 +242,14 @@
|
|||||||
"ALIC": "AliCoin",
|
"ALIC": "AliCoin",
|
||||||
"ALICE": "My Neighbor Alice",
|
"ALICE": "My Neighbor Alice",
|
||||||
"ALIEN": "AlienCoin",
|
"ALIEN": "AlienCoin",
|
||||||
|
"ALINK": "Aave LINK v1",
|
||||||
"ALIS": "ALISmedia",
|
"ALIS": "ALISmedia",
|
||||||
"ALITA": "Alita Network",
|
"ALITA": "Alita Network",
|
||||||
"ALIX": "AlinX",
|
"ALIX": "AlinX",
|
||||||
"ALKI": "Alkimi",
|
"ALKI": "Alkimi",
|
||||||
"ALLBI": "ALL BEST ICO",
|
"ALLBI": "ALL BEST ICO",
|
||||||
"ALLEY": "NFT Alley",
|
"ALLEY": "NFT Alley",
|
||||||
|
"ALLIN": "All in",
|
||||||
"ALN": "Aluna",
|
"ALN": "Aluna",
|
||||||
"ALOHA": "Aloha",
|
"ALOHA": "Aloha",
|
||||||
"ALP": "Alphacon",
|
"ALP": "Alphacon",
|
||||||
@ -410,12 +417,14 @@
|
|||||||
"ARIX": "Arix",
|
"ARIX": "Arix",
|
||||||
"ARK": "ARK",
|
"ARK": "ARK",
|
||||||
"ARKER": "Arker",
|
"ARKER": "Arker",
|
||||||
|
"ARKM": "Arkham",
|
||||||
"ARKN": "Ark Rivals",
|
"ARKN": "Ark Rivals",
|
||||||
"ARM": "Armory Coin",
|
"ARM": "Armory Coin",
|
||||||
"ARMOR": "ARMOR",
|
"ARMOR": "ARMOR",
|
||||||
"ARMR": "ARMR",
|
"ARMR": "ARMR",
|
||||||
"ARMS": "2Acoin",
|
"ARMS": "2Acoin",
|
||||||
"ARNA": "ARNA Panacea",
|
"ARNA": "ARNA Panacea",
|
||||||
|
"ARNM": "Arenum",
|
||||||
"ARNO": "ARNO",
|
"ARNO": "ARNO",
|
||||||
"ARNX": "Aeron",
|
"ARNX": "Aeron",
|
||||||
"ARNXM": "Armor NXM",
|
"ARNXM": "Armor NXM",
|
||||||
@ -472,6 +481,7 @@
|
|||||||
"ASTO": "Altered State Token",
|
"ASTO": "Altered State Token",
|
||||||
"ASTON": "Aston",
|
"ASTON": "Aston",
|
||||||
"ASTR": "Astar",
|
"ASTR": "Astar",
|
||||||
|
"ASTRAFER": "Astrafer",
|
||||||
"ASTRAL": "Astral",
|
"ASTRAL": "Astral",
|
||||||
"ASTRO": "AstroSwap",
|
"ASTRO": "AstroSwap",
|
||||||
"ASTROC": "Astroport Classic",
|
"ASTROC": "Astroport Classic",
|
||||||
@ -531,6 +541,7 @@
|
|||||||
"AURY": "Aurory",
|
"AURY": "Aurory",
|
||||||
"AUSCM": "Auric Network",
|
"AUSCM": "Auric Network",
|
||||||
"AUSD": "Appeal dollar",
|
"AUSD": "Appeal dollar",
|
||||||
|
"AUSDC": "Aave USDC v1",
|
||||||
"AUT": "Autoria",
|
"AUT": "Autoria",
|
||||||
"AUTHORSHIP": "Authorship",
|
"AUTHORSHIP": "Authorship",
|
||||||
"AUTO": "Auto",
|
"AUTO": "Auto",
|
||||||
@ -612,6 +623,7 @@
|
|||||||
"BACK": "DollarBack",
|
"BACK": "DollarBack",
|
||||||
"BACOIN": "BACoin",
|
"BACOIN": "BACoin",
|
||||||
"BACON": "BaconDAO (BACON)",
|
"BACON": "BaconDAO (BACON)",
|
||||||
|
"BAD": "Bad Idea AI",
|
||||||
"BADGER": "Badger DAO",
|
"BADGER": "Badger DAO",
|
||||||
"BAG": "BondAppetit",
|
"BAG": "BondAppetit",
|
||||||
"BAGS": "Basis Gold Share",
|
"BAGS": "Basis Gold Share",
|
||||||
@ -662,6 +674,7 @@
|
|||||||
"BBCT": "TraDove B2BCoin",
|
"BBCT": "TraDove B2BCoin",
|
||||||
"BBDT": "BBD Token",
|
"BBDT": "BBD Token",
|
||||||
"BBF": "Bubblefong",
|
"BBF": "Bubblefong",
|
||||||
|
"BBFT": "Block Busters Tech Token",
|
||||||
"BBG": "BigBang",
|
"BBG": "BigBang",
|
||||||
"BBGC": "BigBang Game",
|
"BBGC": "BigBang Game",
|
||||||
"BBI": "BelugaPay",
|
"BBI": "BelugaPay",
|
||||||
@ -725,6 +738,7 @@
|
|||||||
"BDX": "Beldex",
|
"BDX": "Beldex",
|
||||||
"BDY": "Buddy DAO",
|
"BDY": "Buddy DAO",
|
||||||
"BEACH": "BeachCoin",
|
"BEACH": "BeachCoin",
|
||||||
|
"BEAI": "BeNFT Solutions",
|
||||||
"BEAM": "Beam",
|
"BEAM": "Beam",
|
||||||
"BEAN": "BeanCash",
|
"BEAN": "BeanCash",
|
||||||
"BEAST": "CryptoBeast",
|
"BEAST": "CryptoBeast",
|
||||||
@ -806,6 +820,7 @@
|
|||||||
"BIDR": "Binance IDR Stable Coin",
|
"BIDR": "Binance IDR Stable Coin",
|
||||||
"BIFI": "Beefy.Finance",
|
"BIFI": "Beefy.Finance",
|
||||||
"BIFIF": "BiFi",
|
"BIFIF": "BiFi",
|
||||||
|
"BIG": "Big Eyes",
|
||||||
"BIGHAN": "BighanCoin",
|
"BIGHAN": "BighanCoin",
|
||||||
"BIGSB": "BigShortBets",
|
"BIGSB": "BigShortBets",
|
||||||
"BIGUP": "BigUp",
|
"BIGUP": "BigUp",
|
||||||
@ -1090,6 +1105,7 @@
|
|||||||
"BRNK": "Brank",
|
"BRNK": "Brank",
|
||||||
"BRNX": "Bronix",
|
"BRNX": "Bronix",
|
||||||
"BRO": "Bitradio",
|
"BRO": "Bitradio",
|
||||||
|
"BROCK": "Bitrock",
|
||||||
"BRONZ": "BitBronze",
|
"BRONZ": "BitBronze",
|
||||||
"BRT": "Bikerush",
|
"BRT": "Bikerush",
|
||||||
"BRTR": "Barter",
|
"BRTR": "Barter",
|
||||||
@ -1226,7 +1242,7 @@
|
|||||||
"BULL": "Bullieverse",
|
"BULL": "Bullieverse",
|
||||||
"BULLC": "BuySell",
|
"BULLC": "BuySell",
|
||||||
"BULLION": "BullionFX",
|
"BULLION": "BullionFX",
|
||||||
"BULLS": "BullshitCoin",
|
"BULLS": "Bull Coin",
|
||||||
"BULLSH": "Bullshit Inu",
|
"BULLSH": "Bullshit Inu",
|
||||||
"BUMN": "BUMooN",
|
"BUMN": "BUMooN",
|
||||||
"BUMP": "Bumper",
|
"BUMP": "Bumper",
|
||||||
@ -1277,6 +1293,7 @@
|
|||||||
"BZKY": "Bizkey",
|
"BZKY": "Bizkey",
|
||||||
"BZL": "BZLCoin",
|
"BZL": "BZLCoin",
|
||||||
"BZNT": "Bezant",
|
"BZNT": "Bezant",
|
||||||
|
"BZR": "Bazaars",
|
||||||
"BZRX": "bZx Protocol",
|
"BZRX": "bZx Protocol",
|
||||||
"BZX": "Bitcoin Zero",
|
"BZX": "Bitcoin Zero",
|
||||||
"BZZ": "Swarmv",
|
"BZZ": "Swarmv",
|
||||||
@ -1319,8 +1336,10 @@
|
|||||||
"CAP": "BottleCaps",
|
"CAP": "BottleCaps",
|
||||||
"CAPD": "Capdax",
|
"CAPD": "Capdax",
|
||||||
"CAPP": "Cappasity",
|
"CAPP": "Cappasity",
|
||||||
|
"CAPRICOIN": "CapriCoin",
|
||||||
"CAPS": "Ternoa",
|
"CAPS": "Ternoa",
|
||||||
"CAPT": "Bitcoin Captain",
|
"CAPT": "Bitcoin Captain",
|
||||||
|
"CAPTAINPLANET": "Captain Planet",
|
||||||
"CAR": "CarBlock",
|
"CAR": "CarBlock",
|
||||||
"CARAT": "Carats Token",
|
"CARAT": "Carats Token",
|
||||||
"CARBON": "Carboncoin",
|
"CARBON": "Carboncoin",
|
||||||
@ -1478,6 +1497,7 @@
|
|||||||
"CHECKR": "CheckerChain",
|
"CHECKR": "CheckerChain",
|
||||||
"CHECOIN": "CheCoin",
|
"CHECOIN": "CheCoin",
|
||||||
"CHEDDA": "Chedda",
|
"CHEDDA": "Chedda",
|
||||||
|
"CHEEL": "Cheelee",
|
||||||
"CHEESE": "CHEESE",
|
"CHEESE": "CHEESE",
|
||||||
"CHEESUS": "Cheesus",
|
"CHEESUS": "Cheesus",
|
||||||
"CHEQ": "CHEQD Network",
|
"CHEQ": "CHEQD Network",
|
||||||
@ -1520,7 +1540,8 @@
|
|||||||
"CHX": "Own",
|
"CHX": "Own",
|
||||||
"CHY": "Concern Poverty Chain",
|
"CHY": "Concern Poverty Chain",
|
||||||
"CHZ": "Chiliz",
|
"CHZ": "Chiliz",
|
||||||
"CIC": "CIChain",
|
"CIC": "Crazy Internet Coin",
|
||||||
|
"CICHAIN": "CIChain",
|
||||||
"CIF": "Crypto Improvement Fund",
|
"CIF": "Crypto Improvement Fund",
|
||||||
"CIM": "COINCOME",
|
"CIM": "COINCOME",
|
||||||
"CIN": "CinderCoin",
|
"CIN": "CinderCoin",
|
||||||
@ -1630,7 +1651,6 @@
|
|||||||
"COB": "Cobinhood",
|
"COB": "Cobinhood",
|
||||||
"COC": "Coin of the champions",
|
"COC": "Coin of the champions",
|
||||||
"COCK": "Shibacock",
|
"COCK": "Shibacock",
|
||||||
"COCOS": "COCOS BCX",
|
|
||||||
"CODEO": "Codeo Token",
|
"CODEO": "Codeo Token",
|
||||||
"CODEX": "CODEX Finance",
|
"CODEX": "CODEX Finance",
|
||||||
"CODI": "Codi Finance",
|
"CODI": "Codi Finance",
|
||||||
@ -1659,7 +1679,7 @@
|
|||||||
"COLX": "ColossusCoinXT",
|
"COLX": "ColossusCoinXT",
|
||||||
"COM": "Coliseum",
|
"COM": "Coliseum",
|
||||||
"COMB": "Combo",
|
"COMB": "Combo",
|
||||||
"COMBO": "Furucombo",
|
"COMBO": "COMBO",
|
||||||
"COMFI": "CompliFi",
|
"COMFI": "CompliFi",
|
||||||
"COMM": "Community Coin",
|
"COMM": "Community Coin",
|
||||||
"COMMUNITYCOIN": "Community Coin",
|
"COMMUNITYCOIN": "Community Coin",
|
||||||
@ -1672,7 +1692,6 @@
|
|||||||
"CONI": "CoinBene",
|
"CONI": "CoinBene",
|
||||||
"CONS": "ConSpiracy Coin",
|
"CONS": "ConSpiracy Coin",
|
||||||
"CONSENTIUM": "Consentium",
|
"CONSENTIUM": "Consentium",
|
||||||
"CONT": "Contentos",
|
|
||||||
"CONUN": "CONUN",
|
"CONUN": "CONUN",
|
||||||
"CONV": "Convergence",
|
"CONV": "Convergence",
|
||||||
"COOK": "Cook",
|
"COOK": "Cook",
|
||||||
@ -1683,17 +1702,19 @@
|
|||||||
"COPS": "Cops Finance",
|
"COPS": "Cops Finance",
|
||||||
"COR": "Corion",
|
"COR": "Corion",
|
||||||
"CORAL": "CoralPay",
|
"CORAL": "CoralPay",
|
||||||
"CORE": "Coreum",
|
"CORE": "Core",
|
||||||
"COREDAO": "coreDAO",
|
"COREDAO": "coreDAO",
|
||||||
"COREG": "Core Group Asset",
|
"COREG": "Core Group Asset",
|
||||||
|
"COREUM": "Coreum",
|
||||||
"CORGI": "Corgi Inu",
|
"CORGI": "Corgi Inu",
|
||||||
"CORN": "CORN",
|
"CORN": "CORN",
|
||||||
"CORX": "CorionX",
|
"CORX": "CorionX",
|
||||||
"COS": "COS",
|
"COS": "Contentos",
|
||||||
"COSHI": "CoShi Inu",
|
"COSHI": "CoShi Inu",
|
||||||
"COSM": "CosmoChain",
|
"COSM": "CosmoChain",
|
||||||
"COSMIC": "CosmicSwap",
|
"COSMIC": "CosmicSwap",
|
||||||
"COSP": "Cosplay Token",
|
"COSP": "Cosplay Token",
|
||||||
|
"COSS": "COS",
|
||||||
"COSX": "Cosmecoin",
|
"COSX": "Cosmecoin",
|
||||||
"COT": "CoTrader",
|
"COT": "CoTrader",
|
||||||
"COTI": "COTI",
|
"COTI": "COTI",
|
||||||
@ -1729,7 +1750,7 @@
|
|||||||
"CPOOL": "Clearpool",
|
"CPOOL": "Clearpool",
|
||||||
"CPROP": "CPROP",
|
"CPROP": "CPROP",
|
||||||
"CPRX": "Crypto Perx",
|
"CPRX": "Crypto Perx",
|
||||||
"CPS": "CapriCoin",
|
"CPS": "Cryptostone",
|
||||||
"CPT": "Cryptaur",
|
"CPT": "Cryptaur",
|
||||||
"CPU": "CPUcoin",
|
"CPU": "CPUcoin",
|
||||||
"CPX": "Apex Token",
|
"CPX": "Apex Token",
|
||||||
@ -1796,6 +1817,7 @@
|
|||||||
"CRTS": "Cratos",
|
"CRTS": "Cratos",
|
||||||
"CRU": "Crust Network",
|
"CRU": "Crust Network",
|
||||||
"CRV": "Curve DAO Token",
|
"CRV": "Curve DAO Token",
|
||||||
|
"CRVUSD": "crvUSD",
|
||||||
"CRW": "Crown Coin",
|
"CRW": "Crown Coin",
|
||||||
"CRWD": "CRWD Network",
|
"CRWD": "CRWD Network",
|
||||||
"CRWNY": "Crowny Token",
|
"CRWNY": "Crowny Token",
|
||||||
@ -1843,7 +1865,7 @@
|
|||||||
"CTLX": "Cash Telex",
|
"CTLX": "Cash Telex",
|
||||||
"CTN": "Continuum Finance",
|
"CTN": "Continuum Finance",
|
||||||
"CTO": "Crypto",
|
"CTO": "Crypto",
|
||||||
"CTP": "Captain Planet",
|
"CTP": "Ctomorrow Platform",
|
||||||
"CTPL": "Cultiplan",
|
"CTPL": "Cultiplan",
|
||||||
"CTPT": "Contents Protocol",
|
"CTPT": "Contents Protocol",
|
||||||
"CTR": "Creator Platform",
|
"CTR": "Creator Platform",
|
||||||
@ -2007,6 +2029,7 @@
|
|||||||
"DBC": "DeepBrain Chain",
|
"DBC": "DeepBrain Chain",
|
||||||
"DBCCOIN": "Datablockchain",
|
"DBCCOIN": "Datablockchain",
|
||||||
"DBD": "Day By Day",
|
"DBD": "Day By Day",
|
||||||
|
"DBEAR": "DBear Coin",
|
||||||
"DBET": "Decent.bet",
|
"DBET": "Decent.bet",
|
||||||
"DBIC": "DubaiCoin",
|
"DBIC": "DubaiCoin",
|
||||||
"DBIX": "DubaiCoin",
|
"DBIX": "DubaiCoin",
|
||||||
@ -2058,6 +2081,7 @@
|
|||||||
"DEEP": "DeepCloud AI",
|
"DEEP": "DeepCloud AI",
|
||||||
"DEEPG": "Deep Gold",
|
"DEEPG": "Deep Gold",
|
||||||
"DEEX": "DEEX",
|
"DEEX": "DEEX",
|
||||||
|
"DEEZ": "DEEZ NUTS",
|
||||||
"DEFI": "Defi",
|
"DEFI": "Defi",
|
||||||
"DEFI5": "DEFI Top 5 Tokens Index",
|
"DEFI5": "DEFI Top 5 Tokens Index",
|
||||||
"DEFIL": "DeFIL",
|
"DEFIL": "DeFIL",
|
||||||
@ -2162,11 +2186,12 @@
|
|||||||
"DIEM": "Facebook Diem",
|
"DIEM": "Facebook Diem",
|
||||||
"DIESEL": "Diesel",
|
"DIESEL": "Diesel",
|
||||||
"DIFX": "Digital Financial Exchange",
|
"DIFX": "Digital Financial Exchange",
|
||||||
"DIG": "Dignity",
|
"DIG": "DIEGO",
|
||||||
"DIGG": "DIGG",
|
"DIGG": "DIGG",
|
||||||
"DIGIC": "DigiCube",
|
"DIGIC": "DigiCube",
|
||||||
"DIGIF": "DigiFel",
|
"DIGIF": "DigiFel",
|
||||||
"DIGITAL": "Digital Reserve Currency",
|
"DIGITAL": "Digital Reserve Currency",
|
||||||
|
"DIGNITY": "Dignity",
|
||||||
"DIGS": "Diggits",
|
"DIGS": "Diggits",
|
||||||
"DIKO": "Arkadiko",
|
"DIKO": "Arkadiko",
|
||||||
"DILI": "D Community",
|
"DILI": "D Community",
|
||||||
@ -2246,6 +2271,7 @@
|
|||||||
"DOGBOSS": "Dog Boss",
|
"DOGBOSS": "Dog Boss",
|
||||||
"DOGDEFI": "DogDeFiCoin",
|
"DOGDEFI": "DogDeFiCoin",
|
||||||
"DOGE": "Dogecoin",
|
"DOGE": "Dogecoin",
|
||||||
|
"DOGE20": "Doge 2.0",
|
||||||
"DOGEBNB": "DogeBNB",
|
"DOGEBNB": "DogeBNB",
|
||||||
"DOGEC": "DogeCash",
|
"DOGEC": "DogeCash",
|
||||||
"DOGECEO": "Doge CEO",
|
"DOGECEO": "Doge CEO",
|
||||||
@ -2539,7 +2565,7 @@
|
|||||||
"ELONGT": "Elon GOAT",
|
"ELONGT": "Elon GOAT",
|
||||||
"ELONONE": "AstroElon",
|
"ELONONE": "AstroElon",
|
||||||
"ELP": "Ellerium",
|
"ELP": "Ellerium",
|
||||||
"ELS": "Elysium",
|
"ELS": "Ethlas",
|
||||||
"ELT": "Element Black",
|
"ELT": "Element Black",
|
||||||
"ELTC2": "eLTC",
|
"ELTC2": "eLTC",
|
||||||
"ELTCOIN": "ELTCOIN",
|
"ELTCOIN": "ELTCOIN",
|
||||||
@ -2548,6 +2574,7 @@
|
|||||||
"ELVN": "11Minutes",
|
"ELVN": "11Minutes",
|
||||||
"ELX": "Energy Ledger",
|
"ELX": "Energy Ledger",
|
||||||
"ELY": "Elysian",
|
"ELY": "Elysian",
|
||||||
|
"ELYSIUM": "Elysium",
|
||||||
"EM": "Eminer",
|
"EM": "Eminer",
|
||||||
"EMANATE": "EMANATE",
|
"EMANATE": "EMANATE",
|
||||||
"EMAR": "EmaratCoin",
|
"EMAR": "EmaratCoin",
|
||||||
@ -2559,6 +2586,7 @@
|
|||||||
"EMC2": "Einsteinium",
|
"EMC2": "Einsteinium",
|
||||||
"EMD": "Emerald",
|
"EMD": "Emerald",
|
||||||
"EMIGR": "EmiratesGoldCoin",
|
"EMIGR": "EmiratesGoldCoin",
|
||||||
|
"EML": "EML Protocol",
|
||||||
"EMN.CUR": "Eastman Chemical",
|
"EMN.CUR": "Eastman Chemical",
|
||||||
"EMON": "Ethermon",
|
"EMON": "Ethermon",
|
||||||
"EMOT": "Sentigraph.io",
|
"EMOT": "Sentigraph.io",
|
||||||
@ -2692,6 +2720,7 @@
|
|||||||
"ETHD": "Ethereum Dark",
|
"ETHD": "Ethereum Dark",
|
||||||
"ETHER": "Etherparty",
|
"ETHER": "Etherparty",
|
||||||
"ETHERDELTA": "EtherDelta",
|
"ETHERDELTA": "EtherDelta",
|
||||||
|
"ETHERKING": "Ether Kingdoms Token",
|
||||||
"ETHERNITY": "Ethernity Chain",
|
"ETHERNITY": "Ethernity Chain",
|
||||||
"ETHF": "EthereumFair",
|
"ETHF": "EthereumFair",
|
||||||
"ETHIX": "EthicHub",
|
"ETHIX": "EthicHub",
|
||||||
@ -2709,6 +2738,7 @@
|
|||||||
"ETHSHIB": "Eth Shiba",
|
"ETHSHIB": "Eth Shiba",
|
||||||
"ETHV": "Ethverse",
|
"ETHV": "Ethverse",
|
||||||
"ETHW": "Ethereum PoW",
|
"ETHW": "Ethereum PoW",
|
||||||
|
"ETHX": "Stader ETHx",
|
||||||
"ETHY": "Ethereum Yield",
|
"ETHY": "Ethereum Yield",
|
||||||
"ETI": "EtherInc",
|
"ETI": "EtherInc",
|
||||||
"ETK": "Energi Token",
|
"ETK": "Energi Token",
|
||||||
@ -2722,7 +2752,7 @@
|
|||||||
"ETR": "Electric Token",
|
"ETR": "Electric Token",
|
||||||
"ETRNT": "Eternal Trusts",
|
"ETRNT": "Eternal Trusts",
|
||||||
"ETS": "ETH Share",
|
"ETS": "ETH Share",
|
||||||
"ETSC": "Ether star blockchain",
|
"ETSC": "Ether star blockchain",
|
||||||
"ETT": "EncryptoTel",
|
"ETT": "EncryptoTel",
|
||||||
"ETY": "Ethereum Cloud",
|
"ETY": "Ethereum Cloud",
|
||||||
"ETZ": "EtherZero",
|
"ETZ": "EtherZero",
|
||||||
@ -2773,6 +2803,7 @@
|
|||||||
"EXB": "ExaByte (EXB)",
|
"EXB": "ExaByte (EXB)",
|
||||||
"EXC": "Eximchain",
|
"EXC": "Eximchain",
|
||||||
"EXCC": "ExchangeCoin",
|
"EXCC": "ExchangeCoin",
|
||||||
|
"EXCHANGEN": "ExchangeN",
|
||||||
"EXCL": "Exclusive Coin",
|
"EXCL": "Exclusive Coin",
|
||||||
"EXE": "ExeCoin",
|
"EXE": "ExeCoin",
|
||||||
"EXFI": "Flare Finance",
|
"EXFI": "Flare Finance",
|
||||||
@ -2781,7 +2812,7 @@
|
|||||||
"EXLT": "ExtraLovers",
|
"EXLT": "ExtraLovers",
|
||||||
"EXM": "EXMO Coin",
|
"EXM": "EXMO Coin",
|
||||||
"EXMR": "EXMR FDN",
|
"EXMR": "EXMR FDN",
|
||||||
"EXN": "ExchangeN",
|
"EXN": "Exeno",
|
||||||
"EXO": "Exosis",
|
"EXO": "Exosis",
|
||||||
"EXP": "Expanse",
|
"EXP": "Expanse",
|
||||||
"EXRD": "Radix",
|
"EXRD": "Radix",
|
||||||
@ -2814,6 +2845,7 @@
|
|||||||
"FAIR": "FairCoin",
|
"FAIR": "FairCoin",
|
||||||
"FAIRC": "Faireum Token",
|
"FAIRC": "Faireum Token",
|
||||||
"FAIRG": "FairGame",
|
"FAIRG": "FairGame",
|
||||||
|
"FAKE": "FAKE COIN",
|
||||||
"FAKT": "Medifakt",
|
"FAKT": "Medifakt",
|
||||||
"FALCONS": "Falcon Swaps",
|
"FALCONS": "Falcon Swaps",
|
||||||
"FAME": "Fame MMA",
|
"FAME": "Fame MMA",
|
||||||
@ -2860,6 +2892,7 @@
|
|||||||
"FDO": "Firdaos",
|
"FDO": "Firdaos",
|
||||||
"FDR": "French Digital Reserve",
|
"FDR": "French Digital Reserve",
|
||||||
"FDT": "Frutti Dino",
|
"FDT": "Frutti Dino",
|
||||||
|
"FDUSD": "First Digital USD",
|
||||||
"FDX": "fidentiaX",
|
"FDX": "fidentiaX",
|
||||||
"FDZ": "Friendz",
|
"FDZ": "Friendz",
|
||||||
"FEAR": "Fear",
|
"FEAR": "Fear",
|
||||||
@ -2870,6 +2903,7 @@
|
|||||||
"FEN": "First Ever NFT",
|
"FEN": "First Ever NFT",
|
||||||
"FENOMY": "Fenomy",
|
"FENOMY": "Fenomy",
|
||||||
"FER": "Ferro",
|
"FER": "Ferro",
|
||||||
|
"FERC": "FairERC20",
|
||||||
"FERMA": "Ferma",
|
"FERMA": "Ferma",
|
||||||
"FESS": "Fesschain",
|
"FESS": "Fesschain",
|
||||||
"FET": "Fetch.AI",
|
"FET": "Fetch.AI",
|
||||||
@ -2931,7 +2965,7 @@
|
|||||||
"FLASH": "Flashstake",
|
"FLASH": "Flashstake",
|
||||||
"FLASHC": "FLASH coin",
|
"FLASHC": "FLASH coin",
|
||||||
"FLC": "FlowChainCoin",
|
"FLC": "FlowChainCoin",
|
||||||
"FLD": "FLUID",
|
"FLD": "FluidAI",
|
||||||
"FLDC": "Folding Coin",
|
"FLDC": "Folding Coin",
|
||||||
"FLDT": "FairyLand",
|
"FLDT": "FairyLand",
|
||||||
"FLETA": "FLETA",
|
"FLETA": "FLETA",
|
||||||
@ -3091,6 +3125,7 @@
|
|||||||
"FUEL": "Jetfuel Finance",
|
"FUEL": "Jetfuel Finance",
|
||||||
"FUJIN": "Fujinto",
|
"FUJIN": "Fujinto",
|
||||||
"FUKU": "Furukuru",
|
"FUKU": "Furukuru",
|
||||||
|
"FUMO": "Alien Milady Fumo",
|
||||||
"FUN": "FUN Token",
|
"FUN": "FUN Token",
|
||||||
"FUNC": "FunCoin",
|
"FUNC": "FunCoin",
|
||||||
"FUND": "Unification",
|
"FUND": "Unification",
|
||||||
@ -3101,6 +3136,7 @@
|
|||||||
"FUNDZ": "FundFantasy",
|
"FUNDZ": "FundFantasy",
|
||||||
"FUNK": "Cypherfunks Coin",
|
"FUNK": "Cypherfunks Coin",
|
||||||
"FUR": "Furio",
|
"FUR": "Furio",
|
||||||
|
"FURU": "Furucombo",
|
||||||
"FURY": "Engines of Fury",
|
"FURY": "Engines of Fury",
|
||||||
"FUS": "Fus",
|
"FUS": "Fus",
|
||||||
"FUSE": "Fuse Network Token",
|
"FUSE": "Fuse Network Token",
|
||||||
@ -3118,6 +3154,7 @@
|
|||||||
"FXP": "FXPay",
|
"FXP": "FXPay",
|
||||||
"FXS": "Frax Share",
|
"FXS": "Frax Share",
|
||||||
"FXT": "FuzeX",
|
"FXT": "FuzeX",
|
||||||
|
"FXY": "Floxypay",
|
||||||
"FYN": "Affyn",
|
"FYN": "Affyn",
|
||||||
"FYP": "FlypMe",
|
"FYP": "FlypMe",
|
||||||
"FYZ": "Fyooz",
|
"FYZ": "Fyooz",
|
||||||
@ -3172,6 +3209,7 @@
|
|||||||
"GAT": "GATCOIN",
|
"GAT": "GATCOIN",
|
||||||
"GATE": "GATENet",
|
"GATE": "GATENet",
|
||||||
"GATEWAY": "Gateway Protocol",
|
"GATEWAY": "Gateway Protocol",
|
||||||
|
"GAYPEPE": "Gay Pepe",
|
||||||
"GAZE": "GazeTV",
|
"GAZE": "GazeTV",
|
||||||
"GB": "GoldBlocks",
|
"GB": "GoldBlocks",
|
||||||
"GBA": "Geeba",
|
"GBA": "Geeba",
|
||||||
@ -3222,6 +3260,7 @@
|
|||||||
"GEMZ": "Gemz Social",
|
"GEMZ": "Gemz Social",
|
||||||
"GEN": "DAOstack",
|
"GEN": "DAOstack",
|
||||||
"GENE": "Genopets",
|
"GENE": "Genopets",
|
||||||
|
"GENIE": "The Genie",
|
||||||
"GENIX": "Genix",
|
"GENIX": "Genix",
|
||||||
"GENS": "Genshiro",
|
"GENS": "Genshiro",
|
||||||
"GENSTAKE": "Genstake",
|
"GENSTAKE": "Genstake",
|
||||||
@ -3261,6 +3300,7 @@
|
|||||||
"GHCOLD": "Galaxy Heroes Coin",
|
"GHCOLD": "Galaxy Heroes Coin",
|
||||||
"GHD": "Giftedhands",
|
"GHD": "Giftedhands",
|
||||||
"GHNY": "Grizzly Honey",
|
"GHNY": "Grizzly Honey",
|
||||||
|
"GHO": "GHO",
|
||||||
"GHOST": "GhostbyMcAfee",
|
"GHOST": "GhostbyMcAfee",
|
||||||
"GHOSTCOIN": "GhostCoin",
|
"GHOSTCOIN": "GhostCoin",
|
||||||
"GHOSTM": "GhostMarket",
|
"GHOSTM": "GhostMarket",
|
||||||
@ -3274,6 +3314,7 @@
|
|||||||
"GIFT": "GiftNet",
|
"GIFT": "GiftNet",
|
||||||
"GIG": "GigaCoin",
|
"GIG": "GigaCoin",
|
||||||
"GIGA": "GigaSwap",
|
"GIGA": "GigaSwap",
|
||||||
|
"GIGX": "GigXCoin",
|
||||||
"GIM": "Gimli",
|
"GIM": "Gimli",
|
||||||
"GIMMER": "Gimmer",
|
"GIMMER": "Gimmer",
|
||||||
"GIN": "GINcoin",
|
"GIN": "GINcoin",
|
||||||
@ -3385,6 +3426,7 @@
|
|||||||
"GOVT": "The Government Network",
|
"GOVT": "The Government Network",
|
||||||
"GOZ": "Göztepe S.K. Fan Token",
|
"GOZ": "Göztepe S.K. Fan Token",
|
||||||
"GP": "Wizards And Dragons",
|
"GP": "Wizards And Dragons",
|
||||||
|
"GPBP": "Genius Playboy Billionaire Philanthropist",
|
||||||
"GPKR": "Gold Poker",
|
"GPKR": "Gold Poker",
|
||||||
"GPL": "Gold Pressed Latinum",
|
"GPL": "Gold Pressed Latinum",
|
||||||
"GPPT": "Pluto Project Coin",
|
"GPPT": "Pluto Project Coin",
|
||||||
@ -3501,7 +3543,8 @@
|
|||||||
"HALF": "0.5X Long Bitcoin Token",
|
"HALF": "0.5X Long Bitcoin Token",
|
||||||
"HALFSHIT": "0.5X Long Shitcoin Index Token",
|
"HALFSHIT": "0.5X Long Shitcoin Index Token",
|
||||||
"HALLO": "Halloween Coin",
|
"HALLO": "Halloween Coin",
|
||||||
"HALO": "Halo Platform",
|
"HALO": "Halo Coin",
|
||||||
|
"HALOPLATFORM": "Halo Platform",
|
||||||
"HAM": "Hamster",
|
"HAM": "Hamster",
|
||||||
"HAMS": "HamsterCoin",
|
"HAMS": "HamsterCoin",
|
||||||
"HANA": "Hanacoin",
|
"HANA": "Hanacoin",
|
||||||
@ -3598,6 +3641,7 @@
|
|||||||
"HILL": "President Clinton",
|
"HILL": "President Clinton",
|
||||||
"HINA": "Hina Inu",
|
"HINA": "Hina Inu",
|
||||||
"HINT": "Hintchain",
|
"HINT": "Hintchain",
|
||||||
|
"HIPPO": "HIPPO",
|
||||||
"HIRE": "HireMatch",
|
"HIRE": "HireMatch",
|
||||||
"HIT": "HitChain",
|
"HIT": "HitChain",
|
||||||
"HITBTC": "HitBTC Token",
|
"HITBTC": "HitBTC Token",
|
||||||
@ -3634,6 +3678,7 @@
|
|||||||
"HNTR": "Hunter",
|
"HNTR": "Hunter",
|
||||||
"HNY": "Honey",
|
"HNY": "Honey",
|
||||||
"HNZO": "Hanzo Inu",
|
"HNZO": "Hanzo Inu",
|
||||||
|
"HOBO": "HOBO THE BEAR",
|
||||||
"HOD": "HoDooi.com",
|
"HOD": "HoDooi.com",
|
||||||
"HODL": "HOdlcoin",
|
"HODL": "HOdlcoin",
|
||||||
"HOGE": "Hoge Finance",
|
"HOGE": "Hoge Finance",
|
||||||
@ -3839,7 +3884,7 @@
|
|||||||
"IMPCN": "Brain Space",
|
"IMPCN": "Brain Space",
|
||||||
"IMPER": "Impermax",
|
"IMPER": "Impermax",
|
||||||
"IMPS": "Impulse Coin",
|
"IMPS": "Impulse Coin",
|
||||||
"IMPT": "Ether Kingdoms Token",
|
"IMPT": "IMPT",
|
||||||
"IMPULSE": "IMPULSE by FDR",
|
"IMPULSE": "IMPULSE by FDR",
|
||||||
"IMS": "Independent Money System",
|
"IMS": "Independent Money System",
|
||||||
"IMST": "Imsmart",
|
"IMST": "Imsmart",
|
||||||
@ -4001,6 +4046,7 @@
|
|||||||
"JAM": "Tune.Fm",
|
"JAM": "Tune.Fm",
|
||||||
"JANE": "JaneCoin",
|
"JANE": "JaneCoin",
|
||||||
"JAR": "Jarvis+",
|
"JAR": "Jarvis+",
|
||||||
|
"JARED": "Jared From Subway",
|
||||||
"JASMY": "JasmyCoin",
|
"JASMY": "JasmyCoin",
|
||||||
"JBS": "JumBucks Coin",
|
"JBS": "JumBucks Coin",
|
||||||
"JBX": "Juicebox",
|
"JBX": "Juicebox",
|
||||||
@ -4163,9 +4209,10 @@
|
|||||||
"KIN": "Kin",
|
"KIN": "Kin",
|
||||||
"KIND": "Kind Ads",
|
"KIND": "Kind Ads",
|
||||||
"KINE": "Kine Protocol",
|
"KINE": "Kine Protocol",
|
||||||
"KING": "King Finance",
|
"KING": "KING",
|
||||||
"KING93": "King93",
|
"KING93": "King93",
|
||||||
"KINGDOMQUEST": "Kingdom Quest",
|
"KINGDOMQUEST": "Kingdom Quest",
|
||||||
|
"KINGF": "King Finance",
|
||||||
"KINGSHIB": "King Shiba",
|
"KINGSHIB": "King Shiba",
|
||||||
"KINGSWAP": "KingSwap",
|
"KINGSWAP": "KingSwap",
|
||||||
"KINT": "Kintsugi",
|
"KINT": "Kintsugi",
|
||||||
@ -4175,6 +4222,7 @@
|
|||||||
"KISC": "Kaiser",
|
"KISC": "Kaiser",
|
||||||
"KISHIMOTO": "Kishimoto Inu",
|
"KISHIMOTO": "Kishimoto Inu",
|
||||||
"KISHU": "Kishu Inu",
|
"KISHU": "Kishu Inu",
|
||||||
|
"KITA": "KITA INU",
|
||||||
"KITSU": "Kitsune Inu",
|
"KITSU": "Kitsune Inu",
|
||||||
"KITTY": "Kitty Inu",
|
"KITTY": "Kitty Inu",
|
||||||
"KKO": "Kineko",
|
"KKO": "Kineko",
|
||||||
@ -4267,10 +4315,12 @@
|
|||||||
"KUBO": "KUBO",
|
"KUBO": "KUBO",
|
||||||
"KUBOS": "KubosCoin",
|
"KUBOS": "KubosCoin",
|
||||||
"KUE": "Kuende",
|
"KUE": "Kuende",
|
||||||
|
"KUJI": "Kujira",
|
||||||
"KUMA": "Kuma Inu",
|
"KUMA": "Kuma Inu",
|
||||||
"KUNCI": "Kunci Coin",
|
"KUNCI": "Kunci Coin",
|
||||||
"KUR": "Kuro",
|
"KUR": "Kuro",
|
||||||
"KURT": "Kurrent",
|
"KURT": "Kurrent",
|
||||||
|
"KUSA": "Kusa Inu",
|
||||||
"KUSD": "Kowala",
|
"KUSD": "Kowala",
|
||||||
"KUSH": "KushCoin",
|
"KUSH": "KushCoin",
|
||||||
"KUV": "Kuverit",
|
"KUV": "Kuverit",
|
||||||
@ -4280,6 +4330,7 @@
|
|||||||
"KVT": "Kinesis Velocity Token",
|
"KVT": "Kinesis Velocity Token",
|
||||||
"KWATT": "4New",
|
"KWATT": "4New",
|
||||||
"KWD": "KIWI DEFI",
|
"KWD": "KIWI DEFI",
|
||||||
|
"KWENTA": "Kwenta",
|
||||||
"KWH": "KWHCoin",
|
"KWH": "KWHCoin",
|
||||||
"KWIK": "KwikSwap",
|
"KWIK": "KwikSwap",
|
||||||
"KWS": "Knight War Spirits",
|
"KWS": "Knight War Spirits",
|
||||||
@ -4299,7 +4350,9 @@
|
|||||||
"LABX": "Stakinglab",
|
"LABX": "Stakinglab",
|
||||||
"LACCOIN": "LocalAgro",
|
"LACCOIN": "LocalAgro",
|
||||||
"LACE": "Lovelace World",
|
"LACE": "Lovelace World",
|
||||||
|
"LADYS": "Milady Meme Coin",
|
||||||
"LAEEB": "LaEeb",
|
"LAEEB": "LaEeb",
|
||||||
|
"LAELAPS": "Laelaps",
|
||||||
"LAIKA": "Laika Protocol",
|
"LAIKA": "Laika Protocol",
|
||||||
"LALA": "LaLa World",
|
"LALA": "LaLa World",
|
||||||
"LAMB": "Lambda",
|
"LAMB": "Lambda",
|
||||||
@ -4455,13 +4508,14 @@
|
|||||||
"LLAND": "Lyfe Land",
|
"LLAND": "Lyfe Land",
|
||||||
"LLG": "Loligo",
|
"LLG": "Loligo",
|
||||||
"LLION": "Lydian Lion",
|
"LLION": "Lydian Lion",
|
||||||
"LM": "LM Token",
|
"LM": "LeisureMeta",
|
||||||
"LMAO": "LMAO Finance",
|
"LMAO": "LMAO Finance",
|
||||||
"LMC": "LomoCoin",
|
"LMC": "LomoCoin",
|
||||||
"LMCH": "Latamcash",
|
"LMCH": "Latamcash",
|
||||||
"LMCSWAP": "LimoCoin SWAP",
|
"LMCSWAP": "LimoCoin SWAP",
|
||||||
"LMR": "Lumerin",
|
"LMR": "Lumerin",
|
||||||
"LMT": "Lympo Market Token",
|
"LMT": "Lympo Market Token",
|
||||||
|
"LMTOKEN": "LM Token",
|
||||||
"LMXC": "LimonX",
|
"LMXC": "LimonX",
|
||||||
"LMY": "Lunch Money",
|
"LMY": "Lunch Money",
|
||||||
"LN": "LINK",
|
"LN": "LINK",
|
||||||
@ -4530,6 +4584,7 @@
|
|||||||
"LRG": "Largo Coin",
|
"LRG": "Largo Coin",
|
||||||
"LRN": "Loopring [NEO]",
|
"LRN": "Loopring [NEO]",
|
||||||
"LSD": "LightSpeedCoin",
|
"LSD": "LightSpeedCoin",
|
||||||
|
"LSETH": "Liquid Staked ETH",
|
||||||
"LSK": "Lisk",
|
"LSK": "Lisk",
|
||||||
"LSP": "Lumenswap",
|
"LSP": "Lumenswap",
|
||||||
"LSS": "Lossless",
|
"LSS": "Lossless",
|
||||||
@ -4626,6 +4681,7 @@
|
|||||||
"MAEP": "Maester Protocol",
|
"MAEP": "Maester Protocol",
|
||||||
"MAG": "Magnet",
|
"MAG": "Magnet",
|
||||||
"MAGIC": "Magic",
|
"MAGIC": "Magic",
|
||||||
|
"MAGICF": "MagicFox",
|
||||||
"MAHA": "MahaDAO",
|
"MAHA": "MahaDAO",
|
||||||
"MAI": "Mindsync",
|
"MAI": "Mindsync",
|
||||||
"MAID": "MaidSafe Coin",
|
"MAID": "MaidSafe Coin",
|
||||||
@ -4639,6 +4695,7 @@
|
|||||||
"MANDOX": "MandoX",
|
"MANDOX": "MandoX",
|
||||||
"MANGA": "Manga Token",
|
"MANGA": "Manga Token",
|
||||||
"MANNA": "Manna",
|
"MANNA": "Manna",
|
||||||
|
"MANTLE": "Mantle",
|
||||||
"MAP": "MAP Protocol",
|
"MAP": "MAP Protocol",
|
||||||
"MAPC": "MapCoin",
|
"MAPC": "MapCoin",
|
||||||
"MAPE": "Mecha Morphing",
|
"MAPE": "Mecha Morphing",
|
||||||
@ -4672,6 +4729,7 @@
|
|||||||
"MATIC": "Polygon",
|
"MATIC": "Polygon",
|
||||||
"MATPAD": "MaticPad",
|
"MATPAD": "MaticPad",
|
||||||
"MATTER": "AntiMatter",
|
"MATTER": "AntiMatter",
|
||||||
|
"MAV": "Maverick Protocol",
|
||||||
"MAX": "MaxCoin",
|
"MAX": "MaxCoin",
|
||||||
"MAXR": "Max Revive",
|
"MAXR": "Max Revive",
|
||||||
"MAY": "Theresa May Coin",
|
"MAY": "Theresa May Coin",
|
||||||
@ -4776,6 +4834,7 @@
|
|||||||
"MESA": "MetaVisa",
|
"MESA": "MetaVisa",
|
||||||
"MESG": "MESG",
|
"MESG": "MESG",
|
||||||
"MESH": "MeshBox",
|
"MESH": "MeshBox",
|
||||||
|
"MESSI": "MESSI COIN",
|
||||||
"MET": "Metronome",
|
"MET": "Metronome",
|
||||||
"META": "Metadium",
|
"META": "Metadium",
|
||||||
"METAC": "Metacoin",
|
"METAC": "Metacoin",
|
||||||
@ -4881,6 +4940,7 @@
|
|||||||
"MIODIO": "MIODIOCOIN",
|
"MIODIO": "MIODIOCOIN",
|
||||||
"MIOTA": "IOTA",
|
"MIOTA": "IOTA",
|
||||||
"MIR": "Mirror Protocol",
|
"MIR": "Mirror Protocol",
|
||||||
|
"MIRACLE": "MIRACLE",
|
||||||
"MIRC": "MIR COIN",
|
"MIRC": "MIR COIN",
|
||||||
"MIS": "Mithril Share",
|
"MIS": "Mithril Share",
|
||||||
"MISA": "Sangkara",
|
"MISA": "Sangkara",
|
||||||
@ -4938,7 +4998,6 @@
|
|||||||
"MNRB": "MoneyRebel",
|
"MNRB": "MoneyRebel",
|
||||||
"MNS": "Monnos",
|
"MNS": "Monnos",
|
||||||
"MNST": "MoonStarter",
|
"MNST": "MoonStarter",
|
||||||
"MNT": "microNFT",
|
|
||||||
"MNTC": "Manet Coin",
|
"MNTC": "Manet Coin",
|
||||||
"MNTG": "Monetas",
|
"MNTG": "Monetas",
|
||||||
"MNTL": "AssetMantle",
|
"MNTL": "AssetMantle",
|
||||||
@ -4967,6 +5026,7 @@
|
|||||||
"MOF": "Molecular Future (TRC20)",
|
"MOF": "Molecular Future (TRC20)",
|
||||||
"MOFI": "MobiFi",
|
"MOFI": "MobiFi",
|
||||||
"MOFOLD": "Molecular Future (ERC20)",
|
"MOFOLD": "Molecular Future (ERC20)",
|
||||||
|
"MOG": "Mog Coin",
|
||||||
"MOGU": "Mogu",
|
"MOGU": "Mogu",
|
||||||
"MOGX": "Mogu",
|
"MOGX": "Mogu",
|
||||||
"MOI": "MyOwnItem",
|
"MOI": "MyOwnItem",
|
||||||
@ -4989,9 +5049,11 @@
|
|||||||
"MONEYIMT": "MoneyToken",
|
"MONEYIMT": "MoneyToken",
|
||||||
"MONF": "Monfter",
|
"MONF": "Monfter",
|
||||||
"MONG": "MongCoin",
|
"MONG": "MongCoin",
|
||||||
|
"MONG20": "Mongoose 2.0",
|
||||||
"MONI": "Monsta Infinite",
|
"MONI": "Monsta Infinite",
|
||||||
"MONK": "Monkey Project",
|
"MONK": "Monkey Project",
|
||||||
"MONKEY": "Monkey",
|
"MONKEY": "Monkey",
|
||||||
|
"MONKEYS": "Monkeys Token",
|
||||||
"MONO": "MonoX",
|
"MONO": "MonoX",
|
||||||
"MONONOKEINU": "Mononoke Inu",
|
"MONONOKEINU": "Mononoke Inu",
|
||||||
"MONS": "Monsters Clan",
|
"MONS": "Monsters Clan",
|
||||||
@ -5011,11 +5073,13 @@
|
|||||||
"MOONSHOT": "Moonshot",
|
"MOONSHOT": "Moonshot",
|
||||||
"MOOO": "Hashtagger",
|
"MOOO": "Hashtagger",
|
||||||
"MOOV": "dotmoovs",
|
"MOOV": "dotmoovs",
|
||||||
|
"MOOX": "Moox Protocol",
|
||||||
"MOPS": "Mops",
|
"MOPS": "Mops",
|
||||||
"MORA": "Meliora",
|
"MORA": "Meliora",
|
||||||
"MORE": "More Coin",
|
"MORE": "More Coin",
|
||||||
"MOS": "MOS Coin",
|
"MOS": "MOS Coin",
|
||||||
"MOT": "Olympus Labs",
|
"MOT": "Olympus Labs",
|
||||||
|
"MOTG": "MetaOctagon",
|
||||||
"MOTI": "Motion",
|
"MOTI": "Motion",
|
||||||
"MOTO": "Motocoin",
|
"MOTO": "Motocoin",
|
||||||
"MOV": "MovieCoin",
|
"MOV": "MovieCoin",
|
||||||
@ -5076,6 +5140,7 @@
|
|||||||
"MSWAP": "MoneySwap",
|
"MSWAP": "MoneySwap",
|
||||||
"MT": "MyToken",
|
"MT": "MyToken",
|
||||||
"MTA": "Meta",
|
"MTA": "Meta",
|
||||||
|
"MTB": "MetaBridge",
|
||||||
"MTBC": "Metabolic",
|
"MTBC": "Metabolic",
|
||||||
"MTC": "MEDICAL TOKEN CURRENCY",
|
"MTC": "MEDICAL TOKEN CURRENCY",
|
||||||
"MTCMN": "MTC Mesh",
|
"MTCMN": "MTC Mesh",
|
||||||
@ -5108,6 +5173,7 @@
|
|||||||
"MUE": "MonetaryUnit",
|
"MUE": "MonetaryUnit",
|
||||||
"MULTI": "Multichain",
|
"MULTI": "Multichain",
|
||||||
"MULTIBOT": "Multibot",
|
"MULTIBOT": "Multibot",
|
||||||
|
"MULTIV": "Multiverse",
|
||||||
"MUN": "MUNcoin",
|
"MUN": "MUNcoin",
|
||||||
"MUNCH": "Munch Token",
|
"MUNCH": "Munch Token",
|
||||||
"MUSD": "mStable USD",
|
"MUSD": "mStable USD",
|
||||||
@ -5648,6 +5714,7 @@
|
|||||||
"OZP": "OZAPHYRE",
|
"OZP": "OZAPHYRE",
|
||||||
"P202": "Project 202",
|
"P202": "Project 202",
|
||||||
"P2PS": "P2P Solutions Foundation",
|
"P2PS": "P2P Solutions Foundation",
|
||||||
|
"PAAL": "PAAL AI",
|
||||||
"PAC": "PAC Protocol",
|
"PAC": "PAC Protocol",
|
||||||
"PACOCA": "Pacoca",
|
"PACOCA": "Pacoca",
|
||||||
"PAD": "NearPad",
|
"PAD": "NearPad",
|
||||||
@ -5736,6 +5803,7 @@
|
|||||||
"PEARL": "Pearl Finance",
|
"PEARL": "Pearl Finance",
|
||||||
"PEC": "PeaceCoin",
|
"PEC": "PeaceCoin",
|
||||||
"PEEL": "Meta Apes",
|
"PEEL": "Meta Apes",
|
||||||
|
"PEEPA": "Peepa",
|
||||||
"PEEPS": "The People’s Coin",
|
"PEEPS": "The People’s Coin",
|
||||||
"PEG": "PegNet",
|
"PEG": "PegNet",
|
||||||
"PEGS": "PegShares",
|
"PEGS": "PegShares",
|
||||||
@ -5748,6 +5816,7 @@
|
|||||||
"PEOPLE": "ConstitutionDAO",
|
"PEOPLE": "ConstitutionDAO",
|
||||||
"PEOS": "pEOS",
|
"PEOS": "pEOS",
|
||||||
"PEPE": "Pepe",
|
"PEPE": "Pepe",
|
||||||
|
"PEPE20": "Pepe 2.0",
|
||||||
"PEPECASH": "Pepe Cash",
|
"PEPECASH": "Pepe Cash",
|
||||||
"PEPPER": "Pepper Token",
|
"PEPPER": "Pepper Token",
|
||||||
"PEPS": "PEPS Coin",
|
"PEPS": "PEPS Coin",
|
||||||
@ -5822,6 +5891,7 @@
|
|||||||
"PINK": "PinkCoin",
|
"PINK": "PinkCoin",
|
||||||
"PINKX": "PantherCoin",
|
"PINKX": "PantherCoin",
|
||||||
"PINMO": "Pinmo",
|
"PINMO": "Pinmo",
|
||||||
|
"PINO": "Pinocchu",
|
||||||
"PINU": "Piccolo Inu",
|
"PINU": "Piccolo Inu",
|
||||||
"PIO": "Pioneershares",
|
"PIO": "Pioneershares",
|
||||||
"PIPI": "Pippi Finance",
|
"PIPI": "Pippi Finance",
|
||||||
@ -5885,6 +5955,7 @@
|
|||||||
"PLS": "Pulsechain",
|
"PLS": "Pulsechain",
|
||||||
"PLSD": "PulseDogecoin",
|
"PLSD": "PulseDogecoin",
|
||||||
"PLSPAD": "PulsePad",
|
"PLSPAD": "PulsePad",
|
||||||
|
"PLSX": "PulseX",
|
||||||
"PLT": "Poollotto.finance",
|
"PLT": "Poollotto.finance",
|
||||||
"PLTC": "PlatonCoin",
|
"PLTC": "PlatonCoin",
|
||||||
"PLTX": "PlutusX",
|
"PLTX": "PlutusX",
|
||||||
@ -5911,7 +5982,6 @@
|
|||||||
"PNK": "Kleros",
|
"PNK": "Kleros",
|
||||||
"PNL": "True PNL",
|
"PNL": "True PNL",
|
||||||
"PNODE": "Pinknode",
|
"PNODE": "Pinknode",
|
||||||
"PNP": "LogisticsX",
|
|
||||||
"PNT": "pNetwork Token",
|
"PNT": "pNetwork Token",
|
||||||
"PNX": "PhantomX",
|
"PNX": "PhantomX",
|
||||||
"PNY": "Peony Coin",
|
"PNY": "Peony Coin",
|
||||||
@ -5927,6 +5997,7 @@
|
|||||||
"POINTS": "Cryptsy Points",
|
"POINTS": "Cryptsy Points",
|
||||||
"POK": "Pokmonsters",
|
"POK": "Pokmonsters",
|
||||||
"POKEM": "Pokemonio",
|
"POKEM": "Pokemonio",
|
||||||
|
"POKEMON": "Pokemon",
|
||||||
"POKER": "PokerCoin",
|
"POKER": "PokerCoin",
|
||||||
"POKT": "Pocket Network",
|
"POKT": "Pocket Network",
|
||||||
"POL": "Pool-X",
|
"POL": "Pool-X",
|
||||||
@ -6010,6 +6081,7 @@
|
|||||||
"PRIME": "Echelon Prime",
|
"PRIME": "Echelon Prime",
|
||||||
"PRIMECHAIN": "PrimeChain",
|
"PRIMECHAIN": "PrimeChain",
|
||||||
"PRINT": "Printer.Finance",
|
"PRINT": "Printer.Finance",
|
||||||
|
"PRINTERIUM": "Printerium",
|
||||||
"PRINTS": "FingerprintsDAO",
|
"PRINTS": "FingerprintsDAO",
|
||||||
"PRISM": "Prism",
|
"PRISM": "Prism",
|
||||||
"PRIX": "Privatix",
|
"PRIX": "Privatix",
|
||||||
@ -6033,7 +6105,7 @@
|
|||||||
"PROTON": "Proton",
|
"PROTON": "Proton",
|
||||||
"PROUD": "PROUD Money",
|
"PROUD": "PROUD Money",
|
||||||
"PROXI": "PROXI",
|
"PROXI": "PROXI",
|
||||||
"PRP": "Papyrus",
|
"PRP": "Pepe Prime",
|
||||||
"PRPS": "Purpose",
|
"PRPS": "Purpose",
|
||||||
"PRPT": "Purple Token",
|
"PRPT": "Purple Token",
|
||||||
"PRQ": "PARSIQ",
|
"PRQ": "PARSIQ",
|
||||||
@ -6042,7 +6114,7 @@
|
|||||||
"PRTG": "Pre-Retogeum",
|
"PRTG": "Pre-Retogeum",
|
||||||
"PRV": "PrivacySwap",
|
"PRV": "PrivacySwap",
|
||||||
"PRVS": "Previse",
|
"PRVS": "Previse",
|
||||||
"PRX": "Printerium",
|
"PRX": "Parex",
|
||||||
"PRXY": "Proxy",
|
"PRXY": "Proxy",
|
||||||
"PRY": "PRIMARY",
|
"PRY": "PRIMARY",
|
||||||
"PSB": "Planet Sandbox",
|
"PSB": "Planet Sandbox",
|
||||||
@ -6120,6 +6192,7 @@
|
|||||||
"PYRAM": "Pyram Token",
|
"PYRAM": "Pyram Token",
|
||||||
"PYRK": "Pyrk",
|
"PYRK": "Pyrk",
|
||||||
"PYT": "Payther",
|
"PYT": "Payther",
|
||||||
|
"PYUSD": "PayPal USD",
|
||||||
"PZM": "Prizm",
|
"PZM": "Prizm",
|
||||||
"Q1S": "Quantum1Net",
|
"Q1S": "Quantum1Net",
|
||||||
"Q2C": "QubitCoin",
|
"Q2C": "QubitCoin",
|
||||||
@ -6178,6 +6251,7 @@
|
|||||||
"QUA": "Quantum Tech",
|
"QUA": "Quantum Tech",
|
||||||
"QUACK": "Rich Quack",
|
"QUACK": "Rich Quack",
|
||||||
"QUAM": "Quam Network",
|
"QUAM": "Quam Network",
|
||||||
|
"QUANT": "Quant Finance",
|
||||||
"QUARASHI": "Quarashi Network",
|
"QUARASHI": "Quarashi Network",
|
||||||
"QUARTZ": "Sandclock",
|
"QUARTZ": "Sandclock",
|
||||||
"QUASA": "Quasacoin",
|
"QUASA": "Quasacoin",
|
||||||
@ -6201,7 +6275,7 @@
|
|||||||
"RAC": "RAcoin",
|
"RAC": "RAcoin",
|
||||||
"RACA": "Radio Caca",
|
"RACA": "Radio Caca",
|
||||||
"RACEFI": "RaceFi",
|
"RACEFI": "RaceFi",
|
||||||
"RAD": "Radicle",
|
"RAD": "Radworks",
|
||||||
"RADAR": "DappRadar",
|
"RADAR": "DappRadar",
|
||||||
"RADI": "RadicalCoin",
|
"RADI": "RadicalCoin",
|
||||||
"RADIO": "RadioShack",
|
"RADIO": "RadioShack",
|
||||||
@ -6220,7 +6294,7 @@
|
|||||||
"RAM": "Ramifi Protocol",
|
"RAM": "Ramifi Protocol",
|
||||||
"RAMP": "RAMP",
|
"RAMP": "RAMP",
|
||||||
"RANKER": "RankerDao",
|
"RANKER": "RankerDao",
|
||||||
"RAP": "Rapture",
|
"RAP": "Philosoraptor",
|
||||||
"RAPDOGE": "RapDoge",
|
"RAPDOGE": "RapDoge",
|
||||||
"RARE": "SuperRare",
|
"RARE": "SuperRare",
|
||||||
"RARI": "Rarible",
|
"RARI": "Rarible",
|
||||||
@ -6277,6 +6351,7 @@
|
|||||||
"REA": "Realisto",
|
"REA": "Realisto",
|
||||||
"REAL": "RealLink",
|
"REAL": "RealLink",
|
||||||
"REALM": "Realm",
|
"REALM": "Realm",
|
||||||
|
"REALMS": "Realms of Ethernity",
|
||||||
"REALPLATFORM": "REAL",
|
"REALPLATFORM": "REAL",
|
||||||
"REALY": "Realy Metaverse",
|
"REALY": "Realy Metaverse",
|
||||||
"REAP": "ReapChain",
|
"REAP": "ReapChain",
|
||||||
@ -6287,6 +6362,7 @@
|
|||||||
"RED": "RED TOKEN",
|
"RED": "RED TOKEN",
|
||||||
"REDC": "RedCab",
|
"REDC": "RedCab",
|
||||||
"REDCO": "Redcoin",
|
"REDCO": "Redcoin",
|
||||||
|
"REDDIT": "Reddit",
|
||||||
"REDI": "REDi",
|
"REDI": "REDi",
|
||||||
"REDLANG": "RED",
|
"REDLANG": "RED",
|
||||||
"REDLC": "Redlight Chain",
|
"REDLC": "Redlight Chain",
|
||||||
@ -6324,7 +6400,7 @@
|
|||||||
"REST": "Restore",
|
"REST": "Restore",
|
||||||
"RET": "RealTract",
|
"RET": "RealTract",
|
||||||
"RETAIL": "Retail.Global",
|
"RETAIL": "Retail.Global",
|
||||||
"RETH": "Realms of Ethernity",
|
"RETH": "Rocket Pool ETH",
|
||||||
"RETH2": "rETH2",
|
"RETH2": "rETH2",
|
||||||
"RETIRE": "Retire Token",
|
"RETIRE": "Retire Token",
|
||||||
"REU": "REUCOIN",
|
"REU": "REUCOIN",
|
||||||
@ -6351,6 +6427,7 @@
|
|||||||
"RGP": "Rigel Protocol",
|
"RGP": "Rigel Protocol",
|
||||||
"RGT": "Rari Governance Token",
|
"RGT": "Rari Governance Token",
|
||||||
"RHEA": "Rhea",
|
"RHEA": "Rhea",
|
||||||
|
"RHINO": "RHINO",
|
||||||
"RHOC": "RChain",
|
"RHOC": "RChain",
|
||||||
"RHP": "Rhypton Club",
|
"RHP": "Rhypton Club",
|
||||||
"RIC": "Riecoin",
|
"RIC": "Riecoin",
|
||||||
@ -6490,6 +6567,7 @@
|
|||||||
"RWE": "Real-World Evidence",
|
"RWE": "Real-World Evidence",
|
||||||
"RWN": "Rowan Token",
|
"RWN": "Rowan Token",
|
||||||
"RWS": "Robonomics Web Services",
|
"RWS": "Robonomics Web Services",
|
||||||
|
"RXD": "Radiant",
|
||||||
"RXT": "RIMAUNANGIS",
|
"RXT": "RIMAUNANGIS",
|
||||||
"RYC": "RoyalCoin",
|
"RYC": "RoyalCoin",
|
||||||
"RYCN": "RoyalCoin 2.0",
|
"RYCN": "RoyalCoin 2.0",
|
||||||
@ -6564,6 +6642,7 @@
|
|||||||
"SBTC": "Super Bitcoin",
|
"SBTC": "Super Bitcoin",
|
||||||
"SC": "Siacoin",
|
"SC": "Siacoin",
|
||||||
"SCA": "SiaClassic",
|
"SCA": "SiaClassic",
|
||||||
|
"SCAM": "Scam Coin",
|
||||||
"SCAP": "SafeCapital",
|
"SCAP": "SafeCapital",
|
||||||
"SCAR": "Velhalla",
|
"SCAR": "Velhalla",
|
||||||
"SCASH": "SpaceCash",
|
"SCASH": "SpaceCash",
|
||||||
@ -6624,6 +6703,7 @@
|
|||||||
"SEER": "SEER",
|
"SEER": "SEER",
|
||||||
"SEI": "Sei",
|
"SEI": "Sei",
|
||||||
"SEL": "SelenCoin",
|
"SEL": "SelenCoin",
|
||||||
|
"SELF": "SELFCrypto",
|
||||||
"SEM": "Semux",
|
"SEM": "Semux",
|
||||||
"SEN": "Sentaro",
|
"SEN": "Sentaro",
|
||||||
"SENATE": "SENATE",
|
"SENATE": "SENATE",
|
||||||
@ -6665,6 +6745,7 @@
|
|||||||
"SGE": "Society of Galactic Exploration",
|
"SGE": "Society of Galactic Exploration",
|
||||||
"SGLY": "Singularity",
|
"SGLY": "Singularity",
|
||||||
"SGN": "Signals Network",
|
"SGN": "Signals Network",
|
||||||
|
"SGO": "SafuuGO",
|
||||||
"SGOLD": "SpaceGold",
|
"SGOLD": "SpaceGold",
|
||||||
"SGP": "SGPay",
|
"SGP": "SGPay",
|
||||||
"SGR": "Sogur Currency",
|
"SGR": "Sogur Currency",
|
||||||
@ -6684,6 +6765,7 @@
|
|||||||
"SHEESH": "Sheesh it is bussin bussin",
|
"SHEESH": "Sheesh it is bussin bussin",
|
||||||
"SHEESHA": "Sheesha Finance",
|
"SHEESHA": "Sheesha Finance",
|
||||||
"SHELL": "Shell Token",
|
"SHELL": "Shell Token",
|
||||||
|
"SHERA": "Shera Tokens",
|
||||||
"SHFL": "SHUFFLE!",
|
"SHFL": "SHUFFLE!",
|
||||||
"SHFT": "Shyft Network",
|
"SHFT": "Shyft Network",
|
||||||
"SHI": "Shirtum",
|
"SHI": "Shirtum",
|
||||||
@ -6719,6 +6801,8 @@
|
|||||||
"SHR": "ShareToken",
|
"SHR": "ShareToken",
|
||||||
"SHREK": "ShrekCoin",
|
"SHREK": "ShrekCoin",
|
||||||
"SHROOM": "Shroom.Finance",
|
"SHROOM": "Shroom.Finance",
|
||||||
|
"SHROOMFOX": "Magic Shroom",
|
||||||
|
"SHS": "SHEESH",
|
||||||
"SHX": "Stronghold Token",
|
"SHX": "Stronghold Token",
|
||||||
"SI": "Siren",
|
"SI": "Siren",
|
||||||
"SIB": "SibCoin",
|
"SIB": "SibCoin",
|
||||||
@ -7018,9 +7102,11 @@
|
|||||||
"STEN": "Steneum Coin",
|
"STEN": "Steneum Coin",
|
||||||
"STEP": "Step Finance",
|
"STEP": "Step Finance",
|
||||||
"STEPH": "Step Hero",
|
"STEPH": "Step Hero",
|
||||||
|
"STEPR": "Step",
|
||||||
"STEPS": "Steps",
|
"STEPS": "Steps",
|
||||||
"STERLINGCOIN": "SterlingCoin",
|
"STERLINGCOIN": "SterlingCoin",
|
||||||
"STETH": "Staked Ether",
|
"STETH": "Staked Ether",
|
||||||
|
"STEWIE": "Stewie Coin",
|
||||||
"STEX": "STEX",
|
"STEX": "STEX",
|
||||||
"STF": "Structure Finance",
|
"STF": "Structure Finance",
|
||||||
"STFX": "STFX",
|
"STFX": "STFX",
|
||||||
@ -7055,7 +7141,7 @@
|
|||||||
"STR": "Sourceless",
|
"STR": "Sourceless",
|
||||||
"STRAKS": "Straks",
|
"STRAKS": "Straks",
|
||||||
"STRAX": "Stratis",
|
"STRAX": "Stratis",
|
||||||
"STRAY": "Animal Token",
|
"STRAY": "Stray Dog",
|
||||||
"STREAM": "STREAMIT COIN",
|
"STREAM": "STREAMIT COIN",
|
||||||
"STRIP": "Stripto",
|
"STRIP": "Stripto",
|
||||||
"STRK": "Strike",
|
"STRK": "Strike",
|
||||||
@ -7361,6 +7447,7 @@
|
|||||||
"TOM": "TOM Finance",
|
"TOM": "TOM Finance",
|
||||||
"TOMAHAWKCOIN": "Tomahawkcoin",
|
"TOMAHAWKCOIN": "Tomahawkcoin",
|
||||||
"TOMB": "Tomb",
|
"TOMB": "Tomb",
|
||||||
|
"TOMI": "tomiNet",
|
||||||
"TOMO": "TomoChain",
|
"TOMO": "TomoChain",
|
||||||
"TOMOE": "TomoChain ERC20",
|
"TOMOE": "TomoChain ERC20",
|
||||||
"TOMS": "TomTomCoin",
|
"TOMS": "TomTomCoin",
|
||||||
@ -7385,6 +7472,7 @@
|
|||||||
"TOTM": "Totem",
|
"TOTM": "Totem",
|
||||||
"TOWER": "Tower",
|
"TOWER": "Tower",
|
||||||
"TOWN": "Town Star",
|
"TOWN": "Town Star",
|
||||||
|
"TOX": "INTOverse",
|
||||||
"TOZ": "Tozex",
|
"TOZ": "Tozex",
|
||||||
"TP": "Token Swap",
|
"TP": "Token Swap",
|
||||||
"TPAD": "TrustPad",
|
"TPAD": "TrustPad",
|
||||||
@ -7600,6 +7688,7 @@
|
|||||||
"UNITY": "SuperNET",
|
"UNITY": "SuperNET",
|
||||||
"UNIVRS": "Universe",
|
"UNIVRS": "Universe",
|
||||||
"UNIX": "UniX",
|
"UNIX": "UniX",
|
||||||
|
"UNLEASH": "UnleashClub",
|
||||||
"UNN": "UNION Protocol Governance Token",
|
"UNN": "UNION Protocol Governance Token",
|
||||||
"UNO": "Unobtanium",
|
"UNO": "Unobtanium",
|
||||||
"UNORE": "UnoRe",
|
"UNORE": "UnoRe",
|
||||||
@ -7673,6 +7762,7 @@
|
|||||||
"UTT": "United Traders Token",
|
"UTT": "United Traders Token",
|
||||||
"UTU": "UTU Protocol",
|
"UTU": "UTU Protocol",
|
||||||
"UUU": "U Network",
|
"UUU": "U Network",
|
||||||
|
"UWU": "uwu",
|
||||||
"UZUMAKI": "Uzumaki Inu",
|
"UZUMAKI": "Uzumaki Inu",
|
||||||
"VAB": "Vabble",
|
"VAB": "Vabble",
|
||||||
"VADER": "Vader Protocol",
|
"VADER": "Vader Protocol",
|
||||||
@ -7695,6 +7785,7 @@
|
|||||||
"VCF": "Valencia CF Fan Token",
|
"VCF": "Valencia CF Fan Token",
|
||||||
"VCG": "VCGamers",
|
"VCG": "VCGamers",
|
||||||
"VCK": "28VCK",
|
"VCK": "28VCK",
|
||||||
|
"VCORE": "VCORE",
|
||||||
"VDG": "VeriDocGlobal",
|
"VDG": "VeriDocGlobal",
|
||||||
"VDL": "Vidulum",
|
"VDL": "Vidulum",
|
||||||
"VDO": "VidioCoin",
|
"VDO": "VidioCoin",
|
||||||
@ -7710,6 +7801,7 @@
|
|||||||
"VEIL": "VEIL",
|
"VEIL": "VEIL",
|
||||||
"VELA": "Vela Token",
|
"VELA": "Vela Token",
|
||||||
"VELO": "Velo",
|
"VELO": "Velo",
|
||||||
|
"VELOD": "Velodrome Finance",
|
||||||
"VELOX": "Velox",
|
"VELOX": "Velox",
|
||||||
"VELOXPROJECT": "Velox",
|
"VELOXPROJECT": "Velox",
|
||||||
"VEMP": "vEmpire DDAO",
|
"VEMP": "vEmpire DDAO",
|
||||||
@ -7782,6 +7874,7 @@
|
|||||||
"VNT": "VNT Chain",
|
"VNT": "VNT Chain",
|
||||||
"VNTW": "Value Network Token",
|
"VNTW": "Value Network Token",
|
||||||
"VNX": "VisionX",
|
"VNX": "VisionX",
|
||||||
|
"VNXAU": "VNX Gold",
|
||||||
"VNXLU": "VNX Exchange",
|
"VNXLU": "VNX Exchange",
|
||||||
"VOCO": "Provoco",
|
"VOCO": "Provoco",
|
||||||
"VODKA": "Vodka Token",
|
"VODKA": "Vodka Token",
|
||||||
@ -7902,7 +7995,8 @@
|
|||||||
"WEC": "Whole Earth Coin",
|
"WEC": "Whole Earth Coin",
|
||||||
"WEGEN": "WeGen Platform",
|
"WEGEN": "WeGen Platform",
|
||||||
"WELD": "Weld",
|
"WELD": "Weld",
|
||||||
"WELL": "Well",
|
"WELL": "Moonwell",
|
||||||
|
"WELLTOKEN": "Well",
|
||||||
"WELT": "Fabwelt",
|
"WELT": "Fabwelt",
|
||||||
"WELUPS": "Welups Blockchain",
|
"WELUPS": "Welups Blockchain",
|
||||||
"WEMIX": "WEMIX",
|
"WEMIX": "WEMIX",
|
||||||
@ -7958,6 +8052,7 @@
|
|||||||
"WIX": "Wixlar",
|
"WIX": "Wixlar",
|
||||||
"WIZ": "WIZ Protocol",
|
"WIZ": "WIZ Protocol",
|
||||||
"WKD": "Wakanda Inu",
|
"WKD": "Wakanda Inu",
|
||||||
|
"WLD": "Worldcoin",
|
||||||
"WLF": "Wolfs Group",
|
"WLF": "Wolfs Group",
|
||||||
"WLITI": "wLITI",
|
"WLITI": "wLITI",
|
||||||
"WLK": "Wolk",
|
"WLK": "Wolk",
|
||||||
@ -7983,6 +8078,7 @@
|
|||||||
"WNZ": "Winerz",
|
"WNZ": "Winerz",
|
||||||
"WOA": "Wrapped Origin Axie",
|
"WOA": "Wrapped Origin Axie",
|
||||||
"WOD": "World of Defish",
|
"WOD": "World of Defish",
|
||||||
|
"WOID": "WORLD ID",
|
||||||
"WOJ": "Wojak Finance",
|
"WOJ": "Wojak Finance",
|
||||||
"WOLF": "Insanity Coin",
|
"WOLF": "Insanity Coin",
|
||||||
"WOLFILAND": "Wolfiland",
|
"WOLFILAND": "Wolfiland",
|
||||||
@ -8000,6 +8096,7 @@
|
|||||||
"WOOFY": "Woofy",
|
"WOOFY": "Woofy",
|
||||||
"WOOL": "Wolf Game Wool",
|
"WOOL": "Wolf Game Wool",
|
||||||
"WOONK": "Woonkly",
|
"WOONK": "Woonkly",
|
||||||
|
"WOOO": "wooonen",
|
||||||
"WOOP": "Woonkly Power",
|
"WOOP": "Woonkly Power",
|
||||||
"WOP": "WorldPay",
|
"WOP": "WorldPay",
|
||||||
"WORLD": "World Token",
|
"WORLD": "World Token",
|
||||||
@ -8010,6 +8107,7 @@
|
|||||||
"WOZX": "Efforce",
|
"WOZX": "Efforce",
|
||||||
"WPC": "WePiggy Coin",
|
"WPC": "WePiggy Coin",
|
||||||
"WPE": "OPES (Wrapped PE)",
|
"WPE": "OPES (Wrapped PE)",
|
||||||
|
"WPLS": "Wrapped Pulse",
|
||||||
"WPP": "Green Energy Token",
|
"WPP": "Green Energy Token",
|
||||||
"WPR": "WePower",
|
"WPR": "WePower",
|
||||||
"WQT": "Work Quest",
|
"WQT": "Work Quest",
|
||||||
@ -8049,6 +8147,7 @@
|
|||||||
"WZEC": "Wrapped Zcash",
|
"WZEC": "Wrapped Zcash",
|
||||||
"WZENIQ": "Wrapped Zeniq (ETH)",
|
"WZENIQ": "Wrapped Zeniq (ETH)",
|
||||||
"WZRD": "Wizardia",
|
"WZRD": "Wizardia",
|
||||||
|
"X": "AI-X",
|
||||||
"X2": "X2Coin",
|
"X2": "X2Coin",
|
||||||
"X2Y2": "X2Y2",
|
"X2Y2": "X2Y2",
|
||||||
"X42": "X42 Protocol",
|
"X42": "X42 Protocol",
|
||||||
@ -8096,7 +8195,7 @@
|
|||||||
"XCI": "Cannabis Industry Coin",
|
"XCI": "Cannabis Industry Coin",
|
||||||
"XCLR": "ClearCoin",
|
"XCLR": "ClearCoin",
|
||||||
"XCM": "CoinMetro",
|
"XCM": "CoinMetro",
|
||||||
"XCN": "Chain",
|
"XCN": "Onyxcoin",
|
||||||
"XCO": "XCoin",
|
"XCO": "XCoin",
|
||||||
"XCONSOL": "X-Consoles",
|
"XCONSOL": "X-Consoles",
|
||||||
"XCP": "CounterParty",
|
"XCP": "CounterParty",
|
||||||
@ -8365,6 +8464,7 @@
|
|||||||
"YUANG": "Yuang Coin",
|
"YUANG": "Yuang Coin",
|
||||||
"YUCJ": "Yu Coin",
|
"YUCJ": "Yu Coin",
|
||||||
"YUCT": "Yucreat",
|
"YUCT": "Yucreat",
|
||||||
|
"YUDI": "Yudi",
|
||||||
"YUM": "Yumerium",
|
"YUM": "Yumerium",
|
||||||
"YUMMY": "Yummy",
|
"YUMMY": "Yummy",
|
||||||
"YUP": "Crowdholding",
|
"YUP": "Crowdholding",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"CYBER24781": "CyberConnect",
|
||||||
"LUNA1": "Terra",
|
"LUNA1": "Terra",
|
||||||
"LUNA2": "Terra",
|
"LUNA2": "Terra",
|
||||||
"SGB1": "Songbird",
|
"SGB1": "Songbird",
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset
|
<urlset
|
||||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de</loc>
|
<loc>https://ghostfol.io/de</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -50,6 +50,126 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen</loc>
|
<loc>https://ghostfol.io/de/ressourcen</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-capmon</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-copilot-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-delta</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-divvydiary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-getquin</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-gospatz</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-parqet</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-plannix</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portfolio-dividend-tracker</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portseido</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-projectionlab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-seeking-alpha</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sharesight</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-simple-portfolio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockle</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sumio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-utluna</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -142,6 +262,18 @@
|
|||||||
<loc>https://ghostfol.io/en/blog/2023/07/exploring-the-path-to-fire</loc>
|
<loc>https://ghostfol.io/en/blog/2023/07/exploring-the-path-to-fire</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2023/08/ghostfolio-joins-oss-friends</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2023/09/ghostfolio-2</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2023/09/hacktoberfest-2023</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/faq</loc>
|
<loc>https://ghostfol.io/en/faq</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -180,6 +312,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-capmon</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-copilot-money</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-copilot-money</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -196,6 +332,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -264,6 +404,14 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockle</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -322,6 +470,10 @@
|
|||||||
<loc>https://ghostfol.io/es/sobre/licencia</loc>
|
<loc>https://ghostfol.io/es/sobre/licencia</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/sobre/oss-friends</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc>
|
<loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -342,6 +494,10 @@
|
|||||||
<loc>https://ghostfol.io/fr/a-propos/licence</loc>
|
<loc>https://ghostfol.io/fr/a-propos/licence</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/a-propos/oss-friends</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
|
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -396,12 +552,16 @@
|
|||||||
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
|
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/informazioni-su/licenza</loc>
|
<loc>https://ghostfol.io/it/informazioni-su/licenza</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
|
<loc>https://ghostfol.io/it/informazioni-su/oss-friends</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
@ -426,6 +586,126 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse</loc>
|
<loc>https://ghostfol.io/it/risorse</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-campmon</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-copilot-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-delta</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-divvydiary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-getquin</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-gospatz</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-parqet</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-plannix</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-dividend-tracker</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portseido</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-projectionlab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-seeking-alpha</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sharesight</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-simple-portfolio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockle</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sumio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl</loc>
|
<loc>https://ghostfol.io/nl</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -435,7 +715,127 @@
|
|||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/kenmerken</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-capmon</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-copilot-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-delta</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-divvydiary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-getquin</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-gospatz</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-parqet</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-plannix</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-dividend-tracker</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portseido</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-projectionlab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-seeking-alpha</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sharesight</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-simple-portfolio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockle</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sumio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
@ -460,6 +860,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/over/licentie</loc>
|
<loc>https://ghostfol.io/nl/over/licentie</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/over/oss-friends</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/over/privacybeleid</loc>
|
<loc>https://ghostfol.io/nl/over/privacybeleid</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -473,7 +877,11 @@
|
|||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
<loc>https://ghostfol.io/nl/veelgestelde-vragen</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
@ -520,8 +928,16 @@
|
|||||||
<loc>https://ghostfol.io/pt/sobre/licenca</loc>
|
<loc>https://ghostfol.io/pt/sobre/licenca</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/sobre/oss-friends</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
|
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/tr</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
@ -3,7 +3,7 @@ import { cloneDeep, isArray, isObject } from 'lodash';
|
|||||||
|
|
||||||
export function hasNotDefinedValuesInObject(aObject: Object): boolean {
|
export function hasNotDefinedValuesInObject(aObject: Object): boolean {
|
||||||
for (const key in aObject) {
|
for (const key in aObject) {
|
||||||
if (aObject[key] === null || aObject[key] === null) {
|
if (aObject[key] === null || aObject[key] === undefined) {
|
||||||
return true;
|
return true;
|
||||||
} else if (isObject(aObject[key])) {
|
} else if (isObject(aObject[key])) {
|
||||||
return hasNotDefinedValuesInObject(aObject[key]);
|
return hasNotDefinedValuesInObject(aObject[key]);
|
||||||
|
@ -55,7 +55,6 @@ async function bootstrap() {
|
|||||||
|
|
||||||
app.use(HtmlTemplateMiddleware);
|
app.use(HtmlTemplateMiddleware);
|
||||||
|
|
||||||
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
|
|
||||||
const HOST = configService.get<string>('HOST') || '0.0.0.0';
|
const HOST = configService.get<string>('HOST') || '0.0.0.0';
|
||||||
const PORT = configService.get<number>('PORT') || 3333;
|
const PORT = configService.get<number>('PORT') || 3333;
|
||||||
|
|
||||||
@ -63,15 +62,6 @@ async function bootstrap() {
|
|||||||
logLogo();
|
logLogo();
|
||||||
Logger.log(`Listening at http://${HOST}:${PORT}`);
|
Logger.log(`Listening at http://${HOST}:${PORT}`);
|
||||||
Logger.log('');
|
Logger.log('');
|
||||||
|
|
||||||
if (BASE_CURRENCY) {
|
|
||||||
Logger.warn(
|
|
||||||
`The environment variable "BASE_CURRENCY" is deprecated and will be removed in Ghostfolio 2.0.`
|
|
||||||
);
|
|
||||||
Logger.warn(
|
|
||||||
'Please use the currency converter in the activity dialog instead.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ const descriptions = {
|
|||||||
fr: 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
|
fr: 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
|
||||||
it: 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
|
it: 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
|
||||||
nl: 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.',
|
nl: 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.',
|
||||||
pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.'
|
pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
|
||||||
|
tr: 'Ghostfolio, hisse senetleri, ETF’ler veya kripto para birimleri gibi varlıklarınızı birden fazla platformda takip etmenizi sağlayan bir kişisel finans panosudur.'
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = 'Ghostfolio – Open Source Wealth Management Software';
|
const title = 'Ghostfolio – Open Source Wealth Management Software';
|
||||||
@ -71,6 +72,18 @@ const locales = {
|
|||||||
'/en/blog/2023/07/exploring-the-path-to-fire': {
|
'/en/blog/2023/07/exploring-the-path-to-fire': {
|
||||||
featureGraphicPath: 'assets/images/blog/20230701.jpg',
|
featureGraphicPath: 'assets/images/blog/20230701.jpg',
|
||||||
title: `Exploring the Path to FIRE - ${titleShort}`
|
title: `Exploring the Path to FIRE - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2023/08/ghostfolio-joins-oss-friends': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/ghostfolio-joins-oss-friends.png',
|
||||||
|
title: `Ghostfolio joins OSS Friends - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2023/09/ghostfolio-2': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/ghostfolio-2.jpg',
|
||||||
|
title: `Announcing Ghostfolio 2.0 - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2023/09/hacktoberfest-2023': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
|
||||||
|
title: `Hacktoberfest 2023 - ${titleShort}`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
@ -6,16 +7,18 @@ import {
|
|||||||
UserSettings
|
UserSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
|
||||||
|
|
||||||
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||||
|
private accounts: PortfolioDetails['accounts'];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private accounts: PortfolioDetails['accounts']
|
accounts: PortfolioDetails['accounts']
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Investment'
|
name: 'Investment'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.accounts = accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(ruleSettings: Settings) {
|
public evaluate(ruleSettings: Settings) {
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
|
import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
|
||||||
|
|
||||||
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
||||||
|
private accounts: PortfolioDetails['accounts'];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private accounts: PortfolioDetails['accounts']
|
accounts: PortfolioDetails['accounts']
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Single Account'
|
name: 'Single Account'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.accounts = accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate() {
|
public evaluate() {
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
|
||||||
|
|
||||||
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
|
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
|
||||||
|
private positions: TimelinePosition[];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private positions: TimelinePosition[]
|
positions: TimelinePosition[]
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Investment: Base Currency'
|
name: 'Investment: Base Currency'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.positions = positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(ruleSettings: Settings) {
|
public evaluate(ruleSettings: Settings) {
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
|
||||||
|
|
||||||
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||||
|
private positions: TimelinePosition[];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private positions: TimelinePosition[]
|
positions: TimelinePosition[]
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Investment'
|
name: 'Investment'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.positions = positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(ruleSettings: Settings) {
|
public evaluate(ruleSettings: Settings) {
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
export class EmergencyFundSetup extends Rule<Settings> {
|
||||||
|
private emergencyFund: number;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
|
emergencyFund: number
|
||||||
|
) {
|
||||||
|
super(exchangeRateDataService, {
|
||||||
|
name: 'Emergency Fund: Set up'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emergencyFund = emergencyFund;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(ruleSettings: Settings) {
|
||||||
|
if (this.emergencyFund > ruleSettings.threshold) {
|
||||||
|
return {
|
||||||
|
evaluation: 'An emergency fund has been set up',
|
||||||
|
value: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
evaluation: 'No emergency fund has been set up',
|
||||||
|
value: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSettings(aUserSettings: UserSettings): Settings {
|
||||||
|
return {
|
||||||
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
|
isActive: true,
|
||||||
|
threshold: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Settings extends RuleSettings {
|
||||||
|
baseCurrency: string;
|
||||||
|
threshold: number;
|
||||||
|
}
|
@ -1,22 +1,29 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
|
||||||
|
|
||||||
export class FeeRatioInitialInvestment extends Rule<Settings> {
|
export class FeeRatioInitialInvestment extends Rule<Settings> {
|
||||||
|
private fees: number;
|
||||||
|
private totalInvestment: number;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected exchangeRateDataService: ExchangeRateDataService,
|
protected exchangeRateDataService: ExchangeRateDataService,
|
||||||
private totalInvestment: number,
|
totalInvestment: number,
|
||||||
private fees: number
|
fees: number
|
||||||
) {
|
) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Investment'
|
name: 'Fee Ratio'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.fees = fees;
|
||||||
|
this.totalInvestment = totalInvestment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(ruleSettings: Settings) {
|
public evaluate(ruleSettings: Settings) {
|
||||||
const feeRatio = this.fees / this.totalInvestment;
|
const feeRatio = this.totalInvestment
|
||||||
|
? this.fees / this.totalInvestment
|
||||||
|
: 0;
|
||||||
|
|
||||||
if (feeRatio > ruleSettings.threshold) {
|
if (feeRatio > ruleSettings.threshold) {
|
||||||
return {
|
return {
|
||||||
|
@ -8,14 +8,17 @@ export class ApiService {
|
|||||||
public buildFiltersFromQueryParams({
|
public buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
|
filterBySearchQuery,
|
||||||
filterByTags
|
filterByTags
|
||||||
}: {
|
}: {
|
||||||
filterByAccounts?: string;
|
filterByAccounts?: string;
|
||||||
filterByAssetClasses?: string;
|
filterByAssetClasses?: string;
|
||||||
|
filterBySearchQuery?: string;
|
||||||
filterByTags?: string;
|
filterByTags?: string;
|
||||||
}): Filter[] {
|
}): Filter[] {
|
||||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||||
|
const searchQuery = filterBySearchQuery?.toLowerCase();
|
||||||
const tagIds = filterByTags?.split(',') ?? [];
|
const tagIds = filterByTags?.split(',') ?? [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -31,6 +34,10 @@ export class ApiService {
|
|||||||
type: 'ASSET_CLASS'
|
type: 'ASSET_CLASS'
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
id: searchQuery,
|
||||||
|
type: 'SEARCH_QUERY'
|
||||||
|
},
|
||||||
...tagIds.map((tagId) => {
|
...tagIds.map((tagId) => {
|
||||||
return <Filter>{
|
return <Filter>{
|
||||||
id: tagId,
|
id: tagId,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
|
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
|
||||||
import { DEFAULT_CURRENCY, DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
|
import { DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
|
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
|
||||||
@ -12,10 +12,6 @@ export class ConfigurationService {
|
|||||||
this.environmentConfiguration = cleanEnv(process.env, {
|
this.environmentConfiguration = cleanEnv(process.env, {
|
||||||
ACCESS_TOKEN_SALT: str(),
|
ACCESS_TOKEN_SALT: str(),
|
||||||
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
|
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
|
||||||
BASE_CURRENCY: str({
|
|
||||||
choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'],
|
|
||||||
default: DEFAULT_CURRENCY
|
|
||||||
}),
|
|
||||||
BETTER_UPTIME_API_KEY: str({ default: '' }),
|
BETTER_UPTIME_API_KEY: str({ default: '' }),
|
||||||
CACHE_QUOTES_TTL: num({ default: 1 }),
|
CACHE_QUOTES_TTL: num({ default: 1 }),
|
||||||
CACHE_TTL: num({ default: 1 }),
|
CACHE_TTL: num({ default: 1 }),
|
||||||
|
@ -13,6 +13,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { Job } from 'bull';
|
import { Job } from 'bull';
|
||||||
import {
|
import {
|
||||||
|
addDays,
|
||||||
format,
|
format,
|
||||||
getDate,
|
getDate,
|
||||||
getMonth,
|
getMonth,
|
||||||
@ -101,15 +102,7 @@ export class DataGatheringProcessor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count month one up for iteration
|
currentDate = addDays(currentDate, 1);
|
||||||
currentDate = new Date(
|
|
||||||
Date.UTC(
|
|
||||||
getYear(currentDate),
|
|
||||||
getMonth(currentDate),
|
|
||||||
getDate(currentDate) + 1,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.marketDataService.updateMany({ data });
|
await this.marketDataService.updateMany({ data });
|
||||||
|
@ -127,12 +127,14 @@ export class DataGatheringService {
|
|||||||
uniqueAssets = await this.getUniqueAssets();
|
uniqueAssets = await this.getUniqueAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetProfiles = await this.dataProviderService.getAssetProfiles(
|
if (uniqueAssets.length <= 0) {
|
||||||
uniqueAssets
|
return;
|
||||||
);
|
}
|
||||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
|
||||||
uniqueAssets
|
const assetProfiles =
|
||||||
);
|
await this.dataProviderService.getAssetProfiles(uniqueAssets);
|
||||||
|
const symbolProfiles =
|
||||||
|
await this.symbolProfileService.getSymbolProfiles(uniqueAssets);
|
||||||
|
|
||||||
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
|
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
|
||||||
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
||||||
@ -147,7 +149,9 @@ export class DataGatheringService {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
`Failed to enhance data for symbol ${symbol} by ${dataEnhancer.getName()}`,
|
`Failed to enhance data for ${symbol} (${
|
||||||
|
assetProfile.dataSource
|
||||||
|
}) by ${dataEnhancer.getName()}`,
|
||||||
error,
|
error,
|
||||||
'DataGatheringService'
|
'DataGatheringService'
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,7 @@ import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
|||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
|
import * as Alphavantage from 'alphavantage';
|
||||||
import { format, isAfter, isBefore, parse } from 'date-fns';
|
import { format, isAfter, isBefore, parse } from 'date-fns';
|
||||||
|
|
||||||
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
||||||
@ -20,7 +21,7 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService
|
||||||
) {
|
) {
|
||||||
this.alphaVantage = require('alphavantage')({
|
this.alphaVantage = Alphavantage({
|
||||||
key: this.configurationService.get('ALPHA_VANTAGE_API_KEY')
|
key: this.configurationService.get('ALPHA_VANTAGE_API_KEY')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -126,6 +127,9 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
return {
|
return {
|
||||||
items: result?.bestMatches?.map((bestMatch) => {
|
items: result?.bestMatches?.map((bestMatch) => {
|
||||||
return {
|
return {
|
||||||
|
assetClass: undefined,
|
||||||
|
assetSubClass: undefined,
|
||||||
|
currency: bestMatch['8. currency'],
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
name: bestMatch['2. name'],
|
name: bestMatch['2. name'],
|
||||||
symbol: bestMatch['1. symbol']
|
symbol: bestMatch['1. symbol']
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
DEFAULT_REQUEST_TIMEOUT
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
@ -20,14 +23,9 @@ import got from 'got';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CoinGeckoService implements DataProviderInterface {
|
export class CoinGeckoService implements DataProviderInterface {
|
||||||
private baseCurrency: string;
|
|
||||||
private readonly URL = 'https://api.coingecko.com/api/v3';
|
private readonly URL = 'https://api.coingecko.com/api/v3';
|
||||||
|
|
||||||
public constructor(
|
public constructor() {}
|
||||||
private readonly configurationService: ConfigurationService
|
|
||||||
) {
|
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
return true;
|
return true;
|
||||||
@ -39,13 +37,22 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
const response: Partial<SymbolProfile> = {
|
const response: Partial<SymbolProfile> = {
|
||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.CASH,
|
||||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||||
currency: this.baseCurrency,
|
currency: DEFAULT_CURRENCY,
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
symbol: aSymbol
|
symbol: aSymbol
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { name } = await got(`${this.URL}/coins/${aSymbol}`).json<any>();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
|
const { name } = await got(`${this.URL}/coins/${aSymbol}`, {
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}).json<any>();
|
||||||
|
|
||||||
response.name = name;
|
response.name = name;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -78,12 +85,22 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const { prices } = await got(
|
const { prices } = await got(
|
||||||
`${
|
`${
|
||||||
this.URL
|
this.URL
|
||||||
}/coins/${aSymbol}/market_chart/range?vs_currency=${this.baseCurrency.toLowerCase()}&from=${getUnixTime(
|
}/coins/${aSymbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime(
|
||||||
from
|
from
|
||||||
)}&to=${getUnixTime(to)}`
|
)}&to=${getUnixTime(to)}`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
const result: {
|
const result: {
|
||||||
@ -127,19 +144,29 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const response = await got(
|
const response = await got(
|
||||||
`${this.URL}/simple/price?ids=${aSymbols.join(
|
`${this.URL}/simple/price?ids=${aSymbols.join(
|
||||||
','
|
','
|
||||||
)}&vs_currencies=${this.baseCurrency.toLowerCase()}`
|
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
for (const symbol in response) {
|
for (const symbol in response) {
|
||||||
if (Object.prototype.hasOwnProperty.call(response, symbol)) {
|
if (Object.prototype.hasOwnProperty.call(response, symbol)) {
|
||||||
results[symbol] = {
|
results[symbol] = {
|
||||||
currency: this.baseCurrency,
|
currency: DEFAULT_CURRENCY,
|
||||||
dataProviderInfo: this.getDataProviderInfo(),
|
dataProviderInfo: this.getDataProviderInfo(),
|
||||||
dataSource: DataSource.COINGECKO,
|
dataSource: DataSource.COINGECKO,
|
||||||
marketPrice: response[symbol][this.baseCurrency.toLowerCase()],
|
marketPrice: response[symbol][DEFAULT_CURRENCY.toLowerCase()],
|
||||||
marketState: 'open'
|
marketState: 'open'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -165,9 +192,16 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
let items: LookupItem[] = [];
|
let items: LookupItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { coins } = await got(
|
const abortController = new AbortController();
|
||||||
`${this.URL}/search?query=${query}`
|
|
||||||
).json<any>();
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
|
const { coins } = await got(`${this.URL}/search?query=${query}`, {
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}).json<any>();
|
||||||
|
|
||||||
items = coins.map(({ id: symbol, name }) => {
|
items = coins.map(({ id: symbol, name }) => {
|
||||||
return {
|
return {
|
||||||
@ -175,7 +209,7 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
symbol,
|
symbol,
|
||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.CASH,
|
||||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||||
currency: this.baseCurrency,
|
currency: DEFAULT_CURRENCY,
|
||||||
dataSource: this.getName()
|
dataSource: this.getName()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -4,14 +4,18 @@ import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-p
|
|||||||
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
|
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { DataEnhancerService } from './data-enhancer.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
exports: [
|
exports: [
|
||||||
'DataEnhancers',
|
DataEnhancerService,
|
||||||
TrackinsightDataEnhancerService,
|
TrackinsightDataEnhancerService,
|
||||||
YahooFinanceDataEnhancerService
|
YahooFinanceDataEnhancerService,
|
||||||
|
'DataEnhancers'
|
||||||
],
|
],
|
||||||
imports: [ConfigurationModule, CryptocurrencyModule],
|
imports: [ConfigurationModule, CryptocurrencyModule],
|
||||||
providers: [
|
providers: [
|
||||||
|
DataEnhancerService,
|
||||||
TrackinsightDataEnhancerService,
|
TrackinsightDataEnhancerService,
|
||||||
YahooFinanceDataEnhancerService,
|
YahooFinanceDataEnhancerService,
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
|
import { HttpException, Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DataEnhancerService {
|
||||||
|
public constructor(
|
||||||
|
@Inject('DataEnhancers')
|
||||||
|
private readonly dataEnhancers: DataEnhancerInterface[]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async enhance(aName: string) {
|
||||||
|
const dataEnhancer = this.dataEnhancers.find((dataEnhancer) => {
|
||||||
|
return dataEnhancer.getName() === aName;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dataEnhancer) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const assetProfile = await dataEnhancer.enhance({
|
||||||
|
response: {
|
||||||
|
assetClass: 'EQUITY',
|
||||||
|
assetSubClass: 'ETF'
|
||||||
|
},
|
||||||
|
symbol: dataEnhancer.getTestSymbol()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
(assetProfile.countries as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||||
|
(assetProfile.sectors as unknown as Prisma.JsonArray)?.length > 0
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
|
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -7,7 +8,7 @@ import got from 'got';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||||
private static baseUrl = 'https://data.trackinsight.com';
|
private static baseUrl = 'https://www.trackinsight.com/data-api';
|
||||||
private static countries = require('countries-list/dist/countries.json');
|
private static countries = require('countries-list/dist/countries.json');
|
||||||
private static countriesMapping = {
|
private static countriesMapping = {
|
||||||
'Russian Federation': 'Russia'
|
'Russian Federation': 'Russia'
|
||||||
@ -32,30 +33,82 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const profile = await got(
|
const profile = await got(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
|
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.json<any>()
|
.json<any>()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return {};
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
|
return got(
|
||||||
|
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
|
||||||
|
'.'
|
||||||
|
)?.[0]}.json`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.json<any>()
|
||||||
|
.catch(() => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const isin = profile.isin?.split(';')?.[0];
|
const isin = profile?.isin?.split(';')?.[0];
|
||||||
|
|
||||||
if (isin) {
|
if (isin) {
|
||||||
response.isin = isin;
|
response.isin = isin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const holdings = await got(
|
const holdings = await got(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
|
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.json<any>()
|
.json<any>()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
return got(
|
return got(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
|
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
|
||||||
symbol.split('.')?.[0]
|
'.'
|
||||||
}.json`
|
)?.[0]}.json`,
|
||||||
);
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.json<any>()
|
||||||
|
.catch(() => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (holdings?.weight < 0.95) {
|
if (holdings?.weight < 0.95) {
|
||||||
@ -114,4 +167,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
public getName() {
|
public getName() {
|
||||||
return 'TRACKINSIGHT';
|
return 'TRACKINSIGHT';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTestSymbol() {
|
||||||
|
return 'QQQ';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||||
|
|
||||||
import { YahooFinanceDataEnhancerService } from './yahoo-finance.service';
|
import { YahooFinanceDataEnhancerService } from './yahoo-finance.service';
|
||||||
@ -26,16 +25,13 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('YahooFinanceDataEnhancerService', () => {
|
describe('YahooFinanceDataEnhancerService', () => {
|
||||||
let configurationService: ConfigurationService;
|
|
||||||
let cryptocurrencyService: CryptocurrencyService;
|
let cryptocurrencyService: CryptocurrencyService;
|
||||||
let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService;
|
let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
configurationService = new ConfigurationService();
|
|
||||||
cryptocurrencyService = new CryptocurrencyService();
|
cryptocurrencyService = new CryptocurrencyService();
|
||||||
|
|
||||||
yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService(
|
yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService(
|
||||||
configurationService,
|
|
||||||
cryptocurrencyService
|
cryptocurrencyService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { isCurrency } from '@ghostfolio/common/helper';
|
import { isCurrency } from '@ghostfolio/common/helper';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
AssetClass,
|
AssetClass,
|
||||||
AssetSubClass,
|
AssetSubClass,
|
||||||
DataSource,
|
DataSource,
|
||||||
|
Prisma,
|
||||||
SymbolProfile
|
SymbolProfile
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { countries } from 'countries-list';
|
import { countries } from 'countries-list';
|
||||||
@ -16,23 +16,18 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||||
private baseCurrency: string;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
|
||||||
private readonly cryptocurrencyService: CryptocurrencyService
|
private readonly cryptocurrencyService: CryptocurrencyService
|
||||||
) {
|
) {}
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
|
||||||
|
|
||||||
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||||
let symbol = aYahooFinanceSymbol.replace(
|
let symbol = aYahooFinanceSymbol.replace(
|
||||||
new RegExp(`-${this.baseCurrency}$`),
|
new RegExp(`-${DEFAULT_CURRENCY}$`),
|
||||||
this.baseCurrency
|
DEFAULT_CURRENCY
|
||||||
);
|
);
|
||||||
|
|
||||||
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
|
if (symbol.includes('=X') && !symbol.includes(DEFAULT_CURRENCY)) {
|
||||||
symbol = `${this.baseCurrency}${symbol}`;
|
symbol = `${DEFAULT_CURRENCY}${symbol}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return symbol.replace('=X', '');
|
return symbol.replace('=X', '');
|
||||||
@ -47,21 +42,18 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
|||||||
*/
|
*/
|
||||||
public convertToYahooFinanceSymbol(aSymbol: string) {
|
public convertToYahooFinanceSymbol(aSymbol: string) {
|
||||||
if (
|
if (
|
||||||
aSymbol.includes(this.baseCurrency) &&
|
aSymbol.includes(DEFAULT_CURRENCY) &&
|
||||||
aSymbol.length > this.baseCurrency.length
|
aSymbol.length > DEFAULT_CURRENCY.length
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
isCurrency(
|
isCurrency(
|
||||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return `${aSymbol}=X`;
|
return `${aSymbol}=X`;
|
||||||
} else if (
|
} else if (
|
||||||
this.cryptocurrencyService.isCryptocurrency(
|
this.cryptocurrencyService.isCryptocurrency(
|
||||||
aSymbol.replace(
|
aSymbol.replace(new RegExp(`-${DEFAULT_CURRENCY}$`), DEFAULT_CURRENCY)
|
||||||
new RegExp(`-${this.baseCurrency}$`),
|
|
||||||
this.baseCurrency
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Add a dash before the last three characters
|
// Add a dash before the last three characters
|
||||||
@ -69,8 +61,8 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
|||||||
// DOGEUSD -> DOGE-USD
|
// DOGEUSD -> DOGE-USD
|
||||||
// SOL1USD -> SOL1-USD
|
// SOL1USD -> SOL1-USD
|
||||||
return aSymbol.replace(
|
return aSymbol.replace(
|
||||||
new RegExp(`-?${this.baseCurrency}$`),
|
new RegExp(`-?${DEFAULT_CURRENCY}$`),
|
||||||
`-${this.baseCurrency}`
|
`-${DEFAULT_CURRENCY}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,15 +91,14 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
|||||||
yahooSymbol = quotes[0].symbol;
|
yahooSymbol = quotes[0].symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { countries, sectors, url } = await this.getAssetProfile(
|
const { countries, sectors, url } =
|
||||||
yahooSymbol
|
await this.getAssetProfile(yahooSymbol);
|
||||||
);
|
|
||||||
|
|
||||||
if (countries) {
|
if ((countries as unknown as Prisma.JsonArray)?.length > 0) {
|
||||||
response.countries = countries;
|
response.countries = countries;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sectors) {
|
if ((sectors as unknown as Prisma.JsonArray)?.length > 0) {
|
||||||
response.sectors = sectors;
|
response.sectors = sectors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,6 +225,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
|||||||
return DataSource.YAHOO;
|
return DataSource.YAHOO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTestSymbol() {
|
||||||
|
return 'AAPL';
|
||||||
|
}
|
||||||
|
|
||||||
public parseAssetClass({
|
public parseAssetClass({
|
||||||
quoteType,
|
quoteType,
|
||||||
shortName
|
shortName
|
||||||
|
@ -5,7 +5,10 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
DEFAULT_REQUEST_TIMEOUT
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@ -18,19 +21,16 @@ import {
|
|||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { format, isToday } from 'date-fns';
|
import { format, isToday } from 'date-fns';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
import ms from 'ms';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EodHistoricalDataService implements DataProviderInterface {
|
export class EodHistoricalDataService implements DataProviderInterface {
|
||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
private baseCurrency: string;
|
|
||||||
private readonly URL = 'https://eodhistoricaldata.com/api';
|
private readonly URL = 'https://eodhistoricaldata.com/api';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService
|
||||||
) {
|
) {
|
||||||
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
|
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
@ -78,6 +78,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
const symbol = this.convertToEodSymbol(aSymbol);
|
const symbol = this.convertToEodSymbol(aSymbol);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const response = await got(
|
const response = await got(
|
||||||
`${this.URL}/eod/${symbol}?api_token=${
|
`${this.URL}/eod/${symbol}?api_token=${
|
||||||
this.apiKey
|
this.apiKey
|
||||||
@ -86,9 +92,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)}&period={aGranularity}`,
|
)}&period={aGranularity}`,
|
||||||
{
|
{
|
||||||
timeout: {
|
// @ts-ignore
|
||||||
request: DEFAULT_REQUEST_TIMEOUT
|
signal: abortController.signal
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
@ -138,14 +143,19 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const realTimeResponse = await got(
|
const realTimeResponse = await got(
|
||||||
`${this.URL}/real-time/${symbols[0]}?api_token=${
|
`${this.URL}/real-time/${symbols[0]}?api_token=${
|
||||||
this.apiKey
|
this.apiKey
|
||||||
}&fmt=json&s=${symbols.join(',')}`,
|
}&fmt=json&s=${symbols.join(',')}`,
|
||||||
{
|
{
|
||||||
timeout: {
|
// @ts-ignore
|
||||||
request: DEFAULT_REQUEST_TIMEOUT
|
signal: abortController.signal
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
@ -176,7 +186,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
})?.currency;
|
})?.currency;
|
||||||
|
|
||||||
result[this.convertFromEodSymbol(code)] = {
|
result[this.convertFromEodSymbol(code)] = {
|
||||||
currency: currency ?? this.baseCurrency,
|
currency: currency ?? DEFAULT_CURRENCY,
|
||||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||||
marketPrice: close,
|
marketPrice: close,
|
||||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||||
@ -187,24 +197,24 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response[`${this.baseCurrency}GBP`]) {
|
if (response[`${DEFAULT_CURRENCY}GBP`]) {
|
||||||
response[`${this.baseCurrency}GBp`] = {
|
response[`${DEFAULT_CURRENCY}GBp`] = {
|
||||||
...response[`${this.baseCurrency}GBP`],
|
...response[`${DEFAULT_CURRENCY}GBP`],
|
||||||
currency: `${this.baseCurrency}GBp`,
|
currency: `${DEFAULT_CURRENCY}GBp`,
|
||||||
marketPrice: this.getConvertedValue({
|
marketPrice: this.getConvertedValue({
|
||||||
symbol: `${this.baseCurrency}GBp`,
|
symbol: `${DEFAULT_CURRENCY}GBp`,
|
||||||
value: response[`${this.baseCurrency}GBP`].marketPrice
|
value: response[`${DEFAULT_CURRENCY}GBP`].marketPrice
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response[`${this.baseCurrency}ILS`]) {
|
if (response[`${DEFAULT_CURRENCY}ILS`]) {
|
||||||
response[`${this.baseCurrency}ILA`] = {
|
response[`${DEFAULT_CURRENCY}ILA`] = {
|
||||||
...response[`${this.baseCurrency}ILS`],
|
...response[`${DEFAULT_CURRENCY}ILS`],
|
||||||
currency: `${this.baseCurrency}ILA`,
|
currency: `${DEFAULT_CURRENCY}ILA`,
|
||||||
marketPrice: this.getConvertedValue({
|
marketPrice: this.getConvertedValue({
|
||||||
symbol: `${this.baseCurrency}ILA`,
|
symbol: `${DEFAULT_CURRENCY}ILA`,
|
||||||
value: response[`${this.baseCurrency}ILS`].marketPrice
|
value: response[`${DEFAULT_CURRENCY}ILS`].marketPrice
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -273,7 +283,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
if (symbol.endsWith('.FOREX')) {
|
if (symbol.endsWith('.FOREX')) {
|
||||||
symbol = symbol.replace('GBX', 'GBp');
|
symbol = symbol.replace('GBX', 'GBp');
|
||||||
symbol = symbol.replace('.FOREX', '');
|
symbol = symbol.replace('.FOREX', '');
|
||||||
symbol = `${this.baseCurrency}${symbol}`;
|
symbol = `${DEFAULT_CURRENCY}${symbol}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return symbol;
|
return symbol;
|
||||||
@ -286,17 +296,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
*/
|
*/
|
||||||
private convertToEodSymbol(aSymbol: string) {
|
private convertToEodSymbol(aSymbol: string) {
|
||||||
if (
|
if (
|
||||||
aSymbol.startsWith(this.baseCurrency) &&
|
aSymbol.startsWith(DEFAULT_CURRENCY) &&
|
||||||
aSymbol.length > this.baseCurrency.length
|
aSymbol.length > DEFAULT_CURRENCY.length
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
isCurrency(
|
isCurrency(
|
||||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return `${aSymbol
|
return `${aSymbol
|
||||||
.replace('GBp', 'GBX')
|
.replace('GBp', 'GBX')
|
||||||
.replace(this.baseCurrency, '')}.FOREX`;
|
.replace(DEFAULT_CURRENCY, '')}.FOREX`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,10 +320,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
value: number;
|
value: number;
|
||||||
}) {
|
}) {
|
||||||
if (symbol === `${this.baseCurrency}GBp`) {
|
if (symbol === `${DEFAULT_CURRENCY}GBp`) {
|
||||||
// Convert GPB to GBp (pence)
|
// Convert GPB to GBp (pence)
|
||||||
return new Big(value).mul(100).toNumber();
|
return new Big(value).mul(100).toNumber();
|
||||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
} else if (symbol === `${DEFAULT_CURRENCY}ILA`) {
|
||||||
// Convert ILS to ILA
|
// Convert ILS to ILA
|
||||||
return new Big(value).mul(100).toNumber();
|
return new Big(value).mul(100).toNumber();
|
||||||
}
|
}
|
||||||
@ -331,12 +341,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
let searchResult = [];
|
let searchResult = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const response = await got(
|
const response = await got(
|
||||||
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
|
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
|
||||||
{
|
{
|
||||||
timeout: {
|
// @ts-ignore
|
||||||
request: DEFAULT_REQUEST_TIMEOUT
|
signal: abortController.signal
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
|
@ -5,6 +5,10 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
DEFAULT_REQUEST_TIMEOUT
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
@ -16,7 +20,6 @@ import got from 'got';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class FinancialModelingPrepService implements DataProviderInterface {
|
export class FinancialModelingPrepService implements DataProviderInterface {
|
||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
private baseCurrency: string;
|
|
||||||
private readonly URL = 'https://financialmodelingprep.com/api/v3';
|
private readonly URL = 'https://financialmodelingprep.com/api/v3';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -25,7 +28,6 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
this.apiKey = this.configurationService.get(
|
this.apiKey = this.configurationService.get(
|
||||||
'FINANCIAL_MODELING_PREP_API_KEY'
|
'FINANCIAL_MODELING_PREP_API_KEY'
|
||||||
);
|
);
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
@ -64,8 +66,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const { historical } = await got(
|
const { historical } = await got(
|
||||||
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`
|
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
const result: {
|
const result: {
|
||||||
@ -111,13 +123,23 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const response = await got(
|
const response = await got(
|
||||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`
|
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
for (const { price, symbol } of response) {
|
for (const { price, symbol } of response) {
|
||||||
results[symbol] = {
|
results[symbol] = {
|
||||||
currency: this.baseCurrency,
|
currency: DEFAULT_CURRENCY,
|
||||||
dataProviderInfo: this.getDataProviderInfo(),
|
dataProviderInfo: this.getDataProviderInfo(),
|
||||||
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
||||||
marketPrice: price,
|
marketPrice: price,
|
||||||
@ -145,8 +167,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
let items: LookupItem[] = [];
|
let items: LookupItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const result = await got(
|
const result = await got(
|
||||||
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`
|
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
items = result.map(({ currency, name, symbol }) => {
|
items = result.map(({ currency, name, symbol }) => {
|
||||||
|
@ -10,4 +10,6 @@ export interface DataEnhancerInterface {
|
|||||||
}): Promise<Partial<SymbolProfile>>;
|
}): Promise<Partial<SymbolProfile>>;
|
||||||
|
|
||||||
getName(): string;
|
getName(): string;
|
||||||
|
|
||||||
|
getTestSymbol(): string;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
|
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
extractNumberFromString,
|
extractNumberFromString,
|
||||||
@ -95,7 +96,17 @@ export class ManualService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { body } = await got(url, { headers });
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
|
const { body } = await got(url, {
|
||||||
|
headers,
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
|
});
|
||||||
|
|
||||||
const $ = cheerio.load(body);
|
const $ = cheerio.load(body);
|
||||||
|
|
||||||
|
@ -5,7 +5,10 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_REQUEST_TIMEOUT,
|
||||||
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@ -135,6 +138,12 @@ export class RapidApiService implements DataProviderInterface {
|
|||||||
oneYearAgo: { value: number; valueText: string };
|
oneYearAgo: { value: number; valueText: string };
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, DEFAULT_REQUEST_TIMEOUT);
|
||||||
|
|
||||||
const { fgi } = await got(
|
const { fgi } = await got(
|
||||||
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
|
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
|
||||||
{
|
{
|
||||||
@ -142,7 +151,9 @@ export class RapidApiService implements DataProviderInterface {
|
|||||||
useQueryString: 'true',
|
useQueryString: 'true',
|
||||||
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
||||||
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
|
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
|
||||||
}
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
signal: abortController.signal
|
||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||||
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
|
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
|
||||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||||
@ -7,6 +6,7 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@ -18,15 +18,10 @@ import { Quote } from 'yahoo-finance2/dist/esm/src/modules/quote';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class YahooFinanceService implements DataProviderInterface {
|
export class YahooFinanceService implements DataProviderInterface {
|
||||||
private baseCurrency: string;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
|
||||||
private readonly cryptocurrencyService: CryptocurrencyService,
|
private readonly cryptocurrencyService: CryptocurrencyService,
|
||||||
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
|
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
|
||||||
) {
|
) {}
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
}
|
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
return true;
|
return true;
|
||||||
@ -212,50 +207,50 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
symbol === `${this.baseCurrency}GBP` &&
|
symbol === `${DEFAULT_CURRENCY}GBP` &&
|
||||||
yahooFinanceSymbols.includes(`${this.baseCurrency}GBp=X`)
|
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}GBp=X`)
|
||||||
) {
|
) {
|
||||||
// Convert GPB to GBp (pence)
|
// Convert GPB to GBp (pence)
|
||||||
response[`${this.baseCurrency}GBp`] = {
|
response[`${DEFAULT_CURRENCY}GBp`] = {
|
||||||
...response[symbol],
|
...response[symbol],
|
||||||
currency: 'GBp',
|
currency: 'GBp',
|
||||||
marketPrice: this.getConvertedValue({
|
marketPrice: this.getConvertedValue({
|
||||||
symbol: `${this.baseCurrency}GBp`,
|
symbol: `${DEFAULT_CURRENCY}GBp`,
|
||||||
value: response[symbol].marketPrice
|
value: response[symbol].marketPrice
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
symbol === `${this.baseCurrency}ILS` &&
|
symbol === `${DEFAULT_CURRENCY}ILS` &&
|
||||||
yahooFinanceSymbols.includes(`${this.baseCurrency}ILA=X`)
|
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}ILA=X`)
|
||||||
) {
|
) {
|
||||||
// Convert ILS to ILA
|
// Convert ILS to ILA
|
||||||
response[`${this.baseCurrency}ILA`] = {
|
response[`${DEFAULT_CURRENCY}ILA`] = {
|
||||||
...response[symbol],
|
...response[symbol],
|
||||||
currency: 'ILA',
|
currency: 'ILA',
|
||||||
marketPrice: this.getConvertedValue({
|
marketPrice: this.getConvertedValue({
|
||||||
symbol: `${this.baseCurrency}ILA`,
|
symbol: `${DEFAULT_CURRENCY}ILA`,
|
||||||
value: response[symbol].marketPrice
|
value: response[symbol].marketPrice
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
symbol === `${this.baseCurrency}ZAR` &&
|
symbol === `${DEFAULT_CURRENCY}ZAR` &&
|
||||||
yahooFinanceSymbols.includes(`${this.baseCurrency}ZAc=X`)
|
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}ZAc=X`)
|
||||||
) {
|
) {
|
||||||
// Convert ZAR to ZAc (cents)
|
// Convert ZAR to ZAc (cents)
|
||||||
response[`${this.baseCurrency}ZAc`] = {
|
response[`${DEFAULT_CURRENCY}ZAc`] = {
|
||||||
...response[symbol],
|
...response[symbol],
|
||||||
currency: 'ZAc',
|
currency: 'ZAc',
|
||||||
marketPrice: this.getConvertedValue({
|
marketPrice: this.getConvertedValue({
|
||||||
symbol: `${this.baseCurrency}ZAc`,
|
symbol: `${DEFAULT_CURRENCY}ZAc`,
|
||||||
value: response[symbol].marketPrice
|
value: response[symbol].marketPrice
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yahooFinanceSymbols.includes(`${this.baseCurrency}USX=X`)) {
|
if (yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}USX=X`)) {
|
||||||
// Convert USD to USX (cent)
|
// Convert USD to USX (cent)
|
||||||
response[`${this.baseCurrency}USX`] = {
|
response[`${DEFAULT_CURRENCY}USX`] = {
|
||||||
currency: 'USX',
|
currency: 'USX',
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
marketPrice: new Big(1).mul(100).toNumber(),
|
marketPrice: new Big(1).mul(100).toNumber(),
|
||||||
@ -303,8 +298,8 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
(quoteType === 'CRYPTOCURRENCY' &&
|
(quoteType === 'CRYPTOCURRENCY' &&
|
||||||
this.cryptocurrencyService.isCryptocurrency(
|
this.cryptocurrencyService.isCryptocurrency(
|
||||||
symbol.replace(
|
symbol.replace(
|
||||||
new RegExp(`-${this.baseCurrency}$`),
|
new RegExp(`-${DEFAULT_CURRENCY}$`),
|
||||||
this.baseCurrency
|
DEFAULT_CURRENCY
|
||||||
)
|
)
|
||||||
)) ||
|
)) ||
|
||||||
quoteTypes.includes(quoteType)
|
quoteTypes.includes(quoteType)
|
||||||
@ -314,7 +309,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
if (quoteType === 'CRYPTOCURRENCY') {
|
if (quoteType === 'CRYPTOCURRENCY') {
|
||||||
// Only allow cryptocurrencies in base currency to avoid having redundancy in the database.
|
// Only allow cryptocurrencies in base currency to avoid having redundancy in the database.
|
||||||
// Transactions need to be converted manually to the base currency before
|
// Transactions need to be converted manually to the base currency before
|
||||||
return symbol.includes(this.baseCurrency);
|
return symbol.includes(DEFAULT_CURRENCY);
|
||||||
} else if (quoteType === 'FUTURE') {
|
} else if (quoteType === 'FUTURE') {
|
||||||
// Allow GC=F, but not MGC=F
|
// Allow GC=F, but not MGC=F
|
||||||
return symbol.length === 4;
|
return symbol.length === 4;
|
||||||
@ -373,13 +368,13 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
value: number;
|
value: number;
|
||||||
}) {
|
}) {
|
||||||
if (symbol === `${this.baseCurrency}GBp`) {
|
if (symbol === `${DEFAULT_CURRENCY}GBp`) {
|
||||||
// Convert GPB to GBp (pence)
|
// Convert GPB to GBp (pence)
|
||||||
return new Big(value).mul(100).toNumber();
|
return new Big(value).mul(100).toNumber();
|
||||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
} else if (symbol === `${DEFAULT_CURRENCY}ILA`) {
|
||||||
// Convert ILS to ILA
|
// Convert ILS to ILA
|
||||||
return new Big(value).mul(100).toNumber();
|
return new Big(value).mul(100).toNumber();
|
||||||
} else if (symbol === `${this.baseCurrency}ZAc`) {
|
} else if (symbol === `${DEFAULT_CURRENCY}ZAc`) {
|
||||||
// Convert ZAR to ZAc (cents)
|
// Convert ZAR to ZAc (cents)
|
||||||
return new Big(value).mul(100).toNumber();
|
return new Big(value).mul(100).toNumber();
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
PROPERTY_CURRENCIES
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { format, isToday } from 'date-fns';
|
import { format, isToday } from 'date-fns';
|
||||||
@ -12,13 +14,11 @@ import { isNumber, uniq } from 'lodash';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExchangeRateDataService {
|
export class ExchangeRateDataService {
|
||||||
private baseCurrency: string;
|
|
||||||
private currencies: string[] = [];
|
private currencies: string[] = [];
|
||||||
private currencyPairs: IDataGatheringItem[] = [];
|
private currencyPairs: IDataGatheringItem[] = [];
|
||||||
private exchangeRates: { [currencyPair: string]: number } = {};
|
private exchangeRates: { [currencyPair: string]: number } = {};
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
@ -26,7 +26,7 @@ export class ExchangeRateDataService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public getCurrencies() {
|
public getCurrencies() {
|
||||||
return this.currencies?.length > 0 ? this.currencies : [this.baseCurrency];
|
return this.currencies?.length > 0 ? this.currencies : [DEFAULT_CURRENCY];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrencyPairs() {
|
public getCurrencyPairs() {
|
||||||
@ -43,7 +43,6 @@ export class ExchangeRateDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
|
||||||
this.currencies = await this.prepareCurrencies();
|
this.currencies = await this.prepareCurrencies();
|
||||||
this.currencyPairs = [];
|
this.currencyPairs = [];
|
||||||
this.exchangeRates = {};
|
this.exchangeRates = {};
|
||||||
@ -113,9 +112,9 @@ export class ExchangeRateDataService {
|
|||||||
if (!this.exchangeRates[symbol]) {
|
if (!this.exchangeRates[symbol]) {
|
||||||
// Not found, calculate indirectly via base currency
|
// Not found, calculate indirectly via base currency
|
||||||
this.exchangeRates[symbol] =
|
this.exchangeRates[symbol] =
|
||||||
resultExtended[`${currency1}${this.baseCurrency}`]?.[date]
|
resultExtended[`${currency1}${DEFAULT_CURRENCY}`]?.[date]
|
||||||
?.marketPrice *
|
?.marketPrice *
|
||||||
resultExtended[`${this.baseCurrency}${currency2}`]?.[date]
|
resultExtended[`${DEFAULT_CURRENCY}${currency2}`]?.[date]
|
||||||
?.marketPrice;
|
?.marketPrice;
|
||||||
|
|
||||||
// Calculate the opposite direction
|
// Calculate the opposite direction
|
||||||
@ -144,9 +143,8 @@ export class ExchangeRateDataService {
|
|||||||
} else {
|
} else {
|
||||||
// Calculate indirectly via base currency
|
// Calculate indirectly via base currency
|
||||||
const factor1 =
|
const factor1 =
|
||||||
this.exchangeRates[`${aFromCurrency}${this.baseCurrency}`];
|
this.exchangeRates[`${aFromCurrency}${DEFAULT_CURRENCY}`];
|
||||||
const factor2 =
|
const factor2 = this.exchangeRates[`${DEFAULT_CURRENCY}${aToCurrency}`];
|
||||||
this.exchangeRates[`${this.baseCurrency}${aToCurrency}`];
|
|
||||||
|
|
||||||
factor = factor1 * factor2;
|
factor = factor1 * factor2;
|
||||||
|
|
||||||
@ -204,28 +202,28 @@ export class ExchangeRateDataService {
|
|||||||
let marketPriceBaseCurrencyToCurrency: number;
|
let marketPriceBaseCurrencyToCurrency: number;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.baseCurrency === aFromCurrency) {
|
if (aFromCurrency === DEFAULT_CURRENCY) {
|
||||||
marketPriceBaseCurrencyFromCurrency = 1;
|
marketPriceBaseCurrencyFromCurrency = 1;
|
||||||
} else {
|
} else {
|
||||||
marketPriceBaseCurrencyFromCurrency = (
|
marketPriceBaseCurrencyFromCurrency = (
|
||||||
await this.marketDataService.get({
|
await this.marketDataService.get({
|
||||||
dataSource,
|
dataSource,
|
||||||
date: aDate,
|
date: aDate,
|
||||||
symbol: `${this.baseCurrency}${aFromCurrency}`
|
symbol: `${DEFAULT_CURRENCY}${aFromCurrency}`
|
||||||
})
|
})
|
||||||
)?.marketPrice;
|
)?.marketPrice;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.baseCurrency === aToCurrency) {
|
if (aToCurrency === DEFAULT_CURRENCY) {
|
||||||
marketPriceBaseCurrencyToCurrency = 1;
|
marketPriceBaseCurrencyToCurrency = 1;
|
||||||
} else {
|
} else {
|
||||||
marketPriceBaseCurrencyToCurrency = (
|
marketPriceBaseCurrencyToCurrency = (
|
||||||
await this.marketDataService.get({
|
await this.marketDataService.get({
|
||||||
dataSource,
|
dataSource,
|
||||||
date: aDate,
|
date: aDate,
|
||||||
symbol: `${this.baseCurrency}${aToCurrency}`
|
symbol: `${DEFAULT_CURRENCY}${aToCurrency}`
|
||||||
})
|
})
|
||||||
)?.marketPrice;
|
)?.marketPrice;
|
||||||
}
|
}
|
||||||
@ -295,14 +293,14 @@ export class ExchangeRateDataService {
|
|||||||
private prepareCurrencyPairs(aCurrencies: string[]) {
|
private prepareCurrencyPairs(aCurrencies: string[]) {
|
||||||
return aCurrencies
|
return aCurrencies
|
||||||
.filter((currency) => {
|
.filter((currency) => {
|
||||||
return currency !== this.baseCurrency;
|
return currency !== DEFAULT_CURRENCY;
|
||||||
})
|
})
|
||||||
.map((currency) => {
|
.map((currency) => {
|
||||||
return {
|
return {
|
||||||
currency1: this.baseCurrency,
|
currency1: DEFAULT_CURRENCY,
|
||||||
currency2: currency,
|
currency2: currency,
|
||||||
dataSource: this.dataProviderService.getDataSourceForExchangeRates(),
|
dataSource: this.dataProviderService.getDataSourceForExchangeRates(),
|
||||||
symbol: `${this.baseCurrency}${currency}`
|
symbol: `${DEFAULT_CURRENCY}${currency}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { CleanedEnvAccessors } from 'envalid';
|
|||||||
export interface Environment extends CleanedEnvAccessors {
|
export interface Environment extends CleanedEnvAccessors {
|
||||||
ACCESS_TOKEN_SALT: string;
|
ACCESS_TOKEN_SALT: string;
|
||||||
ALPHA_VANTAGE_API_KEY: string;
|
ALPHA_VANTAGE_API_KEY: string;
|
||||||
BASE_CURRENCY: string;
|
|
||||||
BETTER_UPTIME_API_KEY: string;
|
BETTER_UPTIME_API_KEY: string;
|
||||||
CACHE_QUOTES_TTL: number;
|
CACHE_QUOTES_TTL: number;
|
||||||
CACHE_TTL: number;
|
CACHE_TTL: number;
|
||||||
|
@ -65,9 +65,8 @@ export class TwitterBotService {
|
|||||||
status += benchmarkListing;
|
status += benchmarkListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: createdTweet } = await this.twitterClient.v2.tweet(
|
const { data: createdTweet } =
|
||||||
status
|
await this.twitterClient.v2.tweet(status);
|
||||||
);
|
|
||||||
|
|
||||||
Logger.log(
|
Logger.log(
|
||||||
`Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}`,
|
`Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}`,
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"tsConfig": "apps/client/tsconfig.app.json",
|
"tsConfig": "apps/client/tsconfig.app.json",
|
||||||
"assets": [],
|
"assets": [],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"apps/client/src/assets/fonts/inter.css",
|
||||||
"apps/client/src/styles/theme.scss",
|
"apps/client/src/styles/theme.scss",
|
||||||
"apps/client/src/styles.scss"
|
"apps/client/src/styles.scss"
|
||||||
],
|
],
|
||||||
@ -63,6 +64,10 @@
|
|||||||
"baseHref": "/pt/",
|
"baseHref": "/pt/",
|
||||||
"localize": ["pt"]
|
"localize": ["pt"]
|
||||||
},
|
},
|
||||||
|
"development-tr": {
|
||||||
|
"baseHref": "/tr/",
|
||||||
|
"localize": ["tr"]
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
{
|
{
|
||||||
@ -99,40 +104,40 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"command": "mkdir -p dist/apps/client"
|
"command": "shx mkdir -p dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp -r apps/client/src/assets dist/apps/client"
|
"command": "shx cp -r apps/client/src/assets dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp -r apps/client/src/assets/.well-known dist/apps/client"
|
"command": "shx cp -r apps/client/src/assets/.well-known dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp apps/client/src/assets/favicon.ico dist/apps/client"
|
"command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp apps/client/src/assets/index.html dist/apps/client"
|
"command": "shx cp apps/client/src/assets/index.html dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp apps/client/src/assets/robots.txt dist/apps/client"
|
"command": "shx cp apps/client/src/assets/robots.txt dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp apps/client/src/assets/site.webmanifest dist/apps/client"
|
"command": "shx cp apps/client/src/assets/site.webmanifest dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp node_modules/ionicons/dist/index.js dist/apps/client"
|
"command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp node_modules/ionicons/dist/ionicons.js dist/apps/client"
|
"command": "shx cp node_modules/ionicons/dist/ionicons.js dist/apps/client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp -r node_modules/ionicons/dist/ionicons dist/apps/client/ionicons"
|
"command": "shx cp -r node_modules/ionicons/dist/ionicons dist/apps/client/ionicons"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp CHANGELOG.md dist/apps/client/assets"
|
"command": "shx cp CHANGELOG.md dist/apps/client/assets"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cp LICENSE dist/apps/client/assets"
|
"command": "shx cp LICENSE dist/apps/client/assets"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -165,6 +170,9 @@
|
|||||||
"development-pt": {
|
"development-pt": {
|
||||||
"browserTarget": "client:build:development-pt"
|
"browserTarget": "client:build:development-pt"
|
||||||
},
|
},
|
||||||
|
"development-tr": {
|
||||||
|
"browserTarget": "client:build:development-tr"
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "client:build:production"
|
"browserTarget": "client:build:production"
|
||||||
}
|
}
|
||||||
@ -182,7 +190,8 @@
|
|||||||
"messages.fr.xlf",
|
"messages.fr.xlf",
|
||||||
"messages.it.xlf",
|
"messages.it.xlf",
|
||||||
"messages.nl.xlf",
|
"messages.nl.xlf",
|
||||||
"messages.pt.xlf"
|
"messages.pt.xlf",
|
||||||
|
"messages.tr.xlf"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -226,6 +235,10 @@
|
|||||||
"pt": {
|
"pt": {
|
||||||
"baseHref": "/pt/",
|
"baseHref": "/pt/",
|
||||||
"translation": "apps/client/src/locales/messages.pt.xlf"
|
"translation": "apps/client/src/locales/messages.pt.xlf"
|
||||||
|
},
|
||||||
|
"tr": {
|
||||||
|
"baseHref": "/tr/",
|
||||||
|
"translation": "apps/client/src/locales/messages.tr.xlf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sourceLocale": "en"
|
"sourceLocale": "en"
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes, TitleStrategy } from '@angular/router';
|
import { RouterModule, Routes, TitleStrategy } from '@angular/router';
|
||||||
import { routes as aboutRoutes } from '@ghostfolio/client/pages/about/routes';
|
|
||||||
import { routes as faqRoutes } from '@ghostfolio/client/pages/faq/routes';
|
|
||||||
import { routes as featuresRoutes } from '@ghostfolio/client/pages/features/routes';
|
|
||||||
import { routes as marketsRoutes } from '@ghostfolio/client/pages/markets/routes';
|
|
||||||
import { routes as pricingRoutes } from '@ghostfolio/client/pages/pricing/routes';
|
|
||||||
import { routes as registerRoutes } from '@ghostfolio/client/pages/register/routes';
|
|
||||||
import { routes as resourcesRoutes } from '@ghostfolio/client/pages/resources/routes';
|
|
||||||
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
|
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
|
||||||
|
|
||||||
import { ModulePreloadService } from './core/module-preload.service';
|
import { ModulePreloadService } from './core/module-preload.service';
|
||||||
|
|
||||||
|
export const paths = {
|
||||||
|
about: $localize`about`,
|
||||||
|
faq: $localize`faq`,
|
||||||
|
features: $localize`features`,
|
||||||
|
license: $localize`license`,
|
||||||
|
markets: $localize`markets`,
|
||||||
|
pricing: $localize`pricing`,
|
||||||
|
privacyPolicy: $localize`privacy-policy`,
|
||||||
|
register: $localize`register`,
|
||||||
|
resources: $localize`resources`
|
||||||
|
};
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
...aboutRoutes.map((path) => ({
|
{
|
||||||
path,
|
path: paths.about,
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||||
})),
|
},
|
||||||
{
|
{
|
||||||
path: 'account',
|
path: 'account',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -51,30 +56,30 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
||||||
},
|
},
|
||||||
...faqRoutes.map((path) => ({
|
{
|
||||||
path,
|
path: paths.faq,
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||||
})),
|
},
|
||||||
...featuresRoutes.map((path) => ({
|
{
|
||||||
path,
|
path: paths.features,
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/features/features-page.module').then(
|
import('./pages/features/features-page.module').then(
|
||||||
(m) => m.FeaturesPageModule
|
(m) => m.FeaturesPageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||||
},
|
},
|
||||||
...marketsRoutes.map((path) => ({
|
{
|
||||||
path,
|
path: paths.markets,
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/markets/markets-page.module').then(
|
import('./pages/markets/markets-page.module').then(
|
||||||
(m) => m.MarketsPageModule
|
(m) => m.MarketsPageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
{
|
{
|
||||||
path: 'open',
|
path: 'open',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -94,27 +99,27 @@ const routes: Routes = [
|
|||||||
(m) => m.PortfolioPageModule
|
(m) => m.PortfolioPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
...pricingRoutes.map((path) => ({
|
{
|
||||||
path,
|
path: paths.pricing,
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/pricing/pricing-page.module').then(
|
import('./pages/pricing/pricing-page.module').then(
|
||||||
(m) => m.PricingPageModule
|
(m) => m.PricingPageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
...registerRoutes.map((path) => ({
|
{
|
||||||
path,
|
path: paths.register,
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/register/register-page.module').then(
|
import('./pages/register/register-page.module').then(
|
||||||
(m) => m.RegisterPageModule
|
(m) => m.RegisterPageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
...resourcesRoutes.map((path) => ({
|
{
|
||||||
path,
|
path: paths.resources,
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/resources/resources-page.module').then(
|
import('./pages/resources/resources-page.module').then(
|
||||||
(m) => m.ResourcesPageModule
|
(m) => m.ResourcesPageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
{
|
{
|
||||||
path: 'start',
|
path: 'start',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -1,37 +1,26 @@
|
|||||||
<header>
|
<header>
|
||||||
<gf-header
|
|
||||||
class="position-fixed w-100"
|
|
||||||
[currentRoute]="currentRoute"
|
|
||||||
[info]="info"
|
|
||||||
[pageTitle]="pageTitle"
|
|
||||||
[user]="user"
|
|
||||||
(signOut)="onSignOut()"
|
|
||||||
></gf-header>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main role="main">
|
|
||||||
<div
|
<div
|
||||||
*ngIf="canCreateAccount || (info?.systemMessage && user)"
|
*ngIf="canCreateAccount || (info?.systemMessage && user)"
|
||||||
class="container info-message-container"
|
class="info-message-container"
|
||||||
>
|
>
|
||||||
<div class="row">
|
<div class="info-message-inner-container position-fixed w-100">
|
||||||
<div class="col-md-8 offset-md-2 text-center">
|
<div class="align-items-center d-flex h-100 justify-content-center">
|
||||||
<a
|
<a
|
||||||
*ngIf="canCreateAccount"
|
*ngIf="canCreateAccount"
|
||||||
class="text-center"
|
class="text-center"
|
||||||
[routerLink]="['/register']"
|
[routerLink]="routerLinkRegister"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
class="cursor-pointer d-inline-block info-message"
|
||||||
(click)="onCreateAccount()"
|
(click)="onCreateAccount()"
|
||||||
>
|
>
|
||||||
<span>You are using the Live Demo.</span>
|
<span i18n>You are using the Live Demo.</span>
|
||||||
<span class="a ml-2">Create Account</span>
|
<span class="a ml-2" i18n>Create Account</span>
|
||||||
</div></a
|
</div></a
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
||||||
class="cursor-pointer d-inline-block info-message px-3 py-2 text-truncate"
|
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||||
(click)="onShowSystemMessage()"
|
(click)="onShowSystemMessage()"
|
||||||
>
|
>
|
||||||
{{ info.systemMessage }}
|
{{ info.systemMessage }}
|
||||||
@ -40,25 +29,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<gf-header
|
||||||
|
class="position-fixed w-100"
|
||||||
|
[currentRoute]="currentRoute"
|
||||||
|
[deviceType]="deviceType"
|
||||||
|
[hasTabs]="hasTabs"
|
||||||
|
[info]="info"
|
||||||
|
[pageTitle]="pageTitle"
|
||||||
|
[user]="user"
|
||||||
|
(signOut)="onSignOut()"
|
||||||
|
></gf-header>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main role="main">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer
|
<footer *ngIf="showFooter" class="d-flex justify-content-center py-4 w-100">
|
||||||
*ngIf="
|
|
||||||
(currentRoute === 'blog' ||
|
|
||||||
currentRoute === 'faq' ||
|
|
||||||
currentRoute === 'features' ||
|
|
||||||
currentRoute === 'markets' ||
|
|
||||||
currentRoute === 'open' ||
|
|
||||||
currentRoute === 'p' ||
|
|
||||||
currentRoute === 'pricing' ||
|
|
||||||
currentRoute === 'resources' ||
|
|
||||||
currentRoute === 'register' ||
|
|
||||||
currentRoute === 'start') &&
|
|
||||||
deviceType !== 'mobile'
|
|
||||||
"
|
|
||||||
class="d-flex justify-content-center py-4 w-100"
|
|
||||||
>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
@ -68,36 +55,38 @@
|
|||||||
<div class="h6 mt-2" i18n>Personal Finance</div>
|
<div class="h6 mt-2" i18n>Personal Finance</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li *ngIf="hasPermissionToAccessFearAndGreedIndex">
|
<li *ngIf="hasPermissionToAccessFearAndGreedIndex">
|
||||||
<a i18n [routerLink]="['/markets']">Markets</a>
|
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a i18n [routerLink]="['/resources']">Resources</a></li>
|
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="h6 mt-2">Ghostfolio</div>
|
<div class="h6 mt-2">Ghostfolio</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li><a i18n [routerLink]="['/about']">About</a></li>
|
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
|
||||||
<li *ngIf="hasPermissionForBlog">
|
<li *ngIf="hasPermissionForBlog">
|
||||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a i18n [routerLink]="['/about', 'changelog']">Changelog</a>
|
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a i18n [routerLink]="['/features']">Features</a></li>
|
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
<li *ngIf="hasPermissionForSubscription">
|
||||||
<a i18n [routerLink]="['/faq']">Frequently Asked Questions (FAQ)</a>
|
<a i18n [routerLink]="routerLinkFaq"
|
||||||
|
>Frequently Asked Questions (FAQ)</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a i18n [routerLink]="['/about', 'license']">License</a>
|
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="hasPermissionForStatistics">
|
<li *ngIf="hasPermissionForStatistics">
|
||||||
<a [routerLink]="['/open']">Open Startup</a>
|
<a [routerLink]="['/open']">Open Startup</a>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
<li *ngIf="hasPermissionForSubscription">
|
||||||
<a i18n [routerLink]="['/pricing']">Pricing</a>
|
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="hasPermissionForSubscription">
|
<li *ngIf="hasPermissionForSubscription">
|
||||||
<a i18n [routerLink]="['/about', 'privacy-policy']"
|
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
|
||||||
>Privacy Policy</a
|
>Privacy Policy</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@ -164,6 +153,11 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||||
</li>
|
</li>
|
||||||
|
<!--
|
||||||
|
<li>
|
||||||
|
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
||||||
|
</li>
|
||||||
|
-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,31 +4,47 @@
|
|||||||
display: block;
|
display: block;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
|
&.has-info-message {
|
||||||
|
header {
|
||||||
|
height: calc(2 * var(--mat-toolbar-standard-height));
|
||||||
|
|
||||||
|
.info-message-container {
|
||||||
|
height: var(--mat-toolbar-standard-height);
|
||||||
|
|
||||||
|
.info-message-inner-container {
|
||||||
|
background-color: rgba(var(--palette-primary-500), 1);
|
||||||
|
height: var(--mat-toolbar-standard-height);
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
.info-message {
|
||||||
|
color: rgba(var(--palette-foreground-text), 1);
|
||||||
|
font-size: 80%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.a {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: calc(100vh - 2 * var(--mat-toolbar-standard-height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
height: var(--mat-toolbar-standard-height);
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
min-height: 100vh;
|
min-height: calc(100vh - var(--mat-toolbar-standard-height));
|
||||||
padding-top: 5rem;
|
|
||||||
|
|
||||||
.info-message-container {
|
|
||||||
height: 3.5rem;
|
|
||||||
margin-top: -0.5rem;
|
|
||||||
|
|
||||||
.info-message {
|
|
||||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
|
||||||
border-radius: 2rem;
|
|
||||||
font-size: 80%;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
.a {
|
|
||||||
color: rgba(var(--palette-primary-500), 1);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,12 +52,4 @@
|
|||||||
footer {
|
footer {
|
||||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
.info-message-container {
|
|
||||||
.info-message {
|
|
||||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
HostBinding,
|
||||||
Inject,
|
Inject,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
@ -28,16 +29,36 @@ import { UserService } from './services/user/user.service';
|
|||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss']
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnDestroy, OnInit {
|
export class AppComponent implements OnDestroy, OnInit {
|
||||||
|
@HostBinding('class.has-info-message') get getHasMessage() {
|
||||||
|
return this.hasInfoMessage;
|
||||||
|
}
|
||||||
|
|
||||||
public canCreateAccount: boolean;
|
public canCreateAccount: boolean;
|
||||||
public currentRoute: string;
|
public currentRoute: string;
|
||||||
public currentYear = new Date().getFullYear();
|
public currentYear = new Date().getFullYear();
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public hasInfoMessage: boolean;
|
||||||
public hasPermissionForBlog: boolean;
|
public hasPermissionForBlog: boolean;
|
||||||
public hasPermissionForStatistics: boolean;
|
public hasPermissionForStatistics: boolean;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
|
public hasTabs = false;
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
public pageTitle: string;
|
public pageTitle: string;
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkAboutChangelog = ['/' + $localize`about`, 'changelog'];
|
||||||
|
public routerLinkAboutLicense = ['/' + $localize`about`, $localize`license`];
|
||||||
|
public routerLinkAboutPrivacyPolicy = [
|
||||||
|
'/' + $localize`about`,
|
||||||
|
$localize`privacy-policy`
|
||||||
|
];
|
||||||
|
public routerLinkFaq = ['/' + $localize`faq`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkMarkets = ['/' + $localize`markets`];
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
public routerLinkRegister = ['/' + $localize`register`];
|
||||||
|
public routerLinkResources = ['/' + $localize`resources`];
|
||||||
|
public showFooter = false;
|
||||||
public user: User;
|
public user: User;
|
||||||
public version = environment.version;
|
public version = environment.version;
|
||||||
|
|
||||||
@ -89,6 +110,28 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
const urlSegments = urlSegmentGroup.segments;
|
const urlSegments = urlSegmentGroup.segments;
|
||||||
this.currentRoute = urlSegments[0].path;
|
this.currentRoute = urlSegments[0].path;
|
||||||
|
|
||||||
|
this.hasTabs =
|
||||||
|
(this.currentRoute === this.routerLinkAbout[0].slice(1) ||
|
||||||
|
this.currentRoute === 'account' ||
|
||||||
|
this.currentRoute === 'admin' ||
|
||||||
|
this.currentRoute === 'home' ||
|
||||||
|
this.currentRoute === 'portfolio' ||
|
||||||
|
this.currentRoute === 'zen') &&
|
||||||
|
this.deviceType !== 'mobile';
|
||||||
|
|
||||||
|
this.showFooter =
|
||||||
|
(this.currentRoute === 'blog' ||
|
||||||
|
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
|
||||||
|
this.currentRoute === this.routerLinkFeatures[0].slice(1) ||
|
||||||
|
this.currentRoute === this.routerLinkMarkets[0].slice(1) ||
|
||||||
|
this.currentRoute === 'open' ||
|
||||||
|
this.currentRoute === 'p' ||
|
||||||
|
this.currentRoute === this.routerLinkPricing[0].slice(1) ||
|
||||||
|
this.currentRoute === this.routerLinkRegister[0].slice(1) ||
|
||||||
|
this.currentRoute === this.routerLinkResources[0].slice(1) ||
|
||||||
|
this.currentRoute === 'start') &&
|
||||||
|
this.deviceType !== 'mobile';
|
||||||
|
|
||||||
if (this.deviceType === 'mobile') {
|
if (this.deviceType === 'mobile') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const index = this.title.getTitle().indexOf('–');
|
const index = this.title.getTitle().indexOf('–');
|
||||||
@ -113,6 +156,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
permissions.createUserAccount
|
permissions.createUserAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.hasInfoMessage =
|
||||||
|
hasPermission(
|
||||||
|
this.user?.permissions,
|
||||||
|
permissions.createUserAccount
|
||||||
|
) || !!this.info.systemMessage;
|
||||||
|
|
||||||
this.initializeTheme(this.user?.settings.colorScheme);
|
this.initializeTheme(this.user?.settings.colorScheme);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Platform } from '@angular/cdk/platform';
|
import { Platform } from '@angular/cdk/platform';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatChipsModule } from '@angular/material/chips';
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import {
|
import {
|
||||||
@ -35,6 +35,7 @@ export function NgxStripeFactory(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
bootstrap: [AppComponent],
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
imports: [
|
imports: [
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
@ -72,6 +73,6 @@ export function NgxStripeFactory(): string {
|
|||||||
useFactory: NgxStripeFactory
|
useFactory: NgxStripeFactory
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -1,3 +1,15 @@
|
|||||||
|
<div *ngIf="hasPermissionToCreateAccess" class="d-flex justify-content-end">
|
||||||
|
<a
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[queryParams]="{ createDialog: true }"
|
||||||
|
[routerLink]="[]"
|
||||||
|
>
|
||||||
|
Add Access
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||||
<ng-container matColumnDef="alias">
|
<ng-container matColumnDef="alias">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
||||||
|
@ -19,6 +19,7 @@ import { Access } from '@ghostfolio/common/interfaces';
|
|||||||
})
|
})
|
||||||
export class AccessTableComponent implements OnChanges, OnInit {
|
export class AccessTableComponent implements OnChanges, OnInit {
|
||||||
@Input() accesses: Access[];
|
@Input() accesses: Access[];
|
||||||
|
@Input() hasPermissionToCreateAccess = false;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
|
|
||||||
@Output() accessDeleted = new EventEmitter<string>();
|
@Output() accessDeleted = new EventEmitter<string>();
|
||||||
|
@ -3,13 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { AccessTableComponent } from './access-table.component';
|
import { AccessTableComponent } from './access-table.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AccessTableComponent],
|
declarations: [AccessTableComponent],
|
||||||
exports: [AccessTableComponent],
|
exports: [AccessTableComponent],
|
||||||
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatTableModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfPortfolioAccessTableModule {}
|
export class GfPortfolioAccessTableModule {}
|
||||||
|
@ -29,13 +29,13 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
|
|||||||
styleUrls: ['./account-detail-dialog.component.scss']
|
styleUrls: ['./account-detail-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class AccountDetailDialog implements OnDestroy, OnInit {
|
export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||||
public accountType: string;
|
|
||||||
public balance: number;
|
public balance: number;
|
||||||
public currency: string;
|
public currency: string;
|
||||||
public equity: number;
|
public equity: number;
|
||||||
public name: string;
|
public name: string;
|
||||||
public orders: OrderWithAccount[];
|
public orders: OrderWithAccount[];
|
||||||
public platformName: string;
|
public platformName: string;
|
||||||
|
public transactionCount: number;
|
||||||
public user: User;
|
public user: User;
|
||||||
public valueInBaseCurrency: number;
|
public valueInBaseCurrency: number;
|
||||||
|
|
||||||
@ -65,15 +65,14 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
({
|
({
|
||||||
accountType,
|
|
||||||
balance,
|
balance,
|
||||||
currency,
|
currency,
|
||||||
name,
|
name,
|
||||||
Platform,
|
Platform,
|
||||||
|
transactionCount,
|
||||||
value,
|
value,
|
||||||
valueInBaseCurrency
|
valueInBaseCurrency
|
||||||
}) => {
|
}) => {
|
||||||
this.accountType = translate(accountType);
|
|
||||||
this.balance = balance;
|
this.balance = balance;
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
|
|
||||||
@ -85,6 +84,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.platformName = Platform?.name ?? '-';
|
this.platformName = Platform?.name ?? '-';
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
@ -44,8 +44,8 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value i18n size="medium" [value]="accountType"
|
<gf-value i18n size="medium" [value]="transactionCount"
|
||||||
>Account Type</gf-value
|
>Activities</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
|
<div *ngIf="false" class="d-flex justify-content-end">
|
||||||
|
<button
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-stroked-button
|
||||||
|
(click)="onTransferBalance()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="arrow-redo-outline"></ion-icon>
|
||||||
|
<ng-container i18n>Transfer Cash Balance</ng-container>...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="gf-table w-100" mat-table matSort [dataSource]="dataSource">
|
<table class="gf-table w-100" mat-table matSort [dataSource]="dataSource">
|
||||||
<ng-container matColumnDef="status">
|
<ng-container matColumnDef="status">
|
||||||
<th
|
<th
|
||||||
@ -85,7 +96,7 @@
|
|||||||
<ng-container matColumnDef="transactions">
|
<ng-container matColumnDef="transactions">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="px-1 text-right"
|
class="justify-content-end px-1"
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header="transactionCount"
|
mat-sort-header="transactionCount"
|
||||||
>
|
>
|
||||||
@ -93,9 +104,7 @@
|
|||||||
<span class="d-none d-sm-block" i18n>Activities</span>
|
<span class="d-none d-sm-block" i18n>Activities</span>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
|
{{ element.transactionCount }}
|
||||||
element.transactionCount
|
|
||||||
}}</ng-container>
|
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||||
{{ transactionCount }}
|
{{ transactionCount }}
|
||||||
|
@ -34,6 +34,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
@Output() accountDeleted = new EventEmitter<string>();
|
@Output() accountDeleted = new EventEmitter<string>();
|
||||||
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
||||||
|
@Output() transferBalance = new EventEmitter<void>();
|
||||||
|
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
@ -97,6 +98,10 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
alert(aComment);
|
alert(aComment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onTransferBalance() {
|
||||||
|
this.transferBalance.emit();
|
||||||
|
}
|
||||||
|
|
||||||
public onUpdateAccount(aAccount: AccountModel) {
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
this.accountToUpdate.emit(aAccount);
|
this.accountToUpdate.emit(aAccount);
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
}"
|
}"
|
||||||
[title]="
|
[title]="
|
||||||
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||||
| date : defaultDateFormat) ?? ''
|
| date: defaultDateFormat) ?? ''
|
||||||
"
|
"
|
||||||
(click)="
|
(click)="
|
||||||
onOpenMarketDataDetail({
|
onOpenMarketDataDetail({
|
||||||
|
@ -13,7 +13,6 @@ import { AdminService } from '@ghostfolio/client/services/admin.service';
|
|||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import {
|
import {
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
ScraperConfiguration,
|
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
@ -146,9 +145,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
.postBenchmark({ dataSource, symbol })
|
.postBenchmark({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
setTimeout(() => {
|
this.dataService.updateInfo();
|
||||||
window.location.reload();
|
|
||||||
}, 300);
|
this.isBenchmark = true;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,6 +186,19 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onUnsetBenchmark({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
this.dataService
|
||||||
|
.deleteBenchmark({ dataSource, symbol })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dataService.updateInfo();
|
||||||
|
|
||||||
|
this.isBenchmark = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -37,13 +37,6 @@
|
|||||||
>
|
>
|
||||||
<ng-container i18n>Gather Profile Data</ng-container>
|
<ng-container i18n>Gather Profile Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
mat-menu-item
|
|
||||||
[disabled]="isBenchmark"
|
|
||||||
(click)="onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
|
|
||||||
>
|
|
||||||
<ng-container i18n>Set as Benchmark</ng-container>
|
|
||||||
</button>
|
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -151,6 +144,17 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<div class="w-50">
|
||||||
|
<mat-checkbox
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
[checked]="isBenchmark"
|
||||||
|
(change)="isBenchmark ? onUnsetBenchmark({dataSource: data.dataSource, symbol: data.symbol}) : onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
|
||||||
|
>Benchmark</mat-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Symbol Mapping</mat-label>
|
<mat-label i18n>Symbol Mapping</mat-label>
|
||||||
|
@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
@ -21,6 +22,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
|
|||||||
GfPortfolioProportionChartModule,
|
GfPortfolioProportionChartModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
|
import { environment } from '@ghostfolio/client/../environments/environment';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
@ -42,6 +43,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
public transactionCount: number;
|
public transactionCount: number;
|
||||||
public userCount: number;
|
public userCount: number;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
public version: string;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -202,15 +204,18 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
this.adminService
|
this.adminService
|
||||||
.fetchAdminData()
|
.fetchAdminData()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ exchangeRates, settings, transactionCount, userCount }) => {
|
.subscribe(
|
||||||
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
|
({ exchangeRates, settings, transactionCount, userCount, version }) => {
|
||||||
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
|
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
|
||||||
this.exchangeRates = exchangeRates;
|
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
|
||||||
this.transactionCount = transactionCount;
|
this.exchangeRates = exchangeRates;
|
||||||
this.userCount = userCount;
|
this.transactionCount = transactionCount;
|
||||||
|
this.userCount = userCount;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateCouponCode(aLength: number) {
|
private generateCouponCode(aLength: number) {
|
||||||
|
@ -3,12 +3,18 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<div class="w-50" i18n>Version</div>
|
||||||
|
<div class="w-50">
|
||||||
|
<gf-value [value]="version" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>User Count</div>
|
<div class="w-50" i18n>User Count</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<gf-value
|
<gf-value
|
||||||
precision="0"
|
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
[precision]="0"
|
||||||
[value]="userCount"
|
[value]="userCount"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
@ -17,8 +23,8 @@
|
|||||||
<div class="w-50" i18n>Activity Count</div>
|
<div class="w-50" i18n>Activity Count</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<gf-value
|
<gf-value
|
||||||
precision="0"
|
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
[precision]="0"
|
||||||
[value]="transactionCount"
|
[value]="transactionCount"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
<div *ngIf="transactionCount && userCount">
|
<div *ngIf="transactionCount && userCount">
|
||||||
@ -72,19 +78,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
*ngIf="info?.tags?.length > 0"
|
|
||||||
class="align-items-start d-flex my-3"
|
|
||||||
>
|
|
||||||
<div class="w-50" i18n>Tags</div>
|
|
||||||
<div class="w-50">
|
|
||||||
<table>
|
|
||||||
<tr *ngFor="let tag of info.tags">
|
|
||||||
<td class="pl-1">{{ tag.name }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>User Signup</div>
|
<div class="w-50" i18n>User Signup</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
|
@ -13,13 +13,14 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
||||||
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { Platform } from '@prisma/client';
|
import { Platform } from '@prisma/client';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-account-platform.component';
|
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-platform-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -40,6 +41,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private adminService: AdminService,
|
private adminService: AdminService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -114,10 +116,13 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((platforms) => {
|
.subscribe((platforms) => {
|
||||||
this.platforms = platforms;
|
this.platforms = platforms;
|
||||||
|
|
||||||
this.dataSource = new MatTableDataSource(platforms);
|
this.dataSource = new MatTableDataSource(platforms);
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
this.dataSource.sortingDataAccessor = get;
|
this.dataSource.sortingDataAccessor = get;
|
||||||
|
|
||||||
|
this.dataService.updateInfo();
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -130,7 +135,6 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
|||||||
url: null
|
url: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
@ -170,7 +174,6 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
@ -15,8 +15,8 @@ export class CreateOrUpdatePlatformDialog {
|
|||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>,
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams
|
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public onCancel() {
|
public onCancel() {
|
@ -6,7 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
|||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
|
||||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-account-platform.component';
|
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [CreateOrUpdatePlatformDialog],
|
declarations: [CreateOrUpdatePlatformDialog],
|
||||||
|
@ -2,14 +2,13 @@
|
|||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2 class="text-center" i18n>Platforms</h2>
|
<h2 class="text-center" i18n>Platforms</h2>
|
||||||
<gf-admin-platform></gf-admin-platform>
|
<gf-admin-platform />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2 class="text-center" i18n>Tags</h2>
|
<h2 class="text-center" i18n>Tags</h2>
|
||||||
|
<gf-admin-tag />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,6 @@ import { Subject } from 'rxjs';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
host: { class: 'page' },
|
|
||||||
selector: 'gf-admin-settings',
|
selector: 'gf-admin-settings',
|
||||||
styleUrls: ['./admin-settings.component.scss'],
|
styleUrls: ['./admin-settings.component.scss'],
|
||||||
templateUrl: './admin-settings.component.html'
|
templateUrl: './admin-settings.component.html'
|
||||||
|
@ -2,12 +2,18 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
|
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
|
||||||
|
import { GfAdminTagModule } from '@ghostfolio/client/components/admin-tag/admin-tag.module';
|
||||||
|
|
||||||
import { AdminSettingsComponent } from './admin-settings.component';
|
import { AdminSettingsComponent } from './admin-settings.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AdminSettingsComponent],
|
declarations: [AdminSettingsComponent],
|
||||||
imports: [CommonModule, GfAdminPlatformModule, RouterModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfAdminPlatformModule,
|
||||||
|
GfAdminTagModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfAdminSettingsModule {}
|
export class GfAdminSettingsModule {}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<a
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[queryParams]="{ createTagDialog: true }"
|
||||||
|
[routerLink]="[]"
|
||||||
|
>
|
||||||
|
Add Tag
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<table
|
||||||
|
class="gf-table w-100"
|
||||||
|
mat-table
|
||||||
|
matSort
|
||||||
|
matSortActive="name"
|
||||||
|
matSortDirection="asc"
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
>
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header="name"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Name</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.name }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="activities">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header="activityCount"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Activities</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.activityCount }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="px-1 text-center"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
></th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="tagMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #tagMenu="matMenu" xPosition="before">
|
||||||
|
<button mat-menu-item (click)="onUpdateTag(element)">
|
||||||
|
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||||
|
<span i18n>Edit</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="onDeleteTag(element.id)">
|
||||||
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,5 @@
|
|||||||
|
@import 'apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
204
apps/client/src/app/components/admin-tag/admin-tag.component.ts
Normal file
204
apps/client/src/app/components/admin-tag/admin-tag.component.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
|
||||||
|
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
|
||||||
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { Tag } from '@prisma/client';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
|
import { CreateOrUpdateTagDialog } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
selector: 'gf-admin-tag',
|
||||||
|
styleUrls: ['./admin-tag.component.scss'],
|
||||||
|
templateUrl: './admin-tag.component.html'
|
||||||
|
})
|
||||||
|
export class AdminTagComponent implements OnInit, OnDestroy {
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
public dataSource: MatTableDataSource<Tag> = new MatTableDataSource();
|
||||||
|
public deviceType: string;
|
||||||
|
public displayedColumns = ['name', 'activities', 'actions'];
|
||||||
|
public tags: Tag[];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private adminService: AdminService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private userService: UserService
|
||||||
|
) {
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (params['createTagDialog']) {
|
||||||
|
this.openCreateTagDialog();
|
||||||
|
} else if (params['editTagDialog']) {
|
||||||
|
if (this.tags) {
|
||||||
|
const tag = this.tags.find(({ id }) => {
|
||||||
|
return id === params['tagId'];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.openUpdateTagDialog(tag);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.fetchTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteTag(aId: string) {
|
||||||
|
const confirmation = confirm(
|
||||||
|
$localize`Do you really want to delete this tag?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
this.deleteTag(aId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateTag({ id }: Tag) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { editTagDialog: true, tagId: id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteTag(aId: string) {
|
||||||
|
this.adminService
|
||||||
|
.deleteTag(aId)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.userService
|
||||||
|
.get(true)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.fetchTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchTags() {
|
||||||
|
this.adminService
|
||||||
|
.fetchTags()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((tags) => {
|
||||||
|
this.tags = tags;
|
||||||
|
|
||||||
|
this.dataSource = new MatTableDataSource(this.tags);
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
this.dataSource.sortingDataAccessor = get;
|
||||||
|
|
||||||
|
this.dataService.updateInfo();
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private openCreateTagDialog() {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, {
|
||||||
|
data: {
|
||||||
|
tag: {
|
||||||
|
name: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data) => {
|
||||||
|
const tag: CreateTagDto = data?.tag;
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
this.adminService
|
||||||
|
.postTag(tag)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.userService
|
||||||
|
.get(true)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.fetchTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private openUpdateTagDialog({ id, name }) {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, {
|
||||||
|
data: {
|
||||||
|
tag: {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data) => {
|
||||||
|
const tag: UpdateTagDto = data?.tag;
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
this.adminService
|
||||||
|
.putTag(tag)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.userService
|
||||||
|
.get(true)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.fetchTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
apps/client/src/app/components/admin-tag/admin-tag.module.ts
Normal file
26
apps/client/src/app/components/admin-tag/admin-tag.module.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { AdminTagComponent } from './admin-tag.component';
|
||||||
|
import { GfCreateOrUpdateTagDialogModule } from './create-or-update-tag-dialog/create-or-update-tag-dialog.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AdminTagComponent],
|
||||||
|
exports: [AdminTagComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfCreateOrUpdateTagDialogModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatTableModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfAdminTagModule {}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user