Compare commits
73 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
4955555ddd | |||
a98c788a26 | |||
9c16af81c7 | |||
2df27100f0 | |||
6cf6538719 | |||
0fd3db3228 | |||
18835149e2 | |||
6c9779fb0d | |||
3e98f097ef | |||
183ac8fa2b | |||
9036f53e7d | |||
f7c04e469a | |||
b5f01c0d15 | |||
5a23cd34ad | |||
6e87f34c6f | |||
6618aa2e9b | |||
0d25a96f7e | |||
4f6d9d3a76 | |||
928f6f0c45 | |||
09e95ddcee | |||
2d003225bc | |||
de93cabd69 | |||
51489cca81 | |||
f7f4c3afb1 | |||
0821086e41 | |||
7a905fde63 | |||
d2882b1119 | |||
3a500598c5 | |||
42274917e0 | |||
8ba50f2729 |
@ -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,
|
||||||
|
142
CHANGELOG.md
142
CHANGELOG.md
@ -5,7 +5,133 @@ 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).
|
||||||
|
|
||||||
## 1.297.2 - 2023-08-05
|
## 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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the data export feature to the user account page
|
||||||
|
- Added a currencies preset to the historical market data table of the admin control panel
|
||||||
|
- Added the _OSS Friends_ page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the localized meta data in `html` files
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the rows with cash positions in the holdings table
|
||||||
|
- Fixed an issue with the date parsing in the historical market data editor of the admin control panel
|
||||||
|
|
||||||
|
## 1.300.0 - 2023-08-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added more durations in the coupon system
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Migrated the remaining requests from `bent` to `got`
|
||||||
|
|
||||||
|
## 1.299.1 - 2023-08-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the activities import by allowing a different currency than the asset's official one
|
||||||
|
- Added a timeout to the _EOD Historical Data_ requests
|
||||||
|
- Migrated the requests from `bent` to `got` in the _EOD Historical Data_ service
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the editing of the emergency fund
|
||||||
|
- Fixed the historical data gathering interval for asset profiles used as benchmarks having activities
|
||||||
|
|
||||||
|
## 1.298.0 - 2023-08-06
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `ng-extract-i18n-merge` from version `2.6.0` to `2.7.0`
|
||||||
|
- Upgraded `Nx` from version `16.5.5` to `16.6.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the styles of various components (card, progress, tab) after the upgrade to `@angular/material` `16`
|
||||||
|
|
||||||
|
## 1.297.4 - 2023-08-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@ -576,7 +702,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Changed the slide toggles to checkboxes on the account page
|
- Changed the slide toggles to checkboxes on the user account page
|
||||||
- Changed the slide toggles to checkboxes in the admin control panel
|
- Changed the slide toggles to checkboxes in the admin control panel
|
||||||
- Increased the density of the theme
|
- Increased the density of the theme
|
||||||
- Migrated the style of various components to `@angular/material` `15` (mdc)
|
- Migrated the style of various components to `@angular/material` `15` (mdc)
|
||||||
@ -1138,7 +1264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved the language selector on the account page
|
- Improved the language selector on the user account page
|
||||||
- Improved the wording in the _X-ray_ section (net worth instead of investment)
|
- Improved the wording in the _X-ray_ section (net worth instead of investment)
|
||||||
- Extended the asset profile details dialog in the admin control panel
|
- Extended the asset profile details dialog in the admin control panel
|
||||||
- Updated the browserslist database
|
- Updated the browserslist database
|
||||||
@ -1556,7 +1682,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added a language selector to the account page
|
- Added a language selector to the user account page
|
||||||
- Added support for translated labels in the value component
|
- Added support for translated labels in the value component
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -1885,7 +2011,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added the user id to the account page
|
- Added the user id to the user account page
|
||||||
- Added a new view with jobs of the queue to the admin control panel
|
- Added a new view with jobs of the queue to the admin control panel
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -3540,7 +3666,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Respected the cash balance on the analysis page
|
- Respected the cash balance on the analysis page
|
||||||
- Improved the settings selectors on the account page
|
- Improved the settings selectors on the user account page
|
||||||
- Harmonized the slogan to "Open Source Wealth Management Software"
|
- Harmonized the slogan to "Open Source Wealth Management Software"
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@ -4006,7 +4132,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added a gradient to the line charts
|
- Added a gradient to the line charts
|
||||||
- Added a selector to set the base currency on the account page
|
- Added a selector to set the base currency on the user account page
|
||||||
|
|
||||||
## 0.81.0 - 06.04.2021
|
## 0.81.0 - 06.04.2021
|
||||||
|
|
||||||
@ -4320,7 +4446,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Added the membership status to the account page
|
- Added the membership status to the user account page
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -58,4 +58,4 @@ RUN apt update && apt install -y \
|
|||||||
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
||||||
WORKDIR /ghostfolio/apps/api
|
WORKDIR /ghostfolio/apps/api
|
||||||
EXPOSE ${PORT:-3333}
|
EXPOSE ${PORT:-3333}
|
||||||
CMD [ "yarn", "start:production" ]
|
CMD [ "yarn", "start:production" ]
|
||||||
|
@ -38,7 +38,7 @@ import {
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client';
|
import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client';
|
||||||
import { isDate } from 'date-fns';
|
import { isDate, parseISO } from 'date-fns';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
@ -233,7 +233,7 @@ export class AdminController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(dateString);
|
const date = parseISO(dateString);
|
||||||
|
|
||||||
if (!isDate(date)) {
|
if (!isDate(date)) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@ -333,7 +333,7 @@ export class AdminController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(dateString);
|
const date = parseISO(dateString);
|
||||||
|
|
||||||
return this.marketDataService.updateMarketData({
|
return this.marketDataService.updateMarketData({
|
||||||
data: { marketPrice: data.marketPrice, state: 'CLOSE' },
|
data: { marketPrice: data.marketPrice, state: 'CLOSE' },
|
||||||
|
@ -7,13 +7,14 @@ 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 {
|
import {
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_CURRENCY,
|
||||||
PROPERTY_CURRENCIES
|
PROPERTY_CURRENCIES
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
|
AdminMarketDataItem,
|
||||||
Filter,
|
Filter,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -25,8 +26,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,
|
||||||
@ -36,9 +35,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,
|
||||||
@ -82,15 +79,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
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@ -121,7 +118,9 @@ export class AdminService {
|
|||||||
[{ symbol: 'asc' }];
|
[{ symbol: 'asc' }];
|
||||||
const where: Prisma.SymbolProfileWhereInput = {};
|
const where: Prisma.SymbolProfileWhereInput = {};
|
||||||
|
|
||||||
if (
|
if (presetId === 'CURRENCIES') {
|
||||||
|
return this.getMarketDataForCurrencies();
|
||||||
|
} else if (
|
||||||
presetId === 'ETF_WITHOUT_COUNTRIES' ||
|
presetId === 'ETF_WITHOUT_COUNTRIES' ||
|
||||||
presetId === 'ETF_WITHOUT_SECTORS'
|
presetId === 'ETF_WITHOUT_SECTORS'
|
||||||
) {
|
) {
|
||||||
@ -313,6 +312,36 @@ export class AdminService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
|
||||||
|
const marketDataItems = await this.prismaService.marketData.groupBy({
|
||||||
|
_count: true,
|
||||||
|
by: ['dataSource', 'symbol']
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketData: AdminMarketDataItem[] = this.exchangeRateDataService
|
||||||
|
.getCurrencyPairs()
|
||||||
|
.map(({ dataSource, symbol }) => {
|
||||||
|
const marketDataItemCount =
|
||||||
|
marketDataItems.find((marketDataItem) => {
|
||||||
|
return (
|
||||||
|
marketDataItem.dataSource === dataSource &&
|
||||||
|
marketDataItem.symbol === symbol
|
||||||
|
);
|
||||||
|
})?._count ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataSource,
|
||||||
|
marketDataItemCount,
|
||||||
|
symbol,
|
||||||
|
assetClass: 'CASH',
|
||||||
|
countriesCount: 0,
|
||||||
|
sectorsCount: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { marketData, count: marketData.length };
|
||||||
|
}
|
||||||
|
|
||||||
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
|
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
|
||||||
let orderBy: any = {
|
let orderBy: any = {
|
||||||
createdAt: 'desc'
|
createdAt: 'desc'
|
||||||
|
@ -7,11 +7,16 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
|
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
|
||||||
|
import {
|
||||||
|
DEFAULT_LANGUAGE_CODE,
|
||||||
|
SUPPORTED_LANGUAGE_CODES
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
import { StatusCodes } from 'http-status-codes';
|
||||||
|
|
||||||
import { AccessModule } from './access/access.module';
|
import { AccessModule } from './access/access.module';
|
||||||
import { AccountModule } from './account/account.module';
|
import { AccountModule } from './account/account.module';
|
||||||
@ -23,7 +28,6 @@ import { BenchmarkModule } from './benchmark/benchmark.module';
|
|||||||
import { CacheModule } from './cache/cache.module';
|
import { CacheModule } from './cache/cache.module';
|
||||||
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
|
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
|
||||||
import { ExportModule } from './export/export.module';
|
import { ExportModule } from './export/export.module';
|
||||||
import { FrontendMiddleware } from './frontend.middleware';
|
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
import { ImportModule } from './import/import.module';
|
import { ImportModule } from './import/import.module';
|
||||||
import { InfoModule } from './info/info.module';
|
import { InfoModule } from './info/info.module';
|
||||||
@ -32,14 +36,10 @@ import { OrderModule } from './order/order.module';
|
|||||||
import { PlatformModule } from './platform/platform.module';
|
import { PlatformModule } from './platform/platform.module';
|
||||||
import { PortfolioModule } from './portfolio/portfolio.module';
|
import { PortfolioModule } from './portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
import { RedisCacheModule } from './redis-cache/redis-cache.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 { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
import {
|
|
||||||
DEFAULT_LANGUAGE_CODE,
|
|
||||||
SUPPORTED_LANGUAGE_CODES
|
|
||||||
} from '@ghostfolio/common/config';
|
|
||||||
import { StatusCodes } from 'http-status-codes';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -75,6 +75,8 @@ import { StatusCodes } from 'http-status-codes';
|
|||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
|
exclude: ['/api*', '/sitemap.xml'],
|
||||||
|
rootPath: join(__dirname, '..', 'client'),
|
||||||
serveStaticOptions: {
|
serveStaticOptions: {
|
||||||
setHeaders: (res) => {
|
setHeaders: (res) => {
|
||||||
if (res.req?.path === '/') {
|
if (res.req?.path === '/') {
|
||||||
@ -94,10 +96,9 @@ import { StatusCodes } from 'http-status-codes';
|
|||||||
res.statusCode = StatusCodes.MOVED_PERMANENTLY;
|
res.statusCode = StatusCodes.MOVED_PERMANENTLY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
rootPath: join(__dirname, '..', 'client'),
|
|
||||||
exclude: ['/api*']
|
|
||||||
}),
|
}),
|
||||||
|
SitemapModule,
|
||||||
SubscriptionModule,
|
SubscriptionModule,
|
||||||
SymbolModule,
|
SymbolModule,
|
||||||
TwitterBotModule,
|
TwitterBotModule,
|
||||||
@ -106,10 +107,4 @@ import { StatusCodes } from 'http-status-codes';
|
|||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [CronService]
|
providers: [CronService]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {}
|
||||||
configure(consumer: MiddlewareConsumer) {
|
|
||||||
consumer
|
|
||||||
.apply(FrontendMiddleware)
|
|
||||||
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ 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';
|
||||||
@ -23,7 +24,7 @@ export class ExchangeRateController {
|
|||||||
@Param('dateString') dateString: string,
|
@Param('dateString') dateString: string,
|
||||||
@Param('symbol') symbol: string
|
@Param('symbol') symbol: string
|
||||||
): Promise<IDataProviderHistoricalResponse> {
|
): Promise<IDataProviderHistoricalResponse> {
|
||||||
const date = new Date(dateString);
|
const date = parseISO(dateString);
|
||||||
|
|
||||||
const exchangeRate = await this.exchangeRateService.getExchangeRate({
|
const exchangeRate = await this.exchangeRateService.getExchangeRate({
|
||||||
date,
|
date,
|
||||||
|
@ -1,255 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import { environment } from '@ghostfolio/api/environments/environment';
|
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
|
||||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { NextFunction, Request, Response } from 'express';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FrontendMiddleware implements NestMiddleware {
|
|
||||||
public indexHtmlDe = '';
|
|
||||||
public indexHtmlEn = '';
|
|
||||||
public indexHtmlEs = '';
|
|
||||||
public indexHtmlFr = '';
|
|
||||||
public indexHtmlIt = '';
|
|
||||||
public indexHtmlNl = '';
|
|
||||||
public indexHtmlPt = '';
|
|
||||||
public sitemapXml = '';
|
|
||||||
|
|
||||||
private static readonly DEFAULT_DESCRIPTION =
|
|
||||||
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private readonly configurationService: ConfigurationService
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
this.indexHtmlDe = fs.readFileSync(
|
|
||||||
this.getPathOfIndexHtmlFile('de'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
this.indexHtmlEn = fs.readFileSync(
|
|
||||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
this.indexHtmlEs = fs.readFileSync(
|
|
||||||
this.getPathOfIndexHtmlFile('es'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
this.indexHtmlFr = fs.readFileSync(
|
|
||||||
this.getPathOfIndexHtmlFile('fr'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
this.indexHtmlIt = fs.readFileSync(
|
|
||||||
this.getPathOfIndexHtmlFile('it'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
this.indexHtmlNl = fs.readFileSync(
|
|
||||||
this.getPathOfIndexHtmlFile('nl'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
this.indexHtmlPt = fs.readFileSync(
|
|
||||||
this.getPathOfIndexHtmlFile('pt'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
this.sitemapXml = fs.readFileSync(
|
|
||||||
path.join(__dirname, 'assets', 'sitemap.xml'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public use(request: Request, response: Response, next: NextFunction) {
|
|
||||||
const currentDate = format(new Date(), DATE_FORMAT);
|
|
||||||
let featureGraphicPath = 'assets/cover.png';
|
|
||||||
let title = 'Ghostfolio – Open Source Wealth Management Software';
|
|
||||||
|
|
||||||
if (request.path.startsWith('/en/blog/2022/08/500-stars-on-github')) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
|
|
||||||
title = `500 Stars - ${title}`;
|
|
||||||
} else if (request.path.startsWith('/en/blog/2022/10/hacktoberfest-2022')) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png';
|
|
||||||
title = `Hacktoberfest 2022 - ${title}`;
|
|
||||||
} else if (request.path.startsWith('/en/blog/2022/11/black-friday-2022')) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/black-friday-2022.jpg';
|
|
||||||
title = `Black Friday 2022 - ${title}`;
|
|
||||||
} else if (
|
|
||||||
request.path.startsWith(
|
|
||||||
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/20221226.jpg';
|
|
||||||
title = `The importance of tracking your personal finances - ${title}`;
|
|
||||||
} else if (
|
|
||||||
request.path.startsWith(
|
|
||||||
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png';
|
|
||||||
title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`;
|
|
||||||
} else if (
|
|
||||||
request.path.startsWith('/en/blog/2023/02/ghostfolio-meets-umbrel')
|
|
||||||
) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
|
|
||||||
title = `Ghostfolio meets Umbrel - ${title}`;
|
|
||||||
} else if (
|
|
||||||
request.path.startsWith(
|
|
||||||
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
|
|
||||||
title = `Ghostfolio reaches 1’000 Stars on GitHub - ${title}`;
|
|
||||||
} else if (
|
|
||||||
request.path.startsWith(
|
|
||||||
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/20230520.jpg';
|
|
||||||
title = `Unlock your Financial Potential with Ghostfolio - ${title}`;
|
|
||||||
} else if (
|
|
||||||
request.path.startsWith('/en/blog/2023/07/exploring-the-path-to-fire')
|
|
||||||
) {
|
|
||||||
featureGraphicPath = 'assets/images/blog/20230701.jpg';
|
|
||||||
title = `Exploring the Path to FIRE - ${title}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
request.path.startsWith('/api/') ||
|
|
||||||
this.isFileRequest(request.url) ||
|
|
||||||
!environment.production
|
|
||||||
) {
|
|
||||||
// Skip
|
|
||||||
next();
|
|
||||||
} else if (request.path === '/sitemap.xml') {
|
|
||||||
response.setHeader('content-type', 'application/xml');
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.sitemapXml, {
|
|
||||||
currentDate: format(getYesterday(), DATE_FORMAT)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (request.path === '/de' || request.path.startsWith('/de/')) {
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.indexHtmlDe, {
|
|
||||||
currentDate,
|
|
||||||
featureGraphicPath,
|
|
||||||
title,
|
|
||||||
description:
|
|
||||||
'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
|
|
||||||
languageCode: 'de',
|
|
||||||
path: request.path,
|
|
||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (request.path === '/es' || request.path.startsWith('/es/')) {
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.indexHtmlEs, {
|
|
||||||
currentDate,
|
|
||||||
featureGraphicPath,
|
|
||||||
title,
|
|
||||||
description:
|
|
||||||
'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
|
|
||||||
languageCode: 'es',
|
|
||||||
path: request.path,
|
|
||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (request.path === '/fr' || request.path.startsWith('/fr/')) {
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.indexHtmlFr, {
|
|
||||||
currentDate,
|
|
||||||
featureGraphicPath,
|
|
||||||
title,
|
|
||||||
description:
|
|
||||||
'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
|
|
||||||
languageCode: 'fr',
|
|
||||||
path: request.path,
|
|
||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (request.path === '/it' || request.path.startsWith('/it/')) {
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.indexHtmlIt, {
|
|
||||||
currentDate,
|
|
||||||
featureGraphicPath,
|
|
||||||
title,
|
|
||||||
description:
|
|
||||||
'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
|
|
||||||
languageCode: 'it',
|
|
||||||
path: request.path,
|
|
||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (request.path === '/nl' || request.path.startsWith('/nl/')) {
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.indexHtmlNl, {
|
|
||||||
currentDate,
|
|
||||||
featureGraphicPath,
|
|
||||||
title,
|
|
||||||
description:
|
|
||||||
'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.',
|
|
||||||
languageCode: 'nl',
|
|
||||||
path: request.path,
|
|
||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (request.path === '/pt' || request.path.startsWith('/pt/')) {
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.indexHtmlPt, {
|
|
||||||
currentDate,
|
|
||||||
featureGraphicPath,
|
|
||||||
title,
|
|
||||||
description:
|
|
||||||
'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
|
|
||||||
languageCode: 'pt',
|
|
||||||
path: request.path,
|
|
||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
response.send(
|
|
||||||
this.interpolate(this.indexHtmlEn, {
|
|
||||||
currentDate,
|
|
||||||
featureGraphicPath,
|
|
||||||
title,
|
|
||||||
description: FrontendMiddleware.DEFAULT_DESCRIPTION,
|
|
||||||
languageCode: DEFAULT_LANGUAGE_CODE,
|
|
||||||
path: request.path,
|
|
||||||
rootUrl: this.configurationService.get('ROOT_URL')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPathOfIndexHtmlFile(aLocale: string) {
|
|
||||||
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
private interpolate(template: string, context: any) {
|
|
||||||
return template.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
|
|
||||||
const properties = objectPath.split('.');
|
|
||||||
return properties.reduce(
|
|
||||||
(previous, current) => previous?.[current],
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private isFileRequest(filename: string) {
|
|
||||||
if (filename === '/assets/LICENSE') {
|
|
||||||
return true;
|
|
||||||
} else if (
|
|
||||||
filename === '/sitemap.xml' ||
|
|
||||||
filename.includes('auth/ey') ||
|
|
||||||
filename.includes(
|
|
||||||
'personal-finance-tools/open-source-alternative-to-markets.sh'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return filename.split('.').pop() !== filename;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import {
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
getAssetProfileIdentifier,
|
getAssetProfileIdentifier,
|
||||||
parseDate
|
parseDate
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
@ -24,7 +25,7 @@ import {
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
|
import { endOfToday, format, isAfter, isSameDay, parseISO } from 'date-fns';
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@ -248,17 +249,20 @@ export class ImportService {
|
|||||||
|
|
||||||
const activities: Activity[] = [];
|
const activities: Activity[] = [];
|
||||||
|
|
||||||
for (const {
|
for (let [
|
||||||
accountId,
|
index,
|
||||||
comment,
|
{
|
||||||
date,
|
accountId,
|
||||||
error,
|
comment,
|
||||||
fee,
|
date,
|
||||||
quantity,
|
error,
|
||||||
SymbolProfile,
|
fee,
|
||||||
type,
|
quantity,
|
||||||
unitPrice
|
SymbolProfile,
|
||||||
} of activitiesExtendedWithErrors) {
|
type,
|
||||||
|
unitPrice
|
||||||
|
}
|
||||||
|
] of activitiesExtendedWithErrors.entries()) {
|
||||||
const assetProfile = assetProfiles[
|
const assetProfile = assetProfiles[
|
||||||
getAssetProfileIdentifier({
|
getAssetProfileIdentifier({
|
||||||
dataSource: SymbolProfile.dataSource,
|
dataSource: SymbolProfile.dataSource,
|
||||||
@ -296,6 +300,35 @@ export class ImportService {
|
|||||||
Account?: { id: string; name: string };
|
Account?: { id: string; name: string };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (SymbolProfile.currency !== assetProfile.currency) {
|
||||||
|
// Convert the unit price and fee to the asset currency if the imported
|
||||||
|
// activity is in a different currency
|
||||||
|
unitPrice = await this.exchangeRateDataService.toCurrencyAtDate(
|
||||||
|
unitPrice,
|
||||||
|
SymbolProfile.currency,
|
||||||
|
assetProfile.currency,
|
||||||
|
date
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!unitPrice) {
|
||||||
|
throw new Error(
|
||||||
|
`activities.${index} historical exchange rate at ${format(
|
||||||
|
date,
|
||||||
|
DATE_FORMAT
|
||||||
|
)} is not available from "${SymbolProfile.currency}" to "${
|
||||||
|
assetProfile.currency
|
||||||
|
}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fee = await this.exchangeRateDataService.toCurrencyAtDate(
|
||||||
|
fee,
|
||||||
|
SymbolProfile.currency,
|
||||||
|
assetProfile.currency,
|
||||||
|
date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isDryRun) {
|
if (isDryRun) {
|
||||||
order = {
|
order = {
|
||||||
comment,
|
comment,
|
||||||
@ -533,15 +566,21 @@ export class ImportService {
|
|||||||
])
|
])
|
||||||
)?.[symbol];
|
)?.[symbol];
|
||||||
|
|
||||||
if (assetProfile === undefined) {
|
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}")`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetProfile.currency !== currency) {
|
if (
|
||||||
|
assetProfile.currency !== currency &&
|
||||||
|
!this.exchangeRateDataService.hasCurrencyPair(
|
||||||
|
currency,
|
||||||
|
assetProfile.currency
|
||||||
|
)
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}"`
|
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,13 @@
|
|||||||
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,
|
||||||
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,
|
||||||
@ -30,9 +31,9 @@ import { permissions } from '@ghostfolio/common/permissions';
|
|||||||
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bent from 'bent';
|
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { format, subDays } from 'date-fns';
|
import { format, subDays } from 'date-fns';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InfoService {
|
export class InfoService {
|
||||||
@ -44,10 +45,10 @@ 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> {
|
||||||
@ -139,18 +140,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,17 +168,13 @@ export class InfoService {
|
|||||||
|
|
||||||
private async countDockerHubPulls(): Promise<number> {
|
private async countDockerHubPulls(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const { pull_count } = await got(
|
||||||
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
|
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
|
||||||
'GET',
|
|
||||||
'json',
|
|
||||||
200,
|
|
||||||
{
|
{
|
||||||
'User-Agent': 'request'
|
headers: { 'User-Agent': 'request' }
|
||||||
}
|
}
|
||||||
);
|
).json<any>();
|
||||||
|
|
||||||
const { pull_count } = await get();
|
|
||||||
return pull_count;
|
return pull_count;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'InfoService');
|
Logger.error(error, 'InfoService');
|
||||||
@ -193,16 +185,9 @@ export class InfoService {
|
|||||||
|
|
||||||
private async countGitHubContributors(): Promise<number> {
|
private async countGitHubContributors(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const { body } = await got('https://github.com/ghostfolio/ghostfolio');
|
||||||
'https://github.com/ghostfolio/ghostfolio',
|
|
||||||
'GET',
|
|
||||||
'string',
|
|
||||||
200,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const html = await get();
|
const $ = cheerio.load(body);
|
||||||
const $ = cheerio.load(html);
|
|
||||||
|
|
||||||
return extractNumberFromString(
|
return extractNumberFromString(
|
||||||
$(
|
$(
|
||||||
@ -218,17 +203,13 @@ export class InfoService {
|
|||||||
|
|
||||||
private async countGitHubStargazers(): Promise<number> {
|
private async countGitHubStargazers(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const { stargazers_count } = await got(
|
||||||
`https://api.github.com/repos/ghostfolio/ghostfolio`,
|
`https://api.github.com/repos/ghostfolio/ghostfolio`,
|
||||||
'GET',
|
|
||||||
'json',
|
|
||||||
200,
|
|
||||||
{
|
{
|
||||||
'User-Agent': 'request'
|
headers: { 'User-Agent': 'request' }
|
||||||
}
|
}
|
||||||
);
|
).json<any>();
|
||||||
|
|
||||||
const { stargazers_count } = await get();
|
|
||||||
return stargazers_count;
|
return stargazers_count;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'InfoService');
|
Logger.error(error, 'InfoService');
|
||||||
@ -238,10 +219,7 @@ export class InfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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: [
|
||||||
{
|
{
|
||||||
@ -332,11 +310,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> {
|
||||||
@ -346,22 +323,21 @@ export class InfoService {
|
|||||||
PROPERTY_BETTER_UPTIME_MONITOR_ID
|
PROPERTY_BETTER_UPTIME_MONITOR_ID
|
||||||
)) as string;
|
)) as string;
|
||||||
|
|
||||||
const get = bent(
|
const { data } = await got(
|
||||||
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
`https://betteruptime.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)}`,
|
||||||
'GET',
|
|
||||||
'json',
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
Authorization: `Bearer ${this.configurationService.get(
|
|
||||||
'BETTER_UPTIME_API_KEY'
|
|
||||||
)}`
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = await get();
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.configurationService.get(
|
||||||
|
'BETTER_UPTIME_API_KEY'
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).json<any>();
|
||||||
|
|
||||||
return data.attributes.availability / 100;
|
return data.attributes.availability / 100;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'InfoService');
|
Logger.error(error, 'InfoService');
|
||||||
|
@ -2,7 +2,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/sy
|
|||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { HttpException, Injectable } from '@nestjs/common';
|
import { HttpException, Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import got from 'got';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -41,15 +41,11 @@ export class LogoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getBuffer(aUrl: string) {
|
private getBuffer(aUrl: string) {
|
||||||
const get = bent(
|
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`,
|
||||||
'GET',
|
|
||||||
'buffer',
|
|
||||||
200,
|
|
||||||
{
|
{
|
||||||
'User-Agent': 'request'
|
headers: { 'User-Agent': 'request' }
|
||||||
}
|
}
|
||||||
);
|
).buffer();
|
||||||
return get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,6 @@ describe('CurrentRateService', () => {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
marketDataService = new MarketDataService(null);
|
marketDataService = new MarketDataService(null);
|
||||||
|
@ -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'))
|
||||||
@ -442,8 +441,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);
|
||||||
|
@ -11,12 +11,12 @@ import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/ac
|
|||||||
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 { 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
|
||||||
@ -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
|
||||||
@ -1046,9 +1039,8 @@ 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(
|
const positions = currentPositions.positions.filter(
|
||||||
(item) => !item.quantity.eq(0)
|
(item) => !item.quantity.eq(0)
|
||||||
@ -1238,9 +1230,8 @@ export class PortfolioService {
|
|||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
|
|
||||||
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 positions = currentPositions.positions.filter(
|
const positions = currentPositions.positions.filter(
|
||||||
(item) => !item.quantity.eq(0)
|
(item) => !item.quantity.eq(0)
|
||||||
@ -1772,7 +1763,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,
|
||||||
@ -1994,7 +1985,7 @@ export class PortfolioService {
|
|||||||
return (
|
return (
|
||||||
aUser.Settings?.settings.baseCurrency ??
|
aUser.Settings?.settings.baseCurrency ??
|
||||||
this.request.user?.Settings?.settings.baseCurrency ??
|
this.request.user?.Settings?.settings.baseCurrency ??
|
||||||
this.baseCurrency
|
DEFAULT_CURRENCY
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
apps/api/src/app/sitemap/sitemap.controller.ts
Normal file
36
apps/api/src/app/sitemap/sitemap.controller.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
|
getYesterday,
|
||||||
|
interpolate
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
|
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
@Controller('sitemap.xml')
|
||||||
|
export class SitemapController {
|
||||||
|
public sitemapXml = '';
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
try {
|
||||||
|
this.sitemapXml = fs.readFileSync(
|
||||||
|
path.join(__dirname, 'assets', 'sitemap.xml'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
public async flushCache(@Res() response: Response): Promise<void> {
|
||||||
|
response.setHeader('content-type', 'application/xml');
|
||||||
|
response.send(
|
||||||
|
interpolate(this.sitemapXml, {
|
||||||
|
currentDate: format(getYesterday(), DATE_FORMAT)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
apps/api/src/app/sitemap/sitemap.module.ts
Normal file
24
apps/api/src/app/sitemap/sitemap.module.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { SitemapController } from './sitemap.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [SitemapController],
|
||||||
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
|
PrismaModule,
|
||||||
|
RedisCacheModule,
|
||||||
|
SymbolProfileModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SitemapModule {}
|
@ -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,6 +15,7 @@ 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';
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ export class SymbolController {
|
|||||||
@Param('dateString') dateString: string,
|
@Param('dateString') dateString: string,
|
||||||
@Param('symbol') symbol: string
|
@Param('symbol') symbol: string
|
||||||
): Promise<IDataProviderHistoricalResponse> {
|
): Promise<IDataProviderHistoricalResponse> {
|
||||||
const date = new Date(dateString);
|
const date = parseISO(dateString);
|
||||||
|
|
||||||
if (!isDate(date)) {
|
if (!isDate(date)) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
|
@ -4,7 +4,11 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
|
|||||||
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 { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||||
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
|
locale
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
|
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import {
|
||||||
getPermissions,
|
getPermissions,
|
||||||
@ -21,18 +25,16 @@ const crypto = require('crypto');
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
public static DEFAULT_CURRENCY = 'USD';
|
|
||||||
|
|
||||||
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(
|
||||||
@ -145,8 +147,7 @@ export class UserService {
|
|||||||
|
|
||||||
// Set default value for base currency
|
// Set default value for base currency
|
||||||
if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
|
if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
|
||||||
(user.Settings.settings as UserSettings).baseCurrency =
|
(user.Settings.settings as UserSettings).baseCurrency = DEFAULT_CURRENCY;
|
||||||
UserService.DEFAULT_CURRENCY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default value for date range
|
// Set default value for date range
|
||||||
@ -186,6 +187,9 @@ export class UserService {
|
|||||||
if (Analytics?.activityCount % frequency === 1) {
|
if (Analytics?.activityCount % frequency === 1) {
|
||||||
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset benchmark
|
||||||
|
user.Settings.settings.benchmark = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.subscription?.type === 'Premium') {
|
if (user.subscription?.type === 'Premium') {
|
||||||
@ -263,7 +267,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'
|
||||||
}
|
}
|
||||||
@ -271,7 +275,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",
|
||||||
|
@ -50,6 +50,110 @@
|
|||||||
<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-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-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-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>
|
||||||
@ -66,6 +170,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ueber-uns/lizenz</loc>
|
<loc>https://ghostfol.io/de/ueber-uns/lizenz</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/ueber-uns/oss-friends</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en</loc>
|
<loc>https://ghostfol.io/en</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -82,6 +190,10 @@
|
|||||||
<loc>https://ghostfol.io/en/about/license</loc>
|
<loc>https://ghostfol.io/en/about/license</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/about/oss-friends</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog</loc>
|
<loc>https://ghostfol.io/en/blog</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -134,6 +246,14 @@
|
|||||||
<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>
|
<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>
|
||||||
@ -314,6 +434,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>
|
||||||
@ -334,6 +458,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>
|
||||||
@ -388,12 +516,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>
|
||||||
@ -452,6 +584,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>
|
||||||
@ -468,6 +604,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</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</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/blog</loc>
|
<loc>https://ghostfol.io/pt/blog</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -512,6 +652,10 @@
|
|||||||
<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>
|
||||||
|
@ -7,6 +7,7 @@ import helmet from 'helmet';
|
|||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
import { environment } from './environments/environment';
|
import { environment } from './environments/environment';
|
||||||
|
import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const configApp = await NestFactory.create(AppModule);
|
const configApp = await NestFactory.create(AppModule);
|
||||||
@ -23,7 +24,7 @@ async function bootstrap() {
|
|||||||
defaultVersion: '1',
|
defaultVersion: '1',
|
||||||
type: VersioningType.URI
|
type: VersioningType.URI
|
||||||
});
|
});
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api', { exclude: ['sitemap.xml'] });
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
forbidNonWhitelisted: true,
|
forbidNonWhitelisted: true,
|
||||||
@ -40,6 +41,7 @@ async function bootstrap() {
|
|||||||
helmet({
|
helmet({
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
directives: {
|
directives: {
|
||||||
|
connectSrc: ["'self'", 'https://js.stripe.com'], // Allow connections to Stripe
|
||||||
frameSrc: ["'self'", 'https://js.stripe.com'], // Allow loading frames from Stripe
|
frameSrc: ["'self'", 'https://js.stripe.com'], // Allow loading frames from Stripe
|
||||||
scriptSrc: ["'self'", "'unsafe-inline'", 'https://js.stripe.com'], // Allow inline scripts and scripts from Stripe
|
scriptSrc: ["'self'", "'unsafe-inline'", 'https://js.stripe.com'], // Allow inline scripts and scripts from Stripe
|
||||||
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
|
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
|
||||||
@ -51,7 +53,8 @@ async function bootstrap() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
|
app.use(HtmlTemplateMiddleware);
|
||||||
|
|
||||||
const HOST = configService.get<string>('HOST') || '0.0.0.0';
|
const HOST = configService.get<string>('HOST') || '0.0.0.0';
|
||||||
const PORT = configService.get<number>('PORT') || 3333;
|
const PORT = configService.get<number>('PORT') || 3333;
|
||||||
|
|
||||||
@ -59,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.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
136
apps/api/src/middlewares/html-template.middleware.ts
Normal file
136
apps/api/src/middlewares/html-template.middleware.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
|
import {
|
||||||
|
DEFAULT_LANGUAGE_CODE,
|
||||||
|
DEFAULT_ROOT_URL,
|
||||||
|
SUPPORTED_LANGUAGE_CODES
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
|
import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
const descriptions = {
|
||||||
|
de: 'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
|
||||||
|
en: 'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.',
|
||||||
|
es: 'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
|
||||||
|
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.',
|
||||||
|
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.'
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = 'Ghostfolio – Open Source Wealth Management Software';
|
||||||
|
const titleShort = 'Ghostfolio';
|
||||||
|
|
||||||
|
let indexHtmlMap: { [languageCode: string]: string } = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
indexHtmlMap = SUPPORTED_LANGUAGE_CODES.reduce(
|
||||||
|
(map, languageCode) => ({
|
||||||
|
...map,
|
||||||
|
[languageCode]: fs.readFileSync(
|
||||||
|
join(__dirname, '..', 'client', languageCode, 'index.html'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const locales = {
|
||||||
|
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/ghostfolio-x-sackgeld.png',
|
||||||
|
title: `Ghostfolio auf Sackgeld.com vorgestellt - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2022/08/500-stars-on-github': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/500-stars-on-github.jpg',
|
||||||
|
title: `500 Stars - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2022/10/hacktoberfest-2022': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/hacktoberfest-2022.png',
|
||||||
|
title: `Hacktoberfest 2022 - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/20221226.jpg',
|
||||||
|
title: `The importance of tracking your personal finances - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2023/02/ghostfolio-meets-umbrel': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/ghostfolio-x-umbrel.png',
|
||||||
|
title: `Ghostfolio meets Umbrel - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/1000-stars-on-github.jpg',
|
||||||
|
title: `Ghostfolio reaches 1’000 Stars on GitHub - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/20230520.jpg',
|
||||||
|
title: `Unlock your Financial Potential with Ghostfolio - ${titleShort}`
|
||||||
|
},
|
||||||
|
'/en/blog/2023/07/exploring-the-path-to-fire': {
|
||||||
|
featureGraphicPath: 'assets/images/blog/20230701.jpg',
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFileRequest = (filename: string) => {
|
||||||
|
if (filename === '/assets/LICENSE') {
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
filename.includes('auth/ey') ||
|
||||||
|
filename.includes(
|
||||||
|
'personal-finance-tools/open-source-alternative-to-markets.sh'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename.split('.').pop() !== filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HtmlTemplateMiddleware = async (
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
const path = request.originalUrl.replace(/\/$/, '');
|
||||||
|
let languageCode = path.substr(1, 2);
|
||||||
|
|
||||||
|
if (!SUPPORTED_LANGUAGE_CODES.includes(languageCode)) {
|
||||||
|
languageCode = DEFAULT_LANGUAGE_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = format(new Date(), DATE_FORMAT);
|
||||||
|
const rootUrl = process.env.ROOT_URL || DEFAULT_ROOT_URL;
|
||||||
|
|
||||||
|
if (
|
||||||
|
path.startsWith('/api/') ||
|
||||||
|
isFileRequest(path) ||
|
||||||
|
!environment.production
|
||||||
|
) {
|
||||||
|
// Skip
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
const indexHtml = interpolate(indexHtmlMap[languageCode], {
|
||||||
|
currentDate,
|
||||||
|
languageCode,
|
||||||
|
path,
|
||||||
|
rootUrl,
|
||||||
|
description: descriptions[languageCode],
|
||||||
|
featureGraphicPath:
|
||||||
|
locales[path]?.featureGraphicPath ?? 'assets/cover.png',
|
||||||
|
title: locales[path]?.title ?? title
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.send(indexHtml);
|
||||||
|
}
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
|
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
|
||||||
|
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';
|
||||||
@ -11,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: 'USD'
|
|
||||||
}),
|
|
||||||
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 }),
|
||||||
@ -46,7 +43,7 @@ export class ConfigurationService {
|
|||||||
REDIS_HOST: str({ default: 'localhost' }),
|
REDIS_HOST: str({ default: 'localhost' }),
|
||||||
REDIS_PASSWORD: str({ default: '' }),
|
REDIS_PASSWORD: str({ default: '' }),
|
||||||
REDIS_PORT: port({ default: 6379 }),
|
REDIS_PORT: port({ default: 6379 }),
|
||||||
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
ROOT_URL: str({ default: DEFAULT_ROOT_URL }),
|
||||||
STRIPE_PUBLIC_KEY: str({ default: '' }),
|
STRIPE_PUBLIC_KEY: str({ default: '' }),
|
||||||
STRIPE_SECRET_KEY: str({ default: '' }),
|
STRIPE_SECRET_KEY: str({ default: '' }),
|
||||||
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),
|
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),
|
||||||
|
@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config';
|
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config';
|
||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
@ -28,6 +29,7 @@ import { DataGatheringProcessor } from './data-gathering.processor';
|
|||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
PropertyModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule
|
||||||
],
|
],
|
||||||
providers: [DataGatheringProcessor, DataGatheringService],
|
providers: [DataGatheringProcessor, DataGatheringService],
|
||||||
|
@ -4,18 +4,20 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import {
|
import {
|
||||||
DATA_GATHERING_QUEUE,
|
DATA_GATHERING_QUEUE,
|
||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getAssetProfileIdentifier,
|
getAssetProfileIdentifier,
|
||||||
resetHours
|
resetHours
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { BenchmarkProperty, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
@ -34,6 +36,7 @@ export class DataGatheringService {
|
|||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly propertyService: PropertyService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -124,12 +127,10 @@ export class DataGatheringService {
|
|||||||
uniqueAssets = await this.getUniqueAssets();
|
uniqueAssets = await this.getUniqueAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetProfiles = await this.dataProviderService.getAssetProfiles(
|
const assetProfiles =
|
||||||
uniqueAssets
|
await this.dataProviderService.getAssetProfiles(uniqueAssets);
|
||||||
);
|
const symbolProfiles =
|
||||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
await this.symbolProfileService.getSymbolProfiles(uniqueAssets);
|
||||||
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) => {
|
||||||
@ -255,6 +256,10 @@ export class DataGatheringService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getEarliestDate(aStartDate: Date) {
|
||||||
|
return min([aStartDate, subYears(new Date(), 10)]);
|
||||||
|
}
|
||||||
|
|
||||||
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
@ -321,6 +326,14 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
||||||
|
const benchmarkAssetProfileIdMap: { [key: string]: boolean } = {};
|
||||||
|
(
|
||||||
|
((await this.propertyService.getByKey(
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
|
)) as BenchmarkProperty[]) ?? []
|
||||||
|
).forEach(({ symbolProfileId }) => {
|
||||||
|
benchmarkAssetProfileIdMap[symbolProfileId] = true;
|
||||||
|
});
|
||||||
const startDate =
|
const startDate =
|
||||||
(
|
(
|
||||||
await this.prismaService.order.findFirst({
|
await this.prismaService.order.findFirst({
|
||||||
@ -334,7 +347,7 @@ export class DataGatheringService {
|
|||||||
return {
|
return {
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: min([startDate, subYears(new Date(), 10)])
|
date: this.getEarliestDate(startDate)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -343,6 +356,7 @@ export class DataGatheringService {
|
|||||||
orderBy: [{ symbol: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: {
|
select: {
|
||||||
dataSource: true,
|
dataSource: true,
|
||||||
|
id: true,
|
||||||
Order: {
|
Order: {
|
||||||
orderBy: [{ date: 'asc' }],
|
orderBy: [{ date: 'asc' }],
|
||||||
select: { date: true },
|
select: { date: true },
|
||||||
@ -364,9 +378,15 @@ export class DataGatheringService {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((symbolProfile) => {
|
.map((symbolProfile) => {
|
||||||
|
let date = symbolProfile.Order?.[0]?.date ?? startDate;
|
||||||
|
|
||||||
|
if (benchmarkAssetProfileIdMap[symbolProfile.id]) {
|
||||||
|
date = this.getEarliestDate(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...symbolProfile,
|
...symbolProfile,
|
||||||
date: symbolProfile.Order?.[0]?.date ?? startDate
|
date
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
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 } 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';
|
||||||
@ -15,19 +15,14 @@ import {
|
|||||||
DataSource,
|
DataSource,
|
||||||
SymbolProfile
|
SymbolProfile
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import bent from 'bent';
|
|
||||||
import { format, fromUnixTime, getUnixTime } from 'date-fns';
|
import { format, fromUnixTime, getUnixTime } from 'date-fns';
|
||||||
|
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,14 +34,13 @@ 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 get = bent(`${this.URL}/coins/${aSymbol}`, 'GET', 'json', 200);
|
const { name } = await got(`${this.URL}/coins/${aSymbol}`).json<any>();
|
||||||
const { name } = await get();
|
|
||||||
|
|
||||||
response.name = name;
|
response.name = name;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -79,17 +73,13 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
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)}`
|
||||||
'GET',
|
).json<any>();
|
||||||
'json',
|
|
||||||
200
|
|
||||||
);
|
|
||||||
const { prices } = await get();
|
|
||||||
|
|
||||||
const result: {
|
const result: {
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
@ -132,23 +122,19 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
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()}`
|
||||||
'GET',
|
).json<any>();
|
||||||
'json',
|
|
||||||
200
|
|
||||||
);
|
|
||||||
const response = await get();
|
|
||||||
|
|
||||||
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'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -174,8 +160,9 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
let items: LookupItem[] = [];
|
let items: LookupItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(`${this.URL}/search?query=${query}`, 'GET', 'json', 200);
|
const { coins } = await got(
|
||||||
const { coins } = await get();
|
`${this.URL}/search?query=${query}`
|
||||||
|
).json<any>();
|
||||||
|
|
||||||
items = coins.map(({ id: symbol, name }) => {
|
items = coins.map(({ id: symbol, name }) => {
|
||||||
return {
|
return {
|
||||||
@ -183,7 +170,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;
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,11 @@ 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';
|
||||||
import { SymbolProfile } from '@prisma/client';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import bent from 'bent';
|
import got from 'got';
|
||||||
|
|
||||||
const getJSON = bent('json');
|
|
||||||
|
|
||||||
@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'
|
||||||
@ -34,27 +32,43 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await getJSON(
|
const profile = await got(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
|
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`
|
||||||
).catch(() => {
|
)
|
||||||
return {};
|
.json<any>()
|
||||||
});
|
.catch(() => {
|
||||||
|
return got(
|
||||||
|
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
|
||||||
|
'.'
|
||||||
|
)?.[0]}.json`
|
||||||
|
)
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const holdings = await getJSON(
|
const holdings = await got(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
|
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
|
||||||
).catch(() => {
|
)
|
||||||
return getJSON(
|
.json<any>()
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
|
.catch(() => {
|
||||||
symbol.split('.')?.[0]
|
return got(
|
||||||
}.json`
|
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
|
||||||
);
|
'.'
|
||||||
});
|
)?.[0]}.json`
|
||||||
|
)
|
||||||
|
.json<any>()
|
||||||
|
.catch(() => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (holdings?.weight < 0.95) {
|
if (holdings?.weight < 0.95) {
|
||||||
// Skip if data is inaccurate
|
// Skip if data is inaccurate
|
||||||
@ -112,4 +126,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,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, 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';
|
||||||
@ -14,21 +18,19 @@ import {
|
|||||||
DataSource,
|
DataSource,
|
||||||
SymbolProfile
|
SymbolProfile
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import bent from 'bent';
|
|
||||||
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';
|
||||||
|
|
||||||
@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) {
|
||||||
@ -76,19 +78,19 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
const symbol = this.convertToEodSymbol(aSymbol);
|
const symbol = this.convertToEodSymbol(aSymbol);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const response = await got(
|
||||||
`${this.URL}/eod/${symbol}?api_token=${
|
`${this.URL}/eod/${symbol}?api_token=${
|
||||||
this.apiKey
|
this.apiKey
|
||||||
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
|
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
|
||||||
to,
|
to,
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)}&period={aGranularity}`,
|
)}&period={aGranularity}`,
|
||||||
'GET',
|
{
|
||||||
'json',
|
timeout: {
|
||||||
200
|
request: DEFAULT_REQUEST_TIMEOUT
|
||||||
);
|
}
|
||||||
|
}
|
||||||
const response = await get();
|
).json<any>();
|
||||||
|
|
||||||
return response.reduce(
|
return response.reduce(
|
||||||
(result, historicalItem, index, array) => {
|
(result, historicalItem, index, array) => {
|
||||||
@ -136,16 +138,16 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
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(',')}`,
|
||||||
'GET',
|
{
|
||||||
'json',
|
timeout: {
|
||||||
200
|
request: DEFAULT_REQUEST_TIMEOUT
|
||||||
);
|
}
|
||||||
|
}
|
||||||
const realTimeResponse = await get();
|
).json<any>();
|
||||||
|
|
||||||
const quotes =
|
const quotes =
|
||||||
symbols.length === 1 ? [realTimeResponse] : realTimeResponse;
|
symbols.length === 1 ? [realTimeResponse] : realTimeResponse;
|
||||||
@ -174,7 +176,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'
|
||||||
@ -185,24 +187,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
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -271,7 +273,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;
|
||||||
@ -284,17 +286,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`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,10 +310,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();
|
||||||
}
|
}
|
||||||
@ -329,13 +331,14 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
let searchResult = [];
|
let searchResult = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const response = await got(
|
||||||
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
|
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
|
||||||
'GET',
|
{
|
||||||
'json',
|
timeout: {
|
||||||
200
|
request: DEFAULT_REQUEST_TIMEOUT
|
||||||
);
|
}
|
||||||
const response = await get();
|
}
|
||||||
|
).json<any>();
|
||||||
|
|
||||||
searchResult = response.map(
|
searchResult = response.map(
|
||||||
({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {
|
({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {
|
||||||
|
@ -5,18 +5,18 @@ 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, 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';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import bent from 'bent';
|
|
||||||
import { format, isAfter, isBefore, isSameDay } from 'date-fns';
|
import { format, isAfter, isBefore, isSameDay } from 'date-fns';
|
||||||
|
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 +25,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,13 +63,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const { historical } = await got(
|
||||||
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
|
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`
|
||||||
'GET',
|
).json<any>();
|
||||||
'json',
|
|
||||||
200
|
|
||||||
);
|
|
||||||
const { historical } = await get();
|
|
||||||
|
|
||||||
const result: {
|
const result: {
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
@ -115,17 +110,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const response = await got(
|
||||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
|
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`
|
||||||
'GET',
|
).json<any>();
|
||||||
'json',
|
|
||||||
200
|
|
||||||
);
|
|
||||||
const response = await get();
|
|
||||||
|
|
||||||
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,
|
||||||
@ -153,13 +144,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
let items: LookupItem[] = [];
|
let items: LookupItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const result = await got(
|
||||||
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
|
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`
|
||||||
'GET',
|
).json<any>();
|
||||||
'json',
|
|
||||||
200
|
|
||||||
);
|
|
||||||
const result = await get();
|
|
||||||
|
|
||||||
items = result.map(({ currency, name, symbol }) => {
|
items = result.map(({ currency, name, symbol }) => {
|
||||||
return {
|
return {
|
||||||
|
@ -10,4 +10,6 @@ export interface DataEnhancerInterface {
|
|||||||
}): Promise<Partial<SymbolProfile>>;
|
}): Promise<Partial<SymbolProfile>>;
|
||||||
|
|
||||||
getName(): string;
|
getName(): string;
|
||||||
|
|
||||||
|
getTestSymbol(): string;
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,10 @@ import {
|
|||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import bent from 'bent';
|
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { isUUID } from 'class-validator';
|
import { isUUID } from 'class-validator';
|
||||||
import { addDays, format, isBefore } from 'date-fns';
|
import { addDays, format, isBefore } from 'date-fns';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ManualService implements DataProviderInterface {
|
export class ManualService implements DataProviderInterface {
|
||||||
@ -95,10 +95,9 @@ export class ManualService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const get = bent(url, 'GET', 'string', 200, headers);
|
const { body } = await got(url, { headers });
|
||||||
|
|
||||||
const html = await get();
|
const $ = cheerio.load(body);
|
||||||
const $ = cheerio.load(html);
|
|
||||||
|
|
||||||
const value = extractNumberFromString($(selector).text());
|
const value = extractNumberFromString($(selector).text());
|
||||||
|
|
||||||
|
@ -10,8 +10,8 @@ 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';
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import bent from 'bent';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RapidApiService implements DataProviderInterface {
|
export class RapidApiService implements DataProviderInterface {
|
||||||
@ -135,19 +135,17 @@ export class RapidApiService implements DataProviderInterface {
|
|||||||
oneYearAgo: { value: number; valueText: string };
|
oneYearAgo: { value: number; valueText: string };
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const { fgi } = await got(
|
||||||
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
|
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
|
||||||
'GET',
|
|
||||||
'json',
|
|
||||||
200,
|
|
||||||
{
|
{
|
||||||
useQueryString: true,
|
headers: {
|
||||||
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
useQueryString: 'true',
|
||||||
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
|
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
||||||
|
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
).json<any>();
|
||||||
|
|
||||||
const { fgi } = await get();
|
|
||||||
return fgi;
|
return fgi;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'RapidApiService');
|
Logger.error(error, 'RapidApiService');
|
||||||
|
@ -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,15 +26,23 @@ 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() {
|
||||||
return this.currencyPairs;
|
return this.currencyPairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasCurrencyPair(currency1: string, currency2: string) {
|
||||||
|
return this.currencyPairs.some(({ symbol }) => {
|
||||||
|
return (
|
||||||
|
symbol === `${currency1}${currency2}` ||
|
||||||
|
symbol === `${currency2}${currency1}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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 = {};
|
||||||
@ -104,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
|
||||||
@ -135,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;
|
||||||
|
|
||||||
@ -195,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;
|
||||||
}
|
}
|
||||||
@ -286,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}`,
|
||||||
|
@ -4,25 +4,29 @@ import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strate
|
|||||||
|
|
||||||
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 = [
|
||||||
...[
|
{
|
||||||
'about',
|
path: paths.about,
|
||||||
/////
|
|
||||||
'a-propos',
|
|
||||||
'informazioni-su',
|
|
||||||
'over',
|
|
||||||
'sobre',
|
|
||||||
'ueber-uns'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
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: () =>
|
||||||
import('./pages/account/account-page.module').then(
|
import('./pages/user-account/user-account-page.module').then(
|
||||||
(m) => m.AccountPageModule
|
(m) => m.UserAccountPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -42,64 +46,40 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
||||||
},
|
},
|
||||||
...['blog'].map((path) => ({
|
{
|
||||||
path,
|
path: 'blog',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||||
})),
|
},
|
||||||
{
|
{
|
||||||
path: 'demo',
|
path: 'demo',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
||||||
},
|
},
|
||||||
...[
|
{
|
||||||
'faq',
|
path: paths.faq,
|
||||||
/////
|
|
||||||
'domande-piu-frequenti',
|
|
||||||
'foire-aux-questions',
|
|
||||||
'haeufig-gestellte-fragen',
|
|
||||||
'perguntas-mais-frequentes',
|
|
||||||
'preguntas-mas-frecuentes',
|
|
||||||
'vaak-gestelde-vragen'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||||
})),
|
},
|
||||||
...[
|
{
|
||||||
'features',
|
path: paths.features,
|
||||||
/////
|
|
||||||
'fonctionnalites',
|
|
||||||
'funcionalidades',
|
|
||||||
'funzionalita',
|
|
||||||
'kenmerken'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
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)
|
||||||
},
|
},
|
||||||
...[
|
{
|
||||||
'markets',
|
path: paths.markets,
|
||||||
/////
|
|
||||||
'maerkte',
|
|
||||||
'marches',
|
|
||||||
'markten',
|
|
||||||
'mercados',
|
|
||||||
'mercati'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
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: () =>
|
||||||
@ -119,53 +99,27 @@ const routes: Routes = [
|
|||||||
(m) => m.PortfolioPageModule
|
(m) => m.PortfolioPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
...[
|
{
|
||||||
'pricing',
|
path: paths.pricing,
|
||||||
/////
|
|
||||||
'precios',
|
|
||||||
'precos',
|
|
||||||
'preise',
|
|
||||||
'prezzi',
|
|
||||||
'prijzen',
|
|
||||||
'prix'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/pricing/pricing-page.module').then(
|
import('./pages/pricing/pricing-page.module').then(
|
||||||
(m) => m.PricingPageModule
|
(m) => m.PricingPageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
...[
|
{
|
||||||
'register',
|
path: paths.register,
|
||||||
/////
|
|
||||||
'enregistrement',
|
|
||||||
'iscrizione',
|
|
||||||
'registo',
|
|
||||||
'registratie',
|
|
||||||
'registrierung',
|
|
||||||
'registro'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/register/register-page.module').then(
|
import('./pages/register/register-page.module').then(
|
||||||
(m) => m.RegisterPageModule
|
(m) => m.RegisterPageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
...[
|
{
|
||||||
'resources',
|
path: paths.resources,
|
||||||
/////
|
|
||||||
'bronnen',
|
|
||||||
'recursos',
|
|
||||||
'ressourcen',
|
|
||||||
'ressources',
|
|
||||||
'risorse'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
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: () =>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<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 px-3 py-2"
|
||||||
@ -43,22 +43,7 @@
|
|||||||
<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 +53,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>
|
||||||
|
@ -38,6 +38,20 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
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 +103,19 @@ 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.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('–');
|
||||||
|
@ -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({
|
||||||
|
@ -154,7 +154,7 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
|
|||||||
day: string;
|
day: string;
|
||||||
yearMonth: string;
|
yearMonth: string;
|
||||||
}) {
|
}) {
|
||||||
const date = new Date(`${yearMonth}-${day}`);
|
const date = parseISO(`${yearMonth}-${day}`);
|
||||||
const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice;
|
const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice;
|
||||||
|
|
||||||
if (isSameDay(date, new Date())) {
|
if (isSameDay(date, new Date())) {
|
||||||
|
@ -60,6 +60,11 @@ export class AdminMarketDataComponent
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.concat([
|
.concat([
|
||||||
|
{
|
||||||
|
id: 'CURRENCIES',
|
||||||
|
label: $localize`Currencies`,
|
||||||
|
type: <Filter['type']>'PRESET_ID'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'ETF_WITHOUT_COUNTRIES',
|
id: 'ETF_WITHOUT_COUNTRIES',
|
||||||
label: $localize`ETFs without Countries`,
|
label: $localize`ETFs without Countries`,
|
||||||
|
@ -169,6 +169,8 @@
|
|||||||
<mat-option value="7 days">7 Days</mat-option>
|
<mat-option value="7 days">7 Days</mat-option>
|
||||||
<mat-option value="14 days">14 Days</mat-option>
|
<mat-option value="14 days">14 Days</mat-option>
|
||||||
<mat-option value="30 days">30 Days</mat-option>
|
<mat-option value="30 days">30 Days</mat-option>
|
||||||
|
<mat-option value="90 days">90 Days</mat-option>
|
||||||
|
<mat-option value="180 days">180 Days</mat-option>
|
||||||
<mat-option value="1 year">1 Year</mat-option>
|
<mat-option value="1 year">1 Year</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="text-center" i18n>Platforms</h3>
|
<h2 class="text-center" i18n>Platforms</h2>
|
||||||
<gf-admin-platform></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">
|
||||||
<h3 class="text-center" i18n>Tags</h3>
|
<h2 class="text-center" i18n>Tags</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
@ -39,7 +39,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
@ -52,7 +52,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item">
|
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
@ -65,14 +65,14 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'resources',
|
'font-weight-bold': currentRoute === routeResources,
|
||||||
'text-decoration-underline': currentRoute === 'resources'
|
'text-decoration-underline': currentRoute === routeResources
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/resources']"
|
[routerLink]="routerLinkResources"
|
||||||
>Resources</a
|
>Resources</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@ -83,27 +83,27 @@
|
|||||||
class="list-inline-item"
|
class="list-inline-item"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'pricing',
|
'font-weight-bold': currentRoute === routePricing,
|
||||||
'text-decoration-underline': currentRoute === 'pricing'
|
'text-decoration-underline': currentRoute === routePricing
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/pricing']"
|
[routerLink]="routerLinkPricing"
|
||||||
>Pricing</a
|
>Pricing</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'about',
|
'font-weight-bold': currentRoute === routeAbout,
|
||||||
'text-decoration-underline': currentRoute === 'about'
|
'text-decoration-underline': currentRoute === routeAbout
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/about']"
|
[routerLink]="routerLinkAbout"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@ -129,33 +129,37 @@
|
|||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
<ng-container *ngIf="user?.access?.length > 0">
|
<ng-container *ngIf="user?.access?.length > 0">
|
||||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||||
<ion-icon
|
<span class="align-items-center d-flex">
|
||||||
*ngIf="user?.access?.length > 0"
|
<ion-icon
|
||||||
class="mr-2"
|
*ngIf="user?.access?.length > 0"
|
||||||
[name]="
|
class="mr-2"
|
||||||
impersonationId
|
[name]="
|
||||||
? 'radio-button-off-outline'
|
impersonationId
|
||||||
: 'radio-button-on-outline'
|
? 'radio-button-off-outline'
|
||||||
"
|
: 'radio-button-on-outline'
|
||||||
></ion-icon>
|
"
|
||||||
<span i18n>Me</span>
|
></ion-icon>
|
||||||
|
<span i18n>Me</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngFor="let accessItem of user?.access"
|
*ngFor="let accessItem of user?.access"
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
(click)="impersonateAccount(accessItem.id)"
|
(click)="impersonateAccount(accessItem.id)"
|
||||||
>
|
>
|
||||||
<ion-icon
|
<span class="align-items-center d-flex">
|
||||||
class="mr-2"
|
<ion-icon
|
||||||
name="square-outline"
|
class="mr-2"
|
||||||
[name]="
|
name="square-outline"
|
||||||
accessItem.id === impersonationId
|
[name]="
|
||||||
? 'radio-button-on-outline'
|
accessItem.id === impersonationId
|
||||||
: 'radio-button-off-outline'
|
? 'radio-button-on-outline'
|
||||||
"
|
: 'radio-button-off-outline'
|
||||||
></ion-icon>
|
"
|
||||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
></ion-icon>
|
||||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||||
|
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<hr class="m-0" />
|
<hr class="m-0" />
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -210,9 +214,9 @@
|
|||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'resources'
|
'font-weight-bold': currentRoute === routeResources
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/resources']"
|
[routerLink]="routerLinkResources"
|
||||||
>Resources</a
|
>Resources</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -223,16 +227,16 @@
|
|||||||
class="d-flex d-sm-none"
|
class="d-flex d-sm-none"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'pricing' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === routePricing }"
|
||||||
[routerLink]="['/pricing']"
|
[routerLink]="routerLinkPricing"
|
||||||
>Pricing</a
|
>Pricing</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-flex d-sm-none"
|
class="d-flex d-sm-none"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === routeAbout }"
|
||||||
[routerLink]="['/about']"
|
[routerLink]="routerLinkAbout"
|
||||||
>About Ghostfolio</a
|
>About Ghostfolio</a
|
||||||
>
|
>
|
||||||
<hr class="d-flex d-sm-none m-0" />
|
<hr class="d-flex d-sm-none m-0" />
|
||||||
@ -256,39 +260,40 @@
|
|||||||
<ul class="alig-items-center d-flex list-inline m-0">
|
<ul class="alig-items-center d-flex list-inline m-0">
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'features',
|
'font-weight-bold': currentRoute === routeFeatures,
|
||||||
'text-decoration-underline': currentRoute === 'features'
|
'text-decoration-underline': currentRoute === routeFeatuers
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/features']"
|
[routerLink]="routerLinkFeatures"
|
||||||
>Features</a
|
>Features</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'about',
|
'font-weight-bold': currentRoute === routeAbout,
|
||||||
'text-decoration-underline': currentRoute === 'about'
|
'text-decoration-underline': currentRoute === routeAbout
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/about']"
|
[routerLink]="routerLinkAbout"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="hasPermissionForSubscription" class="list-inline-item">
|
<li *ngIf="hasPermissionForSubscription" class="list-inline-item">
|
||||||
<a
|
<a
|
||||||
|
class="d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'pricing',
|
'font-weight-bold': currentRoute === routePricing,
|
||||||
'text-decoration-underline': currentRoute === 'pricing'
|
'text-decoration-underline': currentRoute === routePricing
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/pricing']"
|
[routerLink]="routerLinkPricing"
|
||||||
>Pricing</a
|
>Pricing</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@ -297,14 +302,14 @@
|
|||||||
class="list-inline-item"
|
class="list-inline-item"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'markets',
|
'font-weight-bold': currentRoute === routeMarkets,
|
||||||
'text-decoration-underline': currentRoute === 'markets'
|
'text-decoration-underline': currentRoute === routeMarkets
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/markets']"
|
[routerLink]="routerLinkMarkets"
|
||||||
>Markets</a
|
>Markets</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@ -317,19 +322,19 @@
|
|||||||
></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<button class="mx-1" mat-flat-button (click)="openLoginDialog()">
|
<button class="d-sm-block" mat-flat-button (click)="openLoginDialog()">
|
||||||
<ng-container i18n>Sign in</ng-container>
|
<ng-container i18n>Sign in</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser"
|
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser"
|
||||||
class="list-inline-item"
|
class="list-inline-item ml-1"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block"
|
class="d-none d-sm-block"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[routerLink]="['/register']"
|
[routerLink]="routerLinkRegister"
|
||||||
><ng-container i18n>Get started</ng-container>
|
><ng-container i18n>Get started</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
.mat-toolbar {
|
.mat-toolbar {
|
||||||
background-color: var(--light-background);
|
background-color: var(--light-background);
|
||||||
|
|
||||||
.spacer {
|
.list-inline-item {
|
||||||
flex: 1 1 auto;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-button {
|
.mdc-button {
|
||||||
@ -24,6 +24,10 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,17 @@ export class HeaderComponent implements OnChanges {
|
|||||||
public hasPermissionToCreateUser: boolean;
|
public hasPermissionToCreateUser: boolean;
|
||||||
public impersonationId: string;
|
public impersonationId: string;
|
||||||
public isMenuOpen: boolean;
|
public isMenuOpen: boolean;
|
||||||
|
public routeAbout = $localize`about`;
|
||||||
|
public routeFeatures = $localize`features`;
|
||||||
|
public routeMarkets = $localize`markets`;
|
||||||
|
public routePricing = $localize`pricing`;
|
||||||
|
public routeResources = $localize`resources`;
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkMarkets = ['/' + $localize`markets`];
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
public routerLinkRegister = ['/' + $localize`register`];
|
||||||
|
public routerLinkResources = ['/' + $localize`resources`];
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Markets</h3>
|
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
<div class="mb-2 text-center text-muted">
|
<div class="mb-2 text-center text-muted">
|
||||||
|
@ -101,7 +101,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioDetails({})
|
.fetchPortfolioDetails()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ summary }) => {
|
.subscribe(({ summary }) => {
|
||||||
this.summary = summary;
|
this.summary = summary;
|
||||||
@ -121,7 +121,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.snackBarRef.onAction().subscribe(() => {
|
this.snackBarRef.onAction().subscribe(() => {
|
||||||
this.router.navigate(['/pricing']);
|
this.router.navigate(['/' + $localize`pricing`]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="container pb-3 px-3">
|
<div class="container pb-3 px-3">
|
||||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3>
|
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Summary</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
<mat-card appearance="outlined">
|
<mat-card appearance="outlined">
|
||||||
|
@ -50,7 +50,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
|||||||
public onEditEmergencyFund() {
|
public onEditEmergencyFund() {
|
||||||
const emergencyFundInput = prompt(
|
const emergencyFundInput = prompt(
|
||||||
$localize`Please enter the amount of your emergency fund:`,
|
$localize`Please enter the amount of your emergency fund:`,
|
||||||
this.summary.emergencyFund?.toString() ?? '0'
|
this.summary.emergencyFund?.total?.toString() ?? '0'
|
||||||
);
|
);
|
||||||
const emergencyFund = parseFloat(emergencyFundInput?.trim());
|
const emergencyFund = parseFloat(emergencyFundInput?.trim());
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
|
|||||||
templateUrl: 'subscription-interstitial-dialog.html'
|
templateUrl: 'subscription-interstitial-dialog.html'
|
||||||
})
|
})
|
||||||
export class SubscriptionInterstitialDialog {
|
export class SubscriptionInterstitialDialog {
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
|
||||||
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>
|
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
<a
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[routerLink]="['/pricing']"
|
[routerLink]="routerLinkPricing"
|
||||||
(click)="closeDialog()"
|
(click)="closeDialog()"
|
||||||
>
|
>
|
||||||
<span i18n>Upgrade Plan</span>
|
<span i18n>Upgrade Plan</span>
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot
|
RouterStateSnapshot
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
import { paths } from '@ghostfolio/client/app-routing.module';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -13,21 +14,17 @@ import { catchError } from 'rxjs/operators';
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthGuard {
|
export class AuthGuard {
|
||||||
private static PUBLIC_PAGE_ROUTES = [
|
private static PUBLIC_PAGE_ROUTES = [
|
||||||
'/about',
|
`/${paths.about}`,
|
||||||
'/about/changelog',
|
|
||||||
'/about/privacy-policy',
|
|
||||||
'/blog',
|
'/blog',
|
||||||
'/de/blog',
|
|
||||||
'/demo',
|
'/demo',
|
||||||
'/en/blog',
|
`/${paths.faq}`,
|
||||||
'/faq',
|
`/${paths.features}`,
|
||||||
'/features',
|
`/${paths.markets}`,
|
||||||
'/markets',
|
|
||||||
'/open',
|
'/open',
|
||||||
'/p',
|
'/p',
|
||||||
'/pricing',
|
`/${paths.pricing}`,
|
||||||
'/register',
|
`/${paths.register}`,
|
||||||
'/resources'
|
`/${paths.resources}`
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -53,7 +50,7 @@ export class AuthGuard {
|
|||||||
this.router.navigate(['/demo']);
|
this.router.navigate(['/demo']);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
} else if (utmSource === 'trusted-web-activity') {
|
} else if (utmSource === 'trusted-web-activity') {
|
||||||
this.router.navigate(['/register']);
|
this.router.navigate(['/' + $localize`register`]);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
} else if (
|
} else if (
|
||||||
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) =>
|
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) =>
|
||||||
|
@ -77,7 +77,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.snackBarRef.onAction().subscribe(() => {
|
this.snackBarRef.onAction().subscribe(() => {
|
||||||
this.router.navigate(['/pricing']);
|
this.router.navigate(['/' + $localize`pricing`]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
|
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { paths } from '@ghostfolio/client/app-routing.module';
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
import { AboutPageComponent } from './about-page.component';
|
import { AboutPageComponent } from './about-page.component';
|
||||||
@ -22,38 +25,27 @@ const routes: Routes = [
|
|||||||
(m) => m.ChangelogPageModule
|
(m) => m.ChangelogPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
...[
|
{
|
||||||
'license',
|
path: paths.license,
|
||||||
/////
|
|
||||||
'licenca',
|
|
||||||
'licence',
|
|
||||||
'licencia',
|
|
||||||
'licentie',
|
|
||||||
'lizenz',
|
|
||||||
'licenza'
|
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./license/license-page.module').then(
|
import('./license/license-page.module').then(
|
||||||
(m) => m.LicensePageModule
|
(m) => m.LicensePageModule
|
||||||
)
|
)
|
||||||
})),
|
},
|
||||||
...[
|
{
|
||||||
'privacy-policy',
|
path: 'oss-friends',
|
||||||
/////
|
loadChildren: () =>
|
||||||
'datenschutzbestimmungen',
|
import('./oss-friends/oss-friends-page.module').then(
|
||||||
'informativa-sulla-privacy',
|
(m) => m.OpenSourceSoftwareFriendsPageModule
|
||||||
'politique-de-confidentialite',
|
)
|
||||||
'politica-de-privacidad',
|
},
|
||||||
'politica-de-privacidade',
|
{
|
||||||
'privacybeleid'
|
path: paths.privacyPolicy,
|
||||||
].map((path) => ({
|
|
||||||
path,
|
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./privacy-policy/privacy-policy-page.module').then(
|
import('./privacy-policy/privacy-policy-page.module').then(
|
||||||
(m) => m.PrivacyPolicyPageModule
|
(m) => m.PrivacyPolicyPageModule
|
||||||
)
|
)
|
||||||
}))
|
}
|
||||||
],
|
],
|
||||||
component: AboutPageComponent,
|
component: AboutPageComponent,
|
||||||
path: '',
|
path: '',
|
||||||
|
@ -44,30 +44,31 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
|||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
|
this.tabs = [
|
||||||
|
{
|
||||||
|
iconName: 'reader-outline',
|
||||||
|
label: $localize`About`,
|
||||||
|
path: ['/' + $localize`about`]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'sparkles-outline',
|
||||||
|
label: $localize`Changelog`,
|
||||||
|
path: ['/' + $localize`about`, 'changelog']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'ribbon-outline',
|
||||||
|
label: $localize`License`,
|
||||||
|
path: ['/' + $localize`about`, $localize`license`]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.tabs = [
|
this.tabs.push({
|
||||||
{
|
iconName: 'shield-checkmark-outline',
|
||||||
iconName: 'reader-outline',
|
label: $localize`Privacy Policy`,
|
||||||
label: $localize`About`,
|
path: ['/' + $localize`about`, $localize`privacy-policy`],
|
||||||
path: ['/about']
|
showCondition: this.hasPermissionForSubscription
|
||||||
},
|
});
|
||||||
{
|
|
||||||
iconName: 'sparkles-outline',
|
|
||||||
label: $localize`Changelog`,
|
|
||||||
path: ['/about', 'changelog']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
iconName: 'ribbon-outline',
|
|
||||||
label: $localize`License`,
|
|
||||||
path: ['/about', 'license']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
iconName: 'shield-checkmark-outline',
|
|
||||||
label: $localize`Privacy Policy`,
|
|
||||||
path: ['/about', 'privacy-policy'],
|
|
||||||
showCondition: this.hasPermissionForSubscription
|
|
||||||
}
|
|
||||||
];
|
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
this.hasMessage =
|
this.hasMessage =
|
||||||
@ -78,6 +79,12 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.tabs.push({
|
||||||
|
iconName: 'happy-outline',
|
||||||
|
label: 'OSS Friends',
|
||||||
|
path: ['/' + $localize`about`, 'oss-friends']
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,14 +11,9 @@
|
|||||||
padding-bottom: constant(safe-area-inset-bottom);
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
gf-about-page,
|
|
||||||
gf-changelog-page,
|
|
||||||
gf-privacy-policy-page {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-mdc-tab-link-container {
|
.mat-mdc-tab-link-container {
|
||||||
|
--mat-tab-header-active-focus-indicator-color: transparent;
|
||||||
|
--mat-tab-header-active-hover-indicator-color: transparent;
|
||||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||||
|
|
||||||
.mat-mdc-tab-link {
|
.mat-mdc-tab-link {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Changelog</h1>
|
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Changelog</h1>
|
||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
|
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>License</h1>
|
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>License</h1>
|
||||||
<div>
|
<div>
|
||||||
<markdown [src]="'../assets/LICENSE'"></markdown>
|
<markdown [src]="'../assets/LICENSE'"></markdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { OpenSourceSoftwareFriendsPageComponent } from './oss-friends-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
component: OpenSourceSoftwareFriendsPageComponent,
|
||||||
|
path: '',
|
||||||
|
title: 'OSS Friends'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
exports: [RouterModule],
|
||||||
|
imports: [RouterModule.forChild(routes)]
|
||||||
|
})
|
||||||
|
export class OpenSourceSoftwareFriendsPageRoutingModule {}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
const ossFriends = require('../../../../assets/oss-friends.json');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
selector: 'gf-oss-friends-page',
|
||||||
|
styleUrls: ['./oss-friends-page.scss'],
|
||||||
|
templateUrl: './oss-friends-page.html'
|
||||||
|
})
|
||||||
|
export class OpenSourceSoftwareFriendsPageComponent implements OnDestroy {
|
||||||
|
public ossFriends = ossFriends.data;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="mb-5 row">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="h3 mb-4 text-center">
|
||||||
|
<span class="d-none d-sm-block"
|
||||||
|
><ng-container i18n>Our</ng-container> OSS Friends</span
|
||||||
|
>
|
||||||
|
<small class="text-muted" i18n
|
||||||
|
>Discover other exciting Open Source Software projects</small
|
||||||
|
>
|
||||||
|
</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div
|
||||||
|
*ngFor="let ossFriend of ossFriends"
|
||||||
|
class="col-xs-12 col-md-4 mb-3"
|
||||||
|
>
|
||||||
|
<a target="_blank" [href]="ossFriend.href">
|
||||||
|
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title class="h4">{{ ossFriend.name }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content class="flex-grow-1">
|
||||||
|
<p>{{ ossFriend.description }}</p>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions class="justify-content-end">
|
||||||
|
<a mat-button target="_blank" [href]="ossFriend.href">
|
||||||
|
<span
|
||||||
|
><ng-container i18n>Visit</ng-container> {{ ossFriend.name
|
||||||
|
}}</span
|
||||||
|
><ion-icon
|
||||||
|
class="ml-1"
|
||||||
|
name="arrow-forward-outline"
|
||||||
|
></ion-icon>
|
||||||
|
</a>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,19 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
|
||||||
|
import { OpenSourceSoftwareFriendsPageRoutingModule } from './oss-friends-page-routing.module';
|
||||||
|
import { OpenSourceSoftwareFriendsPageComponent } from './oss-friends-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [OpenSourceSoftwareFriendsPageComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
OpenSourceSoftwareFriendsPageRoutingModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class OpenSourceSoftwareFriendsPageModule {}
|
@ -0,0 +1,9 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-mdc-card {
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--gf-theme-primary-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,8 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionForStatistics: boolean;
|
public hasPermissionForStatistics: boolean;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public isLoggedIn: boolean;
|
public isLoggedIn: boolean;
|
||||||
|
public routerLinkFaq = ['/' + $localize`faq`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
public user: User;
|
public user: User;
|
||||||
public version = environment.version;
|
public version = environment.version;
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-none d-sm-block mb-3 text-center">About Ghostfolio</h3>
|
<h1 class="d-none d-sm-block h3 mb-4 text-center">
|
||||||
|
<ng-container i18n>About Ghostfolio</ng-container>
|
||||||
|
</h1>
|
||||||
<div class="about-container">
|
<div class="about-container">
|
||||||
<p>
|
<p>
|
||||||
Ghostfolio is a lightweight wealth management application for
|
Ghostfolio is a lightweight wealth management application for
|
||||||
@ -46,7 +48,7 @@
|
|||||||
<p>
|
<p>
|
||||||
If you encounter a bug or would like to suggest an improvement or a
|
If you encounter a bug or would like to suggest an improvement or a
|
||||||
new
|
new
|
||||||
<a [routerLink]="['/features']">feature</a>, please join the
|
<a [routerLink]="routerLinkFeatures">feature</a>, please join the
|
||||||
Ghostfolio
|
Ghostfolio
|
||||||
<a
|
<a
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
@ -139,7 +141,7 @@
|
|||||||
class="py-4 w-100"
|
class="py-4 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[routerLink]="['/faq']"
|
[routerLink]="routerLinkFaq"
|
||||||
>Frequently Asked Questions (FAQ)</a
|
>Frequently Asked Questions (FAQ)</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Privacy Policy</h3>
|
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Privacy Policy</h1>
|
||||||
<markdown [src]="'../assets/privacy-policy.md'"></markdown>
|
<markdown [src]="'../assets/privacy-policy.md'"></markdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Accounts</h3>
|
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Accounts</h1>
|
||||||
<div class="accounts">
|
<div class="accounts">
|
||||||
<gf-accounts-table
|
<gf-accounts-table
|
||||||
[accounts]="accounts"
|
[accounts]="accounts"
|
||||||
|
@ -11,16 +11,9 @@
|
|||||||
padding-bottom: constant(safe-area-inset-bottom);
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
gf-admin-jobs,
|
|
||||||
gf-admin-market-data,
|
|
||||||
gf-admin-overview,
|
|
||||||
gf-admin-settings,
|
|
||||||
gf-admin-users {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-mdc-tab-link-container {
|
.mat-mdc-tab-link-container {
|
||||||
|
--mat-tab-header-active-focus-indicator-color: transparent;
|
||||||
|
--mat-tab-header-active-hover-indicator-color: transparent;
|
||||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||||
|
|
||||||
.mat-mdc-tab-link {
|
.mat-mdc-tab-link {
|
||||||
|
@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './hallo-ghostfolio-page.html'
|
templateUrl: './hallo-ghostfolio-page.html'
|
||||||
})
|
})
|
||||||
export class HalloGhostfolioPageComponent {}
|
export class HalloGhostfolioPageComponent {
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
public routerLinkResources = ['/' + $localize`resources`];
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
Aufgrund der steigenden Inflation und den Negativzinsen befasse ich
|
Aufgrund der steigenden Inflation und den Negativzinsen befasse ich
|
||||||
mich seit einiger Zeit, wie ich mein Vermögen möglichst
|
mich seit einiger Zeit, wie ich mein Vermögen möglichst
|
||||||
diversifiziert anlegen kann. Konkret verfolge ich eine
|
diversifiziert anlegen kann. Konkret verfolge ich eine
|
||||||
<a [routerLink]="['/resources']">Buy and Hold Strategie</a> mit
|
<a [routerLink]="routerLinkResources">Buy and Hold Strategie</a> mit
|
||||||
Investitionen in verschiedene Anlageklassen verteilt auf
|
Investitionen in verschiedene Anlageklassen verteilt auf
|
||||||
unterschiedliche Plattformen. Deshalb suchte ich nach einer App, die
|
unterschiedliche Plattformen. Deshalb suchte ich nach einer App, die
|
||||||
mein Portfolio ganzheitlich zusammenfasst. Bei meiner
|
mein Portfolio ganzheitlich zusammenfasst. Bei meiner
|
||||||
@ -119,7 +119,7 @@
|
|||||||
Anlagestrategie? Ich freue mich über alle, die Ghostfolio
|
Anlagestrategie? Ich freue mich über alle, die Ghostfolio
|
||||||
ausprobieren. Bist du überzeugt vom Potential der Software? Jede
|
ausprobieren. Bist du überzeugt vom Potential der Software? Jede
|
||||||
Unterstützung für Ghostfolio ist willkommen. Sei es mit einer
|
Unterstützung für Ghostfolio ist willkommen. Sei es mit einer
|
||||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a>
|
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
|
||||||
Subscription zur Finanzierung des Hostings, einem positiven Rating
|
Subscription zur Finanzierung des Hostings, einem positiven Rating
|
||||||
im
|
im
|
||||||
<a
|
<a
|
||||||
|
@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './hello-ghostfolio-page.html'
|
templateUrl: './hello-ghostfolio-page.html'
|
||||||
})
|
})
|
||||||
export class HelloGhostfolioPageComponent {}
|
export class HelloGhostfolioPageComponent {
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
public routerLinkResources = ['/' + $localize`resources`];
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
Due to rising inflation and negative interest rates, I have been
|
Due to rising inflation and negative interest rates, I have been
|
||||||
looking for some time at how I can invest my assets in the most
|
looking for some time at how I can invest my assets in the most
|
||||||
diversified way possible. Specifically, I follow a
|
diversified way possible. Specifically, I follow a
|
||||||
<a [routerLink]="['/resources']">buy and hold strategy</a> with
|
<a [routerLink]="routerLinkResources">buy and hold strategy</a> with
|
||||||
investments in different asset classes spread across different
|
investments in different asset classes spread across different
|
||||||
platforms. Therefore, I was looking for an app that would
|
platforms. Therefore, I was looking for an app that would
|
||||||
holistically aggregate my portfolio. During my research on the
|
holistically aggregate my portfolio. During my research on the
|
||||||
@ -115,7 +115,7 @@
|
|||||||
strategy? I'm happy for everyone who tries Ghostfolio. Are you
|
strategy? I'm happy for everyone who tries Ghostfolio. Are you
|
||||||
convinced of its potential? Any support for Ghostfolio is welcome.
|
convinced of its potential? Any support for Ghostfolio is welcome.
|
||||||
Be it with a
|
Be it with a
|
||||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a>
|
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
|
||||||
Subscription to finance the hosting, a positive rating in the
|
Subscription to finance the hosting, a positive rating in the
|
||||||
<a
|
<a
|
||||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||||
|
@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './first-months-in-open-source-page.html'
|
templateUrl: './first-months-in-open-source-page.html'
|
||||||
})
|
})
|
||||||
export class FirstMonthsInOpenSourcePageComponent {}
|
export class FirstMonthsInOpenSourcePageComponent {
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
}
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
My personal goal is to reach break-even with the Saas offering (<a
|
My personal goal is to reach break-even with the Saas offering (<a
|
||||||
[routerLink]="['/pricing']"
|
[routerLink]="routerLinkPricing"
|
||||||
>Ghostfolio Premium</a
|
>Ghostfolio Premium</a
|
||||||
>) and regularly report about the progress and my learnings on this
|
>) and regularly report about the progress and my learnings on this
|
||||||
exciting journey.
|
exciting journey.
|
||||||
|
@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
|
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
|
||||||
})
|
})
|
||||||
export class HowDoIGetMyFinancesInOrderPageComponent {}
|
export class HowDoIGetMyFinancesInOrderPageComponent {
|
||||||
|
public routerLinkResources = ['/' + $localize`resources`];
|
||||||
|
}
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<p>
|
<p>
|
||||||
Before you can think of
|
Before you can think of
|
||||||
<a [routerLink]="['/resources']">long-term investing</a>, you have
|
<a [routerLink]="routerLinkResources">long-term investing</a>, you
|
||||||
to get your finances in order. Take a look at Peter's journey to see
|
have to get your finances in order. Take a look at Peter's journey
|
||||||
how you can achieve it, too.
|
to see how you can achieve it, too.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Peter enjoys life, but sometimes he overspends a bit. He realizes it
|
Peter enjoys life, but sometimes he overspends a bit. He realizes it
|
||||||
|
@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './500-stars-on-github-page.html'
|
templateUrl: './500-stars-on-github-page.html'
|
||||||
})
|
})
|
||||||
export class FiveHundredStarsOnGitHubPageComponent {}
|
export class FiveHundredStarsOnGitHubPageComponent {
|
||||||
|
public routerLinkMarkets = ['/' + $localize`markets`];
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
}
|
||||||
|
@ -71,10 +71,10 @@
|
|||||||
<h2 class="h4">Break-even Point</h2>
|
<h2 class="h4">Break-even Point</h2>
|
||||||
<p>
|
<p>
|
||||||
Despite the complicated
|
Despite the complicated
|
||||||
<a [routerLink]="['/markets']">economic situation</a> at this time,
|
<a [routerLink]="routerLinkMarkets">economic situation</a> at this
|
||||||
the goal set at the beginning of the year to build a sustainable
|
time, the goal set at the beginning of the year to build a
|
||||||
business and reach break-even with the SaaS offering (<a
|
sustainable business and reach break-even with the SaaS offering (<a
|
||||||
[routerLink]="['/pricing']"
|
[routerLink]="routerLinkPricing"
|
||||||
>Ghostfolio Premium</a
|
>Ghostfolio Premium</a
|
||||||
>) has been achieved. We will continue to leverage the revenue to
|
>) has been achieved. We will continue to leverage the revenue to
|
||||||
further improve the fully managed cloud offering for our paying
|
further improve the fully managed cloud offering for our paying
|
||||||
|
@ -11,5 +11,6 @@ import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
|||||||
templateUrl: './black-friday-2022-page.html'
|
templateUrl: './black-friday-2022-page.html'
|
||||||
})
|
})
|
||||||
export class BlackFriday2022PageComponent {
|
export class BlackFriday2022PageComponent {
|
||||||
public constructor() {}
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
}
|
}
|
||||||
|
@ -35,17 +35,18 @@
|
|||||||
software presents the current assets (stocks, ETFs,
|
software presents the current assets (stocks, ETFs,
|
||||||
cryptocurrencies, commodities etc.) in real time to make solid,
|
cryptocurrencies, commodities etc.) in real time to make solid,
|
||||||
data-driven investment decisions. Check out the numerous
|
data-driven investment decisions. Check out the numerous
|
||||||
<a [routerLink]="['/features']">features</a> to manage your wealth.
|
<a [routerLink]="routerLinkFeatures">features</a> to manage your
|
||||||
|
wealth.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<p>
|
<p>
|
||||||
Snap the limited Black Friday 2022 deal before it’s gone. For
|
Snap the limited Black Friday 2022 deal before it’s gone. For
|
||||||
detailed information on plans and pricing, please visit our
|
detailed information on plans and pricing, please visit our
|
||||||
<a [routerLink]="['/pricing']">pricing page</a>.
|
<a [routerLink]="routerLinkPricing">pricing page</a>.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<a color="primary" mat-flat-button [routerLink]="['/pricing']"
|
<a color="primary" mat-flat-button [routerLink]="routerLinkPricing"
|
||||||
>Get the Deal</a
|
>Get the Deal</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './1000-stars-on-github-page.html'
|
templateUrl: './1000-stars-on-github-page.html'
|
||||||
})
|
})
|
||||||
export class ThousandStarsOnGitHubPageComponent {}
|
export class ThousandStarsOnGitHubPageComponent {
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||||
|
}
|
||||||
|
@ -92,7 +92,7 @@
|
|||||||
<p>
|
<p>
|
||||||
These self-hosting platforms allow users to run applications on
|
These self-hosting platforms allow users to run applications on
|
||||||
their own hardware rather than rely on a
|
their own hardware rather than rely on a
|
||||||
<a [routerLink]="['/pricing']">SaaS offering</a>. As a result,
|
<a [routerLink]="routerLinkPricing">SaaS offering</a>. As a result,
|
||||||
Ghostfolio has become accessible to an even wider range of users who
|
Ghostfolio has become accessible to an even wider range of users who
|
||||||
would like to take control of their wealth management.
|
would like to take control of their wealth management.
|
||||||
</p>
|
</p>
|
||||||
@ -108,9 +108,9 @@
|
|||||||
As the project continues to evolve, we can expect to see even more
|
As the project continues to evolve, we can expect to see even more
|
||||||
exciting developments and innovations around Ghostfolio which guides
|
exciting developments and innovations around Ghostfolio which guides
|
||||||
users through the process of
|
users through the process of
|
||||||
<a [routerLink]="['/features']">tracking their assets</a>, such as
|
<a [routerLink]="routerLinkFeatures">tracking their assets</a>, such
|
||||||
stocks, ETFs, or cryptocurrencies. Especially in the areas of data
|
as stocks, ETFs, or cryptocurrencies. Especially in the areas of
|
||||||
import and portfolio analysis.
|
data import and portfolio analysis.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
We are honored to be a part of this vibrant and growing community,
|
We are honored to be a part of this vibrant and growing community,
|
||||||
|
@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
|
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
|
||||||
})
|
})
|
||||||
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {}
|
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResources = ['/' + $localize`resources`];
|
||||||
|
}
|
||||||
|
@ -44,13 +44,13 @@
|
|||||||
<h2 class="h4">Empowering Buy & Hold Strategies</h2>
|
<h2 class="h4">Empowering Buy & Hold Strategies</h2>
|
||||||
<p>
|
<p>
|
||||||
For those committed to a
|
For those committed to a
|
||||||
<a [routerLink]="['/resources']">buy & hold strategy</a>, Ghostfolio
|
<a [routerLink]="routerLinkResources">buy & hold strategy</a>,
|
||||||
provides an intuitive interface to monitor long-term investments.
|
Ghostfolio provides an intuitive interface to monitor long-term
|
||||||
Users can track performance over time, gaining insights into
|
investments. Users can track performance over time, gaining insights
|
||||||
portfolio growth and stability. With strong visualizations and
|
into portfolio growth and stability. With strong visualizations and
|
||||||
reporting <a [routerLink]="['/features']">features</a>, Ghostfolio
|
reporting <a [routerLink]="routerLinkFeatures">features</a>,
|
||||||
equips users to make well-informed decisions aligned with their
|
Ghostfolio equips users to make well-informed decisions aligned with
|
||||||
long-term investment goals.
|
their long-term investment goals.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
@ -91,7 +91,7 @@
|
|||||||
<h2 class="h4">Driving Financial Independence (FIRE)</h2>
|
<h2 class="h4">Driving Financial Independence (FIRE)</h2>
|
||||||
<p>
|
<p>
|
||||||
Achieving
|
Achieving
|
||||||
<a [routerLink]="['/resources']">financial independence</a>
|
<a [routerLink]="routerLinkResources">financial independence</a>
|
||||||
including early retirement (<a
|
including early retirement (<a
|
||||||
href="../en/blog/2023/07/exploring-the-path-to-fire"
|
href="../en/blog/2023/07/exploring-the-path-to-fire"
|
||||||
>FIRE</a
|
>FIRE</a
|
||||||
|
@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './exploring-the-path-to-fire-page.html'
|
templateUrl: './exploring-the-path-to-fire-page.html'
|
||||||
})
|
})
|
||||||
export class ExploringThePathToFirePageComponent {}
|
export class ExploringThePathToFirePageComponent {
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class="mb-3 text-muted"><small>2023-07-01</small></div>
|
<div class="mb-3 text-muted"><small>2023-07-01</small></div>
|
||||||
<img
|
<img
|
||||||
alt="Exploring the Path to Financial Independence and Retiring Early (FIRE) Teaser"
|
alt="Exploring the Path to Financial Independence and Retiring Early (FIRE) Teaser"
|
||||||
class="border rounded w-100"
|
class="rounded w-100"
|
||||||
src="../assets/images/blog/20230701.jpg"
|
src="../assets/images/blog/20230701.jpg"
|
||||||
title="Exploring the Path to Financial Independence and Retiring Early (FIRE)"
|
title="Exploring the Path to Financial Independence and Retiring Early (FIRE)"
|
||||||
/>
|
/>
|
||||||
@ -135,10 +135,10 @@
|
|||||||
track your investments, and make informed decisions to accelerate
|
track your investments, and make informed decisions to accelerate
|
||||||
your progress towards financial independence. Ghostfolio also
|
your progress towards financial independence. Ghostfolio also
|
||||||
provides a dedicated
|
provides a dedicated
|
||||||
<a [routerLink]="['/features']">FIRE calculator</a>, allowing you to
|
<a [routerLink]="routerLinkFeatures">FIRE calculator</a>, allowing
|
||||||
simulate your customized plan to achieve FIRE. You get the tools to
|
you to simulate your customized plan to achieve FIRE. You get the
|
||||||
optimize your financial journey and confidently strive for a future
|
tools to optimize your financial journey and confidently strive for
|
||||||
that is both personally fulfilling and financially secure.
|
a future that is both personally fulfilling and financially secure.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-4 py-3">
|
<section class="mb-4 py-3">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user