Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
1851ae137f | |||
6f6ff94979 | |||
7f25066f0f | |||
fc795aaa8c | |||
d0112968e8 | |||
522025ffa0 | |||
27bf662281 | |||
93c27277c6 | |||
5e6adfcef5 | |||
ab691bb27a | |||
8fc5676443 | |||
1fe1e2fe0c | |||
921d38a706 | |||
6161d5e77c | |||
369386f976 | |||
41437636b1 | |||
b21884eb66 | |||
1c5437e1fd | |||
58278ba5e6 | |||
921f3e9807 | |||
75ca125a70 | |||
a1fd4e7a38 | |||
0d5a8eb33e | |||
b088df2fa3 | |||
f45d8f616a | |||
d8300502ce | |||
502d51ad29 | |||
bc33e5f147 | |||
48ba8f936b | |||
05ec4cce05 | |||
d74f283707 | |||
0f8bc7db32 | |||
431500f28a | |||
9672de174e | |||
c6aa06b933 | |||
1f46a6b6f3 | |||
1bed940bc0 | |||
f9eb3cc3c5 | |||
2519c3ffb0 | |||
91013d1d10 | |||
6deefb9c43 | |||
d0744e07df | |||
93e1ee3ba7 | |||
dceaa55a6c | |||
8b4d55925d | |||
754b49e50f | |||
6ccbda8169 | |||
b0fb986208 | |||
0b59fc639d | |||
7ddd6f27b5 | |||
c5d56f4b47 | |||
2f2b712999 | |||
c2fd31f5e5 | |||
f2d70f9070 | |||
f41dd9cd8e | |||
7d238b4935 | |||
da6591fca0 | |||
1f9b9e9998 | |||
49c4ea306d | |||
ccb5c664ef | |||
97e165ff69 | |||
45aefb6a45 |
@ -11,6 +11,5 @@ POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
|
||||
|
||||
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
|
||||
ALPHA_VANTAGE_API_KEY=
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
|
||||
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
|
||||
|
@ -9,6 +9,7 @@
|
||||
],
|
||||
"attributeSort": "ASC",
|
||||
"endOfLine": "auto",
|
||||
"plugins": ["prettier-plugin-organize-attributes"],
|
||||
"printWidth": 80,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
|
104
CHANGELOG.md
104
CHANGELOG.md
@ -5,6 +5,106 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.2.0 - 2023-09-17
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced a sidebar navigation on desktop
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the style of the system message
|
||||
- Upgraded _Postgres_ from version `12` to `15` in the `docker-compose` files
|
||||
|
||||
## 2.1.0 - 2023-09-15
|
||||
|
||||
### Added
|
||||
|
||||
- Added support to drop a file in the import activities dialog
|
||||
- Added a timeout to all data source requests
|
||||
|
||||
### Changed
|
||||
|
||||
- Harmonized the style of the user interface for granting and revoking public access to share the portfolio
|
||||
- Removed the account type from the user interface as a preparation to remove it from the `Account` database schema
|
||||
- Improved the logger output of the info service
|
||||
- Harmonized the logger output: `<symbol> (<dataSource>)`
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Italian (`it`)
|
||||
- Improved the language localization for Dutch (`nl`)
|
||||
- Improved the read-only mode
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the timeout in _EOD Historical Data_ requests
|
||||
- Fixed an issue with the portfolio summary caused by the language localization for Dutch (`nl`)
|
||||
|
||||
## 2.0.0 - 2023-09-09
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for the cryptocurrency _CyberConnect_
|
||||
- Added a blog post: _Announcing Ghostfolio 2.0_
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking Change**: Removed the deprecated environment variable `BASE_CURRENCY`
|
||||
- Improved the validation in the activities import
|
||||
- Deactivated _Internet Identity_ as a social login provider for the account registration
|
||||
- Improved the language localization for German (`de`)
|
||||
- Refreshed the cryptocurrencies list
|
||||
- Changed the version in the `docker-compose` files from `3.7` to `3.9`
|
||||
- Upgraded `yahoo-finance2` from version `2.4.4` to `2.5.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the _Yahoo Finance_ data enhancer where countries and sectors have been removed
|
||||
|
||||
## 1.305.0 - 2023-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- Added _Hacker News_ to the _As seen in_ section on the landing page
|
||||
|
||||
### Changed
|
||||
|
||||
- Shortened the page titles
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prisma` from version `4.16.2` to `5.2.0`
|
||||
- Upgraded `replace-in-file` from version `6.3.5` to `7.0.1`
|
||||
- Upgraded `yahoo-finance2` from version `2.4.3` to `2.4.4`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the alignment in the header navigation
|
||||
- Fixed the alignment in the menu of the impersonation mode
|
||||
|
||||
## 1.304.0 - 2023-08-27
|
||||
|
||||
### Added
|
||||
|
||||
- Added health check endpoints for data enhancers
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `Nx` from version `16.7.2` to `16.7.4`
|
||||
- Upgraded `prettier` from version `2.8.4` to `3.0.2`
|
||||
|
||||
## 1.303.0 - 2023-08-23
|
||||
|
||||
### Added
|
||||
|
||||
- Added a blog post: _Ghostfolio joins OSS Friends_
|
||||
|
||||
### Changed
|
||||
|
||||
- Refreshed the cryptocurrencies list
|
||||
- Improved the _OSS Friends_ page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the _Trackinsight_ data enhancer for asset profile data
|
||||
|
||||
## 1.302.0 - 2023-08-20
|
||||
|
||||
### Changed
|
||||
@ -1443,7 +1543,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Set up the language localization for Italiano (`it`)
|
||||
- Set up the language localization for Italian (`it`)
|
||||
- Extended the landing page
|
||||
|
||||
## 1.195.0 - 20.09.2022
|
||||
@ -2866,7 +2966,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Supported the management of additional currencies in the admin control panel
|
||||
- Introduced the system message
|
||||
- Introduced the read only mode
|
||||
- Introduced the read-only mode
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -13,6 +13,8 @@
|
||||
[](#contributing)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
|
||||
New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2)
|
||||
|
||||
</div>
|
||||
|
||||
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
|
||||
@ -136,9 +138,9 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
||||
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
||||
At each start, the container will automatically apply the database schema migrations if needed.
|
||||
|
||||
### Run with _Unraid_ (Community)
|
||||
### Home Server Systems (Community)
|
||||
|
||||
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
|
||||
Ghostfolio is available for various home server systems, including [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class CreateAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountType: AccountType;
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class UpdateAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountType: AccountType;
|
||||
|
||||
|
@ -6,7 +6,12 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d
|
||||
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 { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
PROPERTY_CURRENCIES,
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_IS_USER_SIGNUP_ENABLED
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
@ -23,8 +28,6 @@ import { groupBy } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
@ -34,9 +37,7 @@ export class AdminService {
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public async addAssetProfile({
|
||||
dataSource,
|
||||
@ -80,15 +81,15 @@ export class AdminService {
|
||||
exchangeRates: this.exchangeRateDataService
|
||||
.getCurrencies()
|
||||
.filter((currency) => {
|
||||
return currency !== this.baseCurrency;
|
||||
return currency !== DEFAULT_CURRENCY;
|
||||
})
|
||||
.map((currency) => {
|
||||
return {
|
||||
label1: this.baseCurrency,
|
||||
label1: DEFAULT_CURRENCY,
|
||||
label2: currency,
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
1,
|
||||
this.baseCurrency,
|
||||
DEFAULT_CURRENCY,
|
||||
currency
|
||||
)
|
||||
};
|
||||
@ -306,7 +307,9 @@ export class AdminService {
|
||||
response = await this.propertyService.delete({ key });
|
||||
}
|
||||
|
||||
if (key === PROPERTY_CURRENCIES) {
|
||||
if (key === PROPERTY_IS_READ_ONLY_MODE && value === 'true') {
|
||||
await this.putSetting(PROPERTY_IS_USER_SIGNUP_ENABLED, 'false');
|
||||
} else if (key === PROPERTY_CURRENCIES) {
|
||||
await this.exchangeRateDataService.initialize();
|
||||
}
|
||||
|
||||
|
@ -41,9 +41,8 @@ export class AuthController {
|
||||
@Param('accessToken') accessToken: string
|
||||
): Promise<OAuthResponse> {
|
||||
try {
|
||||
const authToken = await this.authService.validateAnonymousLogin(
|
||||
accessToken
|
||||
);
|
||||
const authToken =
|
||||
await this.authService.validateAnonymousLogin(accessToken);
|
||||
return { authToken };
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
|
@ -55,7 +55,7 @@ export class AuthService {
|
||||
const isUserSignupEnabled =
|
||||
await this.propertyService.isUserSignupEnabled();
|
||||
|
||||
if (!isUserSignupEnabled) {
|
||||
if (!isUserSignupEnabled || true) {
|
||||
throw new Error('Sign up forbidden');
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,10 @@ import {
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { ExchangeRateService } from './exchange-rate.service';
|
||||
import { parseISO } from 'date-fns';
|
||||
|
||||
@Controller('exchange-rate')
|
||||
export class ExchangeRateController {
|
||||
|
@ -26,18 +26,8 @@ export class ExportService {
|
||||
where: { userId }
|
||||
})
|
||||
).map(
|
||||
({
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
id,
|
||||
isExcluded,
|
||||
name,
|
||||
platformId
|
||||
}) => {
|
||||
({ balance, comment, currency, id, isExcluded, name, platformId }) => {
|
||||
return {
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
|
@ -18,6 +18,19 @@ export class HealthController {
|
||||
@Get()
|
||||
public async getHealth() {}
|
||||
|
||||
@Get('data-enhancer/:name')
|
||||
public async getHealthOfDataEnhancer(@Param('name') name: string) {
|
||||
const hasResponse =
|
||||
await this.healthService.hasResponseFromDataEnhancer(name);
|
||||
|
||||
if (hasResponse !== true) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE),
|
||||
StatusCodes.SERVICE_UNAVAILABLE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('data-provider/:dataSource')
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
public async getHealthOfDataProvider(
|
||||
@ -30,9 +43,8 @@ export class HealthController {
|
||||
);
|
||||
}
|
||||
|
||||
const hasResponse = await this.healthService.hasResponseFromDataProvider(
|
||||
dataSource
|
||||
);
|
||||
const hasResponse =
|
||||
await this.healthService.hasResponseFromDataProvider(dataSource);
|
||||
|
||||
if (hasResponse !== true) {
|
||||
throw new HttpException(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@ -7,7 +8,7 @@ import { HealthService } from './health.service';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
imports: [ConfigurationModule, DataProviderModule],
|
||||
imports: [ConfigurationModule, DataEnhancerModule, DataProviderModule],
|
||||
providers: [HealthService]
|
||||
})
|
||||
export class HealthModule {}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
@ -5,9 +6,14 @@ import { DataSource } from '@prisma/client';
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
public constructor(
|
||||
private readonly dataEnhancerService: DataEnhancerService,
|
||||
private readonly dataProviderService: DataProviderService
|
||||
) {}
|
||||
|
||||
public async hasResponseFromDataEnhancer(aName: string) {
|
||||
return this.dataEnhancerService.enhance(aName);
|
||||
}
|
||||
|
||||
public async hasResponseFromDataProvider(aDataSource: DataSource) {
|
||||
return this.dataProviderService.checkQuote(aDataSource);
|
||||
}
|
||||
|
@ -566,7 +566,7 @@ export class ImportService {
|
||||
])
|
||||
)?.[symbol];
|
||||
|
||||
if (!assetProfile) {
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
@ -28,11 +29,11 @@ import { InfoService } from './info.service';
|
||||
signOptions: { expiresIn: '30 days' }
|
||||
}),
|
||||
PlatformModule,
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
RedisCacheModule,
|
||||
SymbolProfileModule,
|
||||
TagModule
|
||||
TagModule,
|
||||
UserModule
|
||||
],
|
||||
providers: [InfoService]
|
||||
})
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT,
|
||||
PROPERTY_BETTER_UPTIME_MONITOR_ID,
|
||||
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
|
||||
PROPERTY_DEMO_USER_ID,
|
||||
@ -44,10 +46,10 @@ export class InfoService {
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly platformService: PlatformService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private readonly tagService: TagService
|
||||
private readonly tagService: TagService,
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
|
||||
public async get(): Promise<InfoItem> {
|
||||
@ -139,18 +141,13 @@ export class InfoService {
|
||||
subscriptions,
|
||||
systemMessage,
|
||||
tags,
|
||||
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
||||
baseCurrency: DEFAULT_CURRENCY,
|
||||
currencies: this.exchangeRateDataService.getCurrencies()
|
||||
};
|
||||
}
|
||||
|
||||
private async countActiveUsers(aDays: number) {
|
||||
return await this.prismaService.user.count({
|
||||
orderBy: {
|
||||
Analytics: {
|
||||
updatedAt: 'desc'
|
||||
}
|
||||
},
|
||||
return this.userService.count({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
@ -172,16 +169,24 @@ export class InfoService {
|
||||
|
||||
private async countDockerHubPulls(): Promise<number> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { pull_count } = await got(
|
||||
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
|
||||
{
|
||||
headers: { 'User-Agent': 'request' }
|
||||
headers: { 'User-Agent': 'request' },
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
return pull_count;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - DockerHub');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -189,7 +194,16 @@ export class InfoService {
|
||||
|
||||
private async countGitHubContributors(): Promise<number> {
|
||||
try {
|
||||
const { body } = await got('https://github.com/ghostfolio/ghostfolio');
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { body } = await got('https://github.com/ghostfolio/ghostfolio', {
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
@ -199,7 +213,7 @@ export class InfoService {
|
||||
).text()
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - GitHub');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -207,26 +221,31 @@ export class InfoService {
|
||||
|
||||
private async countGitHubStargazers(): Promise<number> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { stargazers_count } = await got(
|
||||
`https://api.github.com/repos/ghostfolio/ghostfolio`,
|
||||
{
|
||||
headers: { 'User-Agent': 'request' }
|
||||
headers: { 'User-Agent': 'request' },
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
return stargazers_count;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - GitHub');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async countNewUsers(aDays: number) {
|
||||
return await this.prismaService.user.count({
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
return this.userService.count({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
@ -317,11 +336,10 @@ export class InfoService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const stripeConfig = (await this.prismaService.property.findUnique({
|
||||
where: { key: PROPERTY_STRIPE_CONFIG }
|
||||
})) ?? { value: '{}' };
|
||||
|
||||
return JSON.parse(stripeConfig.value);
|
||||
return (
|
||||
((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
private async getUptime(): Promise<number> {
|
||||
@ -331,24 +349,31 @@ export class InfoService {
|
||||
PROPERTY_BETTER_UPTIME_MONITOR_ID
|
||||
)) as string;
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { data } = await got(
|
||||
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
||||
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
||||
subDays(new Date(), 90),
|
||||
DATE_FORMAT
|
||||
)}&to${format(new Date(), DATE_FORMAT)}`,
|
||||
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.configurationService.get(
|
||||
'BETTER_UPTIME_API_KEY'
|
||||
)}`
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
return data.attributes.availability / 100;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - Better Stack');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { HttpException, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
@ -41,10 +42,18 @@ export class LogoService {
|
||||
}
|
||||
|
||||
private getBuffer(aUrl: string) {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
return got(
|
||||
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
|
||||
{
|
||||
headers: { 'User-Agent': 'request' }
|
||||
headers: { 'User-Agent': 'request' },
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).buffer();
|
||||
}
|
||||
|
@ -105,7 +105,6 @@ describe('CurrentRateService', () => {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
marketDataService = new MarketDataService(null);
|
||||
|
@ -784,7 +784,7 @@ export class PortfolioCalculator {
|
||||
);
|
||||
} else if (!currentPosition.quantity.eq(0)) {
|
||||
Logger.warn(
|
||||
`Missing historical market data for symbol ${currentPosition.symbol}`,
|
||||
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
|
||||
'PortfolioCalculator'
|
||||
);
|
||||
hasErrors = true;
|
||||
|
@ -10,7 +10,10 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
|
||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
HEADER_KEY_IMPERSONATION
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioDetails,
|
||||
PortfolioDividends,
|
||||
@ -47,8 +50,6 @@ import { PortfolioService } from './portfolio.service';
|
||||
|
||||
@Controller('portfolio')
|
||||
export class PortfolioController {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly accessService: AccessService,
|
||||
private readonly apiService: ApiService,
|
||||
@ -57,9 +58,7 @@ export class PortfolioController {
|
||||
private readonly portfolioService: PortfolioService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly userService: UserService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
@Get('details')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@ -442,8 +441,7 @@ export class PortfolioController {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||
portfolioPosition.currency,
|
||||
this.request.user?.Settings?.settings.baseCurrency ??
|
||||
this.baseCurrency
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY
|
||||
);
|
||||
})
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
@ -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 { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
EMERGENCY_FUND_TAG_ID,
|
||||
MAX_CHART_ITEMS,
|
||||
UNKNOWN_KEY
|
||||
@ -90,11 +90,8 @@ const europeMarkets = require('../../assets/countries/europe-markets.json');
|
||||
|
||||
@Injectable()
|
||||
export class PortfolioService {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly currentRateService: CurrentRateService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
@ -104,9 +101,7 @@ export class PortfolioService {
|
||||
private readonly rulesService: RulesService,
|
||||
private readonly symbolProfileService: SymbolProfileService,
|
||||
private readonly userService: UserService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public async getAccounts({
|
||||
filters,
|
||||
@ -470,9 +465,8 @@ export class PortfolioService {
|
||||
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
||||
);
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate
|
||||
);
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(startDate);
|
||||
|
||||
const cashDetails = await this.accountService.getCashDetails({
|
||||
filters,
|
||||
@ -810,9 +804,8 @@ export class PortfolioService {
|
||||
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
||||
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
portfolioStart
|
||||
);
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
||||
|
||||
const position = currentPositions.positions.find(
|
||||
(item) => item.symbol === aSymbol
|
||||
@ -1046,9 +1039,8 @@ export class PortfolioService {
|
||||
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate
|
||||
);
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(startDate);
|
||||
|
||||
const positions = currentPositions.positions.filter(
|
||||
(item) => !item.quantity.eq(0)
|
||||
@ -1238,9 +1230,8 @@ export class PortfolioService {
|
||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
portfolioStart
|
||||
);
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
||||
|
||||
const positions = currentPositions.positions.filter(
|
||||
(item) => !item.quantity.eq(0)
|
||||
@ -1772,7 +1763,7 @@ export class PortfolioService {
|
||||
portfolioOrders: PortfolioOrder[];
|
||||
}> {
|
||||
const userCurrency =
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? this.baseCurrency;
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
|
||||
|
||||
const orders = await this.orderService.getOrders({
|
||||
filters,
|
||||
@ -1994,7 +1985,7 @@ export class PortfolioService {
|
||||
return (
|
||||
aUser.Settings?.settings.baseCurrency ??
|
||||
this.request.user?.Settings?.settings.baseCurrency ??
|
||||
this.baseCurrency
|
||||
DEFAULT_CURRENCY
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -93,9 +93,8 @@ export class SubscriptionService {
|
||||
|
||||
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
|
||||
try {
|
||||
const session = await this.stripe.checkout.sessions.retrieve(
|
||||
aCheckoutSessionId
|
||||
);
|
||||
const session =
|
||||
await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId);
|
||||
|
||||
await this.createSubscription({
|
||||
price: session.amount_total / 100,
|
||||
|
@ -15,13 +15,13 @@ import {
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import { isDate, isEmpty } from 'lodash';
|
||||
|
||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||
import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||
import { SymbolService } from './symbol.service';
|
||||
import { parseISO } from 'date-fns';
|
||||
|
||||
@Controller('symbol')
|
||||
export class SymbolController {
|
||||
|
@ -19,22 +19,22 @@ import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Role, User } from '@prisma/client';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { sortBy } from 'lodash';
|
||||
import { sortBy, without } from 'lodash';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly tagService: TagService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
) {}
|
||||
|
||||
public async count(args?: Prisma.UserCountArgs) {
|
||||
return this.prismaService.user.count(args);
|
||||
}
|
||||
|
||||
public async getUser(
|
||||
@ -188,6 +188,11 @@ export class UserService {
|
||||
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||
}
|
||||
|
||||
currentPermissions = without(
|
||||
currentPermissions,
|
||||
permissions.createAccess
|
||||
);
|
||||
|
||||
// Reset benchmark
|
||||
user.Settings.settings.benchmark = undefined;
|
||||
}
|
||||
@ -267,7 +272,7 @@ export class UserService {
|
||||
...data,
|
||||
Account: {
|
||||
create: {
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
isDefault: true,
|
||||
name: 'Default Account'
|
||||
}
|
||||
@ -275,7 +280,7 @@ export class UserService {
|
||||
Settings: {
|
||||
create: {
|
||||
settings: {
|
||||
currency: this.baseCurrency
|
||||
currency: DEFAULT_CURRENCY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,9 @@
|
||||
"3FT": "ThreeFold Token",
|
||||
"3ULL": "3ULL Coin",
|
||||
"3XD": "3DChain",
|
||||
"420CHAN": "420chan",
|
||||
"4ART": "4ART Coin",
|
||||
"4CHAN": "4Chan",
|
||||
"4JNET": "4JNET",
|
||||
"77G": "GraphenTech",
|
||||
"7E": "7ELEVEN",
|
||||
@ -60,6 +62,7 @@
|
||||
"8BT": "8 Circuit Studios",
|
||||
"8PAY": "8Pay",
|
||||
"8X8": "8X8 Protocol",
|
||||
"9GAG": "9GAG",
|
||||
"A5T": "Alpha5",
|
||||
"AAA": "Moon Rabbit",
|
||||
"AAB": "AAX Token",
|
||||
@ -101,6 +104,7 @@
|
||||
"ACN": "AvonCoin",
|
||||
"ACOIN": "ACoin",
|
||||
"ACP": "Anarchists Prime",
|
||||
"ACQ": "Acquire.Fi",
|
||||
"ACS": "Access Protocol",
|
||||
"ACT": "Achain",
|
||||
"ACTIN": "Actinium",
|
||||
@ -180,7 +184,7 @@
|
||||
"AGX": "Agricoin",
|
||||
"AHOO": "Ahoolee",
|
||||
"AHT": "AhaToken",
|
||||
"AI": "Multiverse",
|
||||
"AI": "AiDoge",
|
||||
"AIB": "AdvancedInternetBlock",
|
||||
"AIBB": "AiBB",
|
||||
"AIBK": "AIB Utility Token",
|
||||
@ -213,6 +217,7 @@
|
||||
"AKA": "Akroma",
|
||||
"AKITA": "Akita Inu",
|
||||
"AKN": "Akoin",
|
||||
"AKNC": "Aave KNC v1",
|
||||
"AKRO": "Akropolis",
|
||||
"AKT": "Akash Network",
|
||||
"AKTIO": "AKTIO Coin",
|
||||
@ -237,12 +242,14 @@
|
||||
"ALIC": "AliCoin",
|
||||
"ALICE": "My Neighbor Alice",
|
||||
"ALIEN": "AlienCoin",
|
||||
"ALINK": "Aave LINK v1",
|
||||
"ALIS": "ALISmedia",
|
||||
"ALITA": "Alita Network",
|
||||
"ALIX": "AlinX",
|
||||
"ALKI": "Alkimi",
|
||||
"ALLBI": "ALL BEST ICO",
|
||||
"ALLEY": "NFT Alley",
|
||||
"ALLIN": "All in",
|
||||
"ALN": "Aluna",
|
||||
"ALOHA": "Aloha",
|
||||
"ALP": "Alphacon",
|
||||
@ -410,12 +417,14 @@
|
||||
"ARIX": "Arix",
|
||||
"ARK": "ARK",
|
||||
"ARKER": "Arker",
|
||||
"ARKM": "Arkham",
|
||||
"ARKN": "Ark Rivals",
|
||||
"ARM": "Armory Coin",
|
||||
"ARMOR": "ARMOR",
|
||||
"ARMR": "ARMR",
|
||||
"ARMS": "2Acoin",
|
||||
"ARNA": "ARNA Panacea",
|
||||
"ARNM": "Arenum",
|
||||
"ARNO": "ARNO",
|
||||
"ARNX": "Aeron",
|
||||
"ARNXM": "Armor NXM",
|
||||
@ -472,6 +481,7 @@
|
||||
"ASTO": "Altered State Token",
|
||||
"ASTON": "Aston",
|
||||
"ASTR": "Astar",
|
||||
"ASTRAFER": "Astrafer",
|
||||
"ASTRAL": "Astral",
|
||||
"ASTRO": "AstroSwap",
|
||||
"ASTROC": "Astroport Classic",
|
||||
@ -531,6 +541,7 @@
|
||||
"AURY": "Aurory",
|
||||
"AUSCM": "Auric Network",
|
||||
"AUSD": "Appeal dollar",
|
||||
"AUSDC": "Aave USDC v1",
|
||||
"AUT": "Autoria",
|
||||
"AUTHORSHIP": "Authorship",
|
||||
"AUTO": "Auto",
|
||||
@ -612,6 +623,7 @@
|
||||
"BACK": "DollarBack",
|
||||
"BACOIN": "BACoin",
|
||||
"BACON": "BaconDAO (BACON)",
|
||||
"BAD": "Bad Idea AI",
|
||||
"BADGER": "Badger DAO",
|
||||
"BAG": "BondAppetit",
|
||||
"BAGS": "Basis Gold Share",
|
||||
@ -662,6 +674,7 @@
|
||||
"BBCT": "TraDove B2BCoin",
|
||||
"BBDT": "BBD Token",
|
||||
"BBF": "Bubblefong",
|
||||
"BBFT": "Block Busters Tech Token",
|
||||
"BBG": "BigBang",
|
||||
"BBGC": "BigBang Game",
|
||||
"BBI": "BelugaPay",
|
||||
@ -725,6 +738,7 @@
|
||||
"BDX": "Beldex",
|
||||
"BDY": "Buddy DAO",
|
||||
"BEACH": "BeachCoin",
|
||||
"BEAI": "BeNFT Solutions",
|
||||
"BEAM": "Beam",
|
||||
"BEAN": "BeanCash",
|
||||
"BEAST": "CryptoBeast",
|
||||
@ -806,6 +820,7 @@
|
||||
"BIDR": "Binance IDR Stable Coin",
|
||||
"BIFI": "Beefy.Finance",
|
||||
"BIFIF": "BiFi",
|
||||
"BIG": "Big Eyes",
|
||||
"BIGHAN": "BighanCoin",
|
||||
"BIGSB": "BigShortBets",
|
||||
"BIGUP": "BigUp",
|
||||
@ -1090,6 +1105,7 @@
|
||||
"BRNK": "Brank",
|
||||
"BRNX": "Bronix",
|
||||
"BRO": "Bitradio",
|
||||
"BROCK": "Bitrock",
|
||||
"BRONZ": "BitBronze",
|
||||
"BRT": "Bikerush",
|
||||
"BRTR": "Barter",
|
||||
@ -1226,7 +1242,7 @@
|
||||
"BULL": "Bullieverse",
|
||||
"BULLC": "BuySell",
|
||||
"BULLION": "BullionFX",
|
||||
"BULLS": "BullshitCoin",
|
||||
"BULLS": "Bull Coin",
|
||||
"BULLSH": "Bullshit Inu",
|
||||
"BUMN": "BUMooN",
|
||||
"BUMP": "Bumper",
|
||||
@ -1277,6 +1293,7 @@
|
||||
"BZKY": "Bizkey",
|
||||
"BZL": "BZLCoin",
|
||||
"BZNT": "Bezant",
|
||||
"BZR": "Bazaars",
|
||||
"BZRX": "bZx Protocol",
|
||||
"BZX": "Bitcoin Zero",
|
||||
"BZZ": "Swarmv",
|
||||
@ -1319,8 +1336,10 @@
|
||||
"CAP": "BottleCaps",
|
||||
"CAPD": "Capdax",
|
||||
"CAPP": "Cappasity",
|
||||
"CAPRICOIN": "CapriCoin",
|
||||
"CAPS": "Ternoa",
|
||||
"CAPT": "Bitcoin Captain",
|
||||
"CAPTAINPLANET": "Captain Planet",
|
||||
"CAR": "CarBlock",
|
||||
"CARAT": "Carats Token",
|
||||
"CARBON": "Carboncoin",
|
||||
@ -1478,6 +1497,7 @@
|
||||
"CHECKR": "CheckerChain",
|
||||
"CHECOIN": "CheCoin",
|
||||
"CHEDDA": "Chedda",
|
||||
"CHEEL": "Cheelee",
|
||||
"CHEESE": "CHEESE",
|
||||
"CHEESUS": "Cheesus",
|
||||
"CHEQ": "CHEQD Network",
|
||||
@ -1520,7 +1540,8 @@
|
||||
"CHX": "Own",
|
||||
"CHY": "Concern Poverty Chain",
|
||||
"CHZ": "Chiliz",
|
||||
"CIC": "CIChain",
|
||||
"CIC": "Crazy Internet Coin",
|
||||
"CICHAIN": "CIChain",
|
||||
"CIF": "Crypto Improvement Fund",
|
||||
"CIM": "COINCOME",
|
||||
"CIN": "CinderCoin",
|
||||
@ -1630,7 +1651,6 @@
|
||||
"COB": "Cobinhood",
|
||||
"COC": "Coin of the champions",
|
||||
"COCK": "Shibacock",
|
||||
"COCOS": "COCOS BCX",
|
||||
"CODEO": "Codeo Token",
|
||||
"CODEX": "CODEX Finance",
|
||||
"CODI": "Codi Finance",
|
||||
@ -1659,7 +1679,7 @@
|
||||
"COLX": "ColossusCoinXT",
|
||||
"COM": "Coliseum",
|
||||
"COMB": "Combo",
|
||||
"COMBO": "Furucombo",
|
||||
"COMBO": "COMBO",
|
||||
"COMFI": "CompliFi",
|
||||
"COMM": "Community Coin",
|
||||
"COMMUNITYCOIN": "Community Coin",
|
||||
@ -1672,7 +1692,6 @@
|
||||
"CONI": "CoinBene",
|
||||
"CONS": "ConSpiracy Coin",
|
||||
"CONSENTIUM": "Consentium",
|
||||
"CONT": "Contentos",
|
||||
"CONUN": "CONUN",
|
||||
"CONV": "Convergence",
|
||||
"COOK": "Cook",
|
||||
@ -1683,17 +1702,19 @@
|
||||
"COPS": "Cops Finance",
|
||||
"COR": "Corion",
|
||||
"CORAL": "CoralPay",
|
||||
"CORE": "Coreum",
|
||||
"CORE": "Core",
|
||||
"COREDAO": "coreDAO",
|
||||
"COREG": "Core Group Asset",
|
||||
"COREUM": "Coreum",
|
||||
"CORGI": "Corgi Inu",
|
||||
"CORN": "CORN",
|
||||
"CORX": "CorionX",
|
||||
"COS": "COS",
|
||||
"COS": "Contentos",
|
||||
"COSHI": "CoShi Inu",
|
||||
"COSM": "CosmoChain",
|
||||
"COSMIC": "CosmicSwap",
|
||||
"COSP": "Cosplay Token",
|
||||
"COSS": "COS",
|
||||
"COSX": "Cosmecoin",
|
||||
"COT": "CoTrader",
|
||||
"COTI": "COTI",
|
||||
@ -1729,7 +1750,7 @@
|
||||
"CPOOL": "Clearpool",
|
||||
"CPROP": "CPROP",
|
||||
"CPRX": "Crypto Perx",
|
||||
"CPS": "CapriCoin",
|
||||
"CPS": "Cryptostone",
|
||||
"CPT": "Cryptaur",
|
||||
"CPU": "CPUcoin",
|
||||
"CPX": "Apex Token",
|
||||
@ -1796,6 +1817,7 @@
|
||||
"CRTS": "Cratos",
|
||||
"CRU": "Crust Network",
|
||||
"CRV": "Curve DAO Token",
|
||||
"CRVUSD": "crvUSD",
|
||||
"CRW": "Crown Coin",
|
||||
"CRWD": "CRWD Network",
|
||||
"CRWNY": "Crowny Token",
|
||||
@ -1843,7 +1865,7 @@
|
||||
"CTLX": "Cash Telex",
|
||||
"CTN": "Continuum Finance",
|
||||
"CTO": "Crypto",
|
||||
"CTP": "Captain Planet",
|
||||
"CTP": "Ctomorrow Platform",
|
||||
"CTPL": "Cultiplan",
|
||||
"CTPT": "Contents Protocol",
|
||||
"CTR": "Creator Platform",
|
||||
@ -2007,6 +2029,7 @@
|
||||
"DBC": "DeepBrain Chain",
|
||||
"DBCCOIN": "Datablockchain",
|
||||
"DBD": "Day By Day",
|
||||
"DBEAR": "DBear Coin",
|
||||
"DBET": "Decent.bet",
|
||||
"DBIC": "DubaiCoin",
|
||||
"DBIX": "DubaiCoin",
|
||||
@ -2058,6 +2081,7 @@
|
||||
"DEEP": "DeepCloud AI",
|
||||
"DEEPG": "Deep Gold",
|
||||
"DEEX": "DEEX",
|
||||
"DEEZ": "DEEZ NUTS",
|
||||
"DEFI": "Defi",
|
||||
"DEFI5": "DEFI Top 5 Tokens Index",
|
||||
"DEFIL": "DeFIL",
|
||||
@ -2162,11 +2186,12 @@
|
||||
"DIEM": "Facebook Diem",
|
||||
"DIESEL": "Diesel",
|
||||
"DIFX": "Digital Financial Exchange",
|
||||
"DIG": "Dignity",
|
||||
"DIG": "DIEGO",
|
||||
"DIGG": "DIGG",
|
||||
"DIGIC": "DigiCube",
|
||||
"DIGIF": "DigiFel",
|
||||
"DIGITAL": "Digital Reserve Currency",
|
||||
"DIGNITY": "Dignity",
|
||||
"DIGS": "Diggits",
|
||||
"DIKO": "Arkadiko",
|
||||
"DILI": "D Community",
|
||||
@ -2246,6 +2271,7 @@
|
||||
"DOGBOSS": "Dog Boss",
|
||||
"DOGDEFI": "DogDeFiCoin",
|
||||
"DOGE": "Dogecoin",
|
||||
"DOGE20": "Doge 2.0",
|
||||
"DOGEBNB": "DogeBNB",
|
||||
"DOGEC": "DogeCash",
|
||||
"DOGECEO": "Doge CEO",
|
||||
@ -2539,7 +2565,7 @@
|
||||
"ELONGT": "Elon GOAT",
|
||||
"ELONONE": "AstroElon",
|
||||
"ELP": "Ellerium",
|
||||
"ELS": "Elysium",
|
||||
"ELS": "Ethlas",
|
||||
"ELT": "Element Black",
|
||||
"ELTC2": "eLTC",
|
||||
"ELTCOIN": "ELTCOIN",
|
||||
@ -2548,6 +2574,7 @@
|
||||
"ELVN": "11Minutes",
|
||||
"ELX": "Energy Ledger",
|
||||
"ELY": "Elysian",
|
||||
"ELYSIUM": "Elysium",
|
||||
"EM": "Eminer",
|
||||
"EMANATE": "EMANATE",
|
||||
"EMAR": "EmaratCoin",
|
||||
@ -2559,6 +2586,7 @@
|
||||
"EMC2": "Einsteinium",
|
||||
"EMD": "Emerald",
|
||||
"EMIGR": "EmiratesGoldCoin",
|
||||
"EML": "EML Protocol",
|
||||
"EMN.CUR": "Eastman Chemical",
|
||||
"EMON": "Ethermon",
|
||||
"EMOT": "Sentigraph.io",
|
||||
@ -2692,6 +2720,7 @@
|
||||
"ETHD": "Ethereum Dark",
|
||||
"ETHER": "Etherparty",
|
||||
"ETHERDELTA": "EtherDelta",
|
||||
"ETHERKING": "Ether Kingdoms Token",
|
||||
"ETHERNITY": "Ethernity Chain",
|
||||
"ETHF": "EthereumFair",
|
||||
"ETHIX": "EthicHub",
|
||||
@ -2709,6 +2738,7 @@
|
||||
"ETHSHIB": "Eth Shiba",
|
||||
"ETHV": "Ethverse",
|
||||
"ETHW": "Ethereum PoW",
|
||||
"ETHX": "Stader ETHx",
|
||||
"ETHY": "Ethereum Yield",
|
||||
"ETI": "EtherInc",
|
||||
"ETK": "Energi Token",
|
||||
@ -2722,7 +2752,7 @@
|
||||
"ETR": "Electric Token",
|
||||
"ETRNT": "Eternal Trusts",
|
||||
"ETS": "ETH Share",
|
||||
"ETSC": "Ether star blockchain",
|
||||
"ETSC": "Ether star blockchain",
|
||||
"ETT": "EncryptoTel",
|
||||
"ETY": "Ethereum Cloud",
|
||||
"ETZ": "EtherZero",
|
||||
@ -2773,6 +2803,7 @@
|
||||
"EXB": "ExaByte (EXB)",
|
||||
"EXC": "Eximchain",
|
||||
"EXCC": "ExchangeCoin",
|
||||
"EXCHANGEN": "ExchangeN",
|
||||
"EXCL": "Exclusive Coin",
|
||||
"EXE": "ExeCoin",
|
||||
"EXFI": "Flare Finance",
|
||||
@ -2781,7 +2812,7 @@
|
||||
"EXLT": "ExtraLovers",
|
||||
"EXM": "EXMO Coin",
|
||||
"EXMR": "EXMR FDN",
|
||||
"EXN": "ExchangeN",
|
||||
"EXN": "Exeno",
|
||||
"EXO": "Exosis",
|
||||
"EXP": "Expanse",
|
||||
"EXRD": "Radix",
|
||||
@ -2814,6 +2845,7 @@
|
||||
"FAIR": "FairCoin",
|
||||
"FAIRC": "Faireum Token",
|
||||
"FAIRG": "FairGame",
|
||||
"FAKE": "FAKE COIN",
|
||||
"FAKT": "Medifakt",
|
||||
"FALCONS": "Falcon Swaps",
|
||||
"FAME": "Fame MMA",
|
||||
@ -2860,6 +2892,7 @@
|
||||
"FDO": "Firdaos",
|
||||
"FDR": "French Digital Reserve",
|
||||
"FDT": "Frutti Dino",
|
||||
"FDUSD": "First Digital USD",
|
||||
"FDX": "fidentiaX",
|
||||
"FDZ": "Friendz",
|
||||
"FEAR": "Fear",
|
||||
@ -2870,6 +2903,7 @@
|
||||
"FEN": "First Ever NFT",
|
||||
"FENOMY": "Fenomy",
|
||||
"FER": "Ferro",
|
||||
"FERC": "FairERC20",
|
||||
"FERMA": "Ferma",
|
||||
"FESS": "Fesschain",
|
||||
"FET": "Fetch.AI",
|
||||
@ -2931,7 +2965,7 @@
|
||||
"FLASH": "Flashstake",
|
||||
"FLASHC": "FLASH coin",
|
||||
"FLC": "FlowChainCoin",
|
||||
"FLD": "FLUID",
|
||||
"FLD": "FluidAI",
|
||||
"FLDC": "Folding Coin",
|
||||
"FLDT": "FairyLand",
|
||||
"FLETA": "FLETA",
|
||||
@ -3091,6 +3125,7 @@
|
||||
"FUEL": "Jetfuel Finance",
|
||||
"FUJIN": "Fujinto",
|
||||
"FUKU": "Furukuru",
|
||||
"FUMO": "Alien Milady Fumo",
|
||||
"FUN": "FUN Token",
|
||||
"FUNC": "FunCoin",
|
||||
"FUND": "Unification",
|
||||
@ -3101,6 +3136,7 @@
|
||||
"FUNDZ": "FundFantasy",
|
||||
"FUNK": "Cypherfunks Coin",
|
||||
"FUR": "Furio",
|
||||
"FURU": "Furucombo",
|
||||
"FURY": "Engines of Fury",
|
||||
"FUS": "Fus",
|
||||
"FUSE": "Fuse Network Token",
|
||||
@ -3118,6 +3154,7 @@
|
||||
"FXP": "FXPay",
|
||||
"FXS": "Frax Share",
|
||||
"FXT": "FuzeX",
|
||||
"FXY": "Floxypay",
|
||||
"FYN": "Affyn",
|
||||
"FYP": "FlypMe",
|
||||
"FYZ": "Fyooz",
|
||||
@ -3172,6 +3209,7 @@
|
||||
"GAT": "GATCOIN",
|
||||
"GATE": "GATENet",
|
||||
"GATEWAY": "Gateway Protocol",
|
||||
"GAYPEPE": "Gay Pepe",
|
||||
"GAZE": "GazeTV",
|
||||
"GB": "GoldBlocks",
|
||||
"GBA": "Geeba",
|
||||
@ -3222,6 +3260,7 @@
|
||||
"GEMZ": "Gemz Social",
|
||||
"GEN": "DAOstack",
|
||||
"GENE": "Genopets",
|
||||
"GENIE": "The Genie",
|
||||
"GENIX": "Genix",
|
||||
"GENS": "Genshiro",
|
||||
"GENSTAKE": "Genstake",
|
||||
@ -3261,6 +3300,7 @@
|
||||
"GHCOLD": "Galaxy Heroes Coin",
|
||||
"GHD": "Giftedhands",
|
||||
"GHNY": "Grizzly Honey",
|
||||
"GHO": "GHO",
|
||||
"GHOST": "GhostbyMcAfee",
|
||||
"GHOSTCOIN": "GhostCoin",
|
||||
"GHOSTM": "GhostMarket",
|
||||
@ -3274,6 +3314,7 @@
|
||||
"GIFT": "GiftNet",
|
||||
"GIG": "GigaCoin",
|
||||
"GIGA": "GigaSwap",
|
||||
"GIGX": "GigXCoin",
|
||||
"GIM": "Gimli",
|
||||
"GIMMER": "Gimmer",
|
||||
"GIN": "GINcoin",
|
||||
@ -3385,6 +3426,7 @@
|
||||
"GOVT": "The Government Network",
|
||||
"GOZ": "Göztepe S.K. Fan Token",
|
||||
"GP": "Wizards And Dragons",
|
||||
"GPBP": "Genius Playboy Billionaire Philanthropist",
|
||||
"GPKR": "Gold Poker",
|
||||
"GPL": "Gold Pressed Latinum",
|
||||
"GPPT": "Pluto Project Coin",
|
||||
@ -3501,7 +3543,8 @@
|
||||
"HALF": "0.5X Long Bitcoin Token",
|
||||
"HALFSHIT": "0.5X Long Shitcoin Index Token",
|
||||
"HALLO": "Halloween Coin",
|
||||
"HALO": "Halo Platform",
|
||||
"HALO": "Halo Coin",
|
||||
"HALOPLATFORM": "Halo Platform",
|
||||
"HAM": "Hamster",
|
||||
"HAMS": "HamsterCoin",
|
||||
"HANA": "Hanacoin",
|
||||
@ -3598,6 +3641,7 @@
|
||||
"HILL": "President Clinton",
|
||||
"HINA": "Hina Inu",
|
||||
"HINT": "Hintchain",
|
||||
"HIPPO": "HIPPO",
|
||||
"HIRE": "HireMatch",
|
||||
"HIT": "HitChain",
|
||||
"HITBTC": "HitBTC Token",
|
||||
@ -3634,6 +3678,7 @@
|
||||
"HNTR": "Hunter",
|
||||
"HNY": "Honey",
|
||||
"HNZO": "Hanzo Inu",
|
||||
"HOBO": "HOBO THE BEAR",
|
||||
"HOD": "HoDooi.com",
|
||||
"HODL": "HOdlcoin",
|
||||
"HOGE": "Hoge Finance",
|
||||
@ -3839,7 +3884,7 @@
|
||||
"IMPCN": "Brain Space",
|
||||
"IMPER": "Impermax",
|
||||
"IMPS": "Impulse Coin",
|
||||
"IMPT": "Ether Kingdoms Token",
|
||||
"IMPT": "IMPT",
|
||||
"IMPULSE": "IMPULSE by FDR",
|
||||
"IMS": "Independent Money System",
|
||||
"IMST": "Imsmart",
|
||||
@ -4001,6 +4046,7 @@
|
||||
"JAM": "Tune.Fm",
|
||||
"JANE": "JaneCoin",
|
||||
"JAR": "Jarvis+",
|
||||
"JARED": "Jared From Subway",
|
||||
"JASMY": "JasmyCoin",
|
||||
"JBS": "JumBucks Coin",
|
||||
"JBX": "Juicebox",
|
||||
@ -4163,9 +4209,10 @@
|
||||
"KIN": "Kin",
|
||||
"KIND": "Kind Ads",
|
||||
"KINE": "Kine Protocol",
|
||||
"KING": "King Finance",
|
||||
"KING": "KING",
|
||||
"KING93": "King93",
|
||||
"KINGDOMQUEST": "Kingdom Quest",
|
||||
"KINGF": "King Finance",
|
||||
"KINGSHIB": "King Shiba",
|
||||
"KINGSWAP": "KingSwap",
|
||||
"KINT": "Kintsugi",
|
||||
@ -4175,6 +4222,7 @@
|
||||
"KISC": "Kaiser",
|
||||
"KISHIMOTO": "Kishimoto Inu",
|
||||
"KISHU": "Kishu Inu",
|
||||
"KITA": "KITA INU",
|
||||
"KITSU": "Kitsune Inu",
|
||||
"KITTY": "Kitty Inu",
|
||||
"KKO": "Kineko",
|
||||
@ -4267,10 +4315,12 @@
|
||||
"KUBO": "KUBO",
|
||||
"KUBOS": "KubosCoin",
|
||||
"KUE": "Kuende",
|
||||
"KUJI": "Kujira",
|
||||
"KUMA": "Kuma Inu",
|
||||
"KUNCI": "Kunci Coin",
|
||||
"KUR": "Kuro",
|
||||
"KURT": "Kurrent",
|
||||
"KUSA": "Kusa Inu",
|
||||
"KUSD": "Kowala",
|
||||
"KUSH": "KushCoin",
|
||||
"KUV": "Kuverit",
|
||||
@ -4280,6 +4330,7 @@
|
||||
"KVT": "Kinesis Velocity Token",
|
||||
"KWATT": "4New",
|
||||
"KWD": "KIWI DEFI",
|
||||
"KWENTA": "Kwenta",
|
||||
"KWH": "KWHCoin",
|
||||
"KWIK": "KwikSwap",
|
||||
"KWS": "Knight War Spirits",
|
||||
@ -4299,7 +4350,9 @@
|
||||
"LABX": "Stakinglab",
|
||||
"LACCOIN": "LocalAgro",
|
||||
"LACE": "Lovelace World",
|
||||
"LADYS": "Milady Meme Coin",
|
||||
"LAEEB": "LaEeb",
|
||||
"LAELAPS": "Laelaps",
|
||||
"LAIKA": "Laika Protocol",
|
||||
"LALA": "LaLa World",
|
||||
"LAMB": "Lambda",
|
||||
@ -4455,13 +4508,14 @@
|
||||
"LLAND": "Lyfe Land",
|
||||
"LLG": "Loligo",
|
||||
"LLION": "Lydian Lion",
|
||||
"LM": "LM Token",
|
||||
"LM": "LeisureMeta",
|
||||
"LMAO": "LMAO Finance",
|
||||
"LMC": "LomoCoin",
|
||||
"LMCH": "Latamcash",
|
||||
"LMCSWAP": "LimoCoin SWAP",
|
||||
"LMR": "Lumerin",
|
||||
"LMT": "Lympo Market Token",
|
||||
"LMTOKEN": "LM Token",
|
||||
"LMXC": "LimonX",
|
||||
"LMY": "Lunch Money",
|
||||
"LN": "LINK",
|
||||
@ -4530,6 +4584,7 @@
|
||||
"LRG": "Largo Coin",
|
||||
"LRN": "Loopring [NEO]",
|
||||
"LSD": "LightSpeedCoin",
|
||||
"LSETH": "Liquid Staked ETH",
|
||||
"LSK": "Lisk",
|
||||
"LSP": "Lumenswap",
|
||||
"LSS": "Lossless",
|
||||
@ -4626,6 +4681,7 @@
|
||||
"MAEP": "Maester Protocol",
|
||||
"MAG": "Magnet",
|
||||
"MAGIC": "Magic",
|
||||
"MAGICF": "MagicFox",
|
||||
"MAHA": "MahaDAO",
|
||||
"MAI": "Mindsync",
|
||||
"MAID": "MaidSafe Coin",
|
||||
@ -4639,6 +4695,7 @@
|
||||
"MANDOX": "MandoX",
|
||||
"MANGA": "Manga Token",
|
||||
"MANNA": "Manna",
|
||||
"MANTLE": "Mantle",
|
||||
"MAP": "MAP Protocol",
|
||||
"MAPC": "MapCoin",
|
||||
"MAPE": "Mecha Morphing",
|
||||
@ -4672,6 +4729,7 @@
|
||||
"MATIC": "Polygon",
|
||||
"MATPAD": "MaticPad",
|
||||
"MATTER": "AntiMatter",
|
||||
"MAV": "Maverick Protocol",
|
||||
"MAX": "MaxCoin",
|
||||
"MAXR": "Max Revive",
|
||||
"MAY": "Theresa May Coin",
|
||||
@ -4776,6 +4834,7 @@
|
||||
"MESA": "MetaVisa",
|
||||
"MESG": "MESG",
|
||||
"MESH": "MeshBox",
|
||||
"MESSI": "MESSI COIN",
|
||||
"MET": "Metronome",
|
||||
"META": "Metadium",
|
||||
"METAC": "Metacoin",
|
||||
@ -4881,6 +4940,7 @@
|
||||
"MIODIO": "MIODIOCOIN",
|
||||
"MIOTA": "IOTA",
|
||||
"MIR": "Mirror Protocol",
|
||||
"MIRACLE": "MIRACLE",
|
||||
"MIRC": "MIR COIN",
|
||||
"MIS": "Mithril Share",
|
||||
"MISA": "Sangkara",
|
||||
@ -4938,7 +4998,6 @@
|
||||
"MNRB": "MoneyRebel",
|
||||
"MNS": "Monnos",
|
||||
"MNST": "MoonStarter",
|
||||
"MNT": "microNFT",
|
||||
"MNTC": "Manet Coin",
|
||||
"MNTG": "Monetas",
|
||||
"MNTL": "AssetMantle",
|
||||
@ -4967,6 +5026,7 @@
|
||||
"MOF": "Molecular Future (TRC20)",
|
||||
"MOFI": "MobiFi",
|
||||
"MOFOLD": "Molecular Future (ERC20)",
|
||||
"MOG": "Mog Coin",
|
||||
"MOGU": "Mogu",
|
||||
"MOGX": "Mogu",
|
||||
"MOI": "MyOwnItem",
|
||||
@ -4989,9 +5049,11 @@
|
||||
"MONEYIMT": "MoneyToken",
|
||||
"MONF": "Monfter",
|
||||
"MONG": "MongCoin",
|
||||
"MONG20": "Mongoose 2.0",
|
||||
"MONI": "Monsta Infinite",
|
||||
"MONK": "Monkey Project",
|
||||
"MONKEY": "Monkey",
|
||||
"MONKEYS": "Monkeys Token",
|
||||
"MONO": "MonoX",
|
||||
"MONONOKEINU": "Mononoke Inu",
|
||||
"MONS": "Monsters Clan",
|
||||
@ -5011,11 +5073,13 @@
|
||||
"MOONSHOT": "Moonshot",
|
||||
"MOOO": "Hashtagger",
|
||||
"MOOV": "dotmoovs",
|
||||
"MOOX": "Moox Protocol",
|
||||
"MOPS": "Mops",
|
||||
"MORA": "Meliora",
|
||||
"MORE": "More Coin",
|
||||
"MOS": "MOS Coin",
|
||||
"MOT": "Olympus Labs",
|
||||
"MOTG": "MetaOctagon",
|
||||
"MOTI": "Motion",
|
||||
"MOTO": "Motocoin",
|
||||
"MOV": "MovieCoin",
|
||||
@ -5076,6 +5140,7 @@
|
||||
"MSWAP": "MoneySwap",
|
||||
"MT": "MyToken",
|
||||
"MTA": "Meta",
|
||||
"MTB": "MetaBridge",
|
||||
"MTBC": "Metabolic",
|
||||
"MTC": "MEDICAL TOKEN CURRENCY",
|
||||
"MTCMN": "MTC Mesh",
|
||||
@ -5108,6 +5173,7 @@
|
||||
"MUE": "MonetaryUnit",
|
||||
"MULTI": "Multichain",
|
||||
"MULTIBOT": "Multibot",
|
||||
"MULTIV": "Multiverse",
|
||||
"MUN": "MUNcoin",
|
||||
"MUNCH": "Munch Token",
|
||||
"MUSD": "mStable USD",
|
||||
@ -5648,6 +5714,7 @@
|
||||
"OZP": "OZAPHYRE",
|
||||
"P202": "Project 202",
|
||||
"P2PS": "P2P Solutions Foundation",
|
||||
"PAAL": "PAAL AI",
|
||||
"PAC": "PAC Protocol",
|
||||
"PACOCA": "Pacoca",
|
||||
"PAD": "NearPad",
|
||||
@ -5736,6 +5803,7 @@
|
||||
"PEARL": "Pearl Finance",
|
||||
"PEC": "PeaceCoin",
|
||||
"PEEL": "Meta Apes",
|
||||
"PEEPA": "Peepa",
|
||||
"PEEPS": "The People’s Coin",
|
||||
"PEG": "PegNet",
|
||||
"PEGS": "PegShares",
|
||||
@ -5748,6 +5816,7 @@
|
||||
"PEOPLE": "ConstitutionDAO",
|
||||
"PEOS": "pEOS",
|
||||
"PEPE": "Pepe",
|
||||
"PEPE20": "Pepe 2.0",
|
||||
"PEPECASH": "Pepe Cash",
|
||||
"PEPPER": "Pepper Token",
|
||||
"PEPS": "PEPS Coin",
|
||||
@ -5822,6 +5891,7 @@
|
||||
"PINK": "PinkCoin",
|
||||
"PINKX": "PantherCoin",
|
||||
"PINMO": "Pinmo",
|
||||
"PINO": "Pinocchu",
|
||||
"PINU": "Piccolo Inu",
|
||||
"PIO": "Pioneershares",
|
||||
"PIPI": "Pippi Finance",
|
||||
@ -5885,6 +5955,7 @@
|
||||
"PLS": "Pulsechain",
|
||||
"PLSD": "PulseDogecoin",
|
||||
"PLSPAD": "PulsePad",
|
||||
"PLSX": "PulseX",
|
||||
"PLT": "Poollotto.finance",
|
||||
"PLTC": "PlatonCoin",
|
||||
"PLTX": "PlutusX",
|
||||
@ -5911,7 +5982,6 @@
|
||||
"PNK": "Kleros",
|
||||
"PNL": "True PNL",
|
||||
"PNODE": "Pinknode",
|
||||
"PNP": "LogisticsX",
|
||||
"PNT": "pNetwork Token",
|
||||
"PNX": "PhantomX",
|
||||
"PNY": "Peony Coin",
|
||||
@ -5927,6 +5997,7 @@
|
||||
"POINTS": "Cryptsy Points",
|
||||
"POK": "Pokmonsters",
|
||||
"POKEM": "Pokemonio",
|
||||
"POKEMON": "Pokemon",
|
||||
"POKER": "PokerCoin",
|
||||
"POKT": "Pocket Network",
|
||||
"POL": "Pool-X",
|
||||
@ -6010,6 +6081,7 @@
|
||||
"PRIME": "Echelon Prime",
|
||||
"PRIMECHAIN": "PrimeChain",
|
||||
"PRINT": "Printer.Finance",
|
||||
"PRINTERIUM": "Printerium",
|
||||
"PRINTS": "FingerprintsDAO",
|
||||
"PRISM": "Prism",
|
||||
"PRIX": "Privatix",
|
||||
@ -6033,7 +6105,7 @@
|
||||
"PROTON": "Proton",
|
||||
"PROUD": "PROUD Money",
|
||||
"PROXI": "PROXI",
|
||||
"PRP": "Papyrus",
|
||||
"PRP": "Pepe Prime",
|
||||
"PRPS": "Purpose",
|
||||
"PRPT": "Purple Token",
|
||||
"PRQ": "PARSIQ",
|
||||
@ -6042,7 +6114,7 @@
|
||||
"PRTG": "Pre-Retogeum",
|
||||
"PRV": "PrivacySwap",
|
||||
"PRVS": "Previse",
|
||||
"PRX": "Printerium",
|
||||
"PRX": "Parex",
|
||||
"PRXY": "Proxy",
|
||||
"PRY": "PRIMARY",
|
||||
"PSB": "Planet Sandbox",
|
||||
@ -6120,6 +6192,7 @@
|
||||
"PYRAM": "Pyram Token",
|
||||
"PYRK": "Pyrk",
|
||||
"PYT": "Payther",
|
||||
"PYUSD": "PayPal USD",
|
||||
"PZM": "Prizm",
|
||||
"Q1S": "Quantum1Net",
|
||||
"Q2C": "QubitCoin",
|
||||
@ -6178,6 +6251,7 @@
|
||||
"QUA": "Quantum Tech",
|
||||
"QUACK": "Rich Quack",
|
||||
"QUAM": "Quam Network",
|
||||
"QUANT": "Quant Finance",
|
||||
"QUARASHI": "Quarashi Network",
|
||||
"QUARTZ": "Sandclock",
|
||||
"QUASA": "Quasacoin",
|
||||
@ -6201,7 +6275,7 @@
|
||||
"RAC": "RAcoin",
|
||||
"RACA": "Radio Caca",
|
||||
"RACEFI": "RaceFi",
|
||||
"RAD": "Radicle",
|
||||
"RAD": "Radworks",
|
||||
"RADAR": "DappRadar",
|
||||
"RADI": "RadicalCoin",
|
||||
"RADIO": "RadioShack",
|
||||
@ -6220,7 +6294,7 @@
|
||||
"RAM": "Ramifi Protocol",
|
||||
"RAMP": "RAMP",
|
||||
"RANKER": "RankerDao",
|
||||
"RAP": "Rapture",
|
||||
"RAP": "Philosoraptor",
|
||||
"RAPDOGE": "RapDoge",
|
||||
"RARE": "SuperRare",
|
||||
"RARI": "Rarible",
|
||||
@ -6277,6 +6351,7 @@
|
||||
"REA": "Realisto",
|
||||
"REAL": "RealLink",
|
||||
"REALM": "Realm",
|
||||
"REALMS": "Realms of Ethernity",
|
||||
"REALPLATFORM": "REAL",
|
||||
"REALY": "Realy Metaverse",
|
||||
"REAP": "ReapChain",
|
||||
@ -6287,6 +6362,7 @@
|
||||
"RED": "RED TOKEN",
|
||||
"REDC": "RedCab",
|
||||
"REDCO": "Redcoin",
|
||||
"REDDIT": "Reddit",
|
||||
"REDI": "REDi",
|
||||
"REDLANG": "RED",
|
||||
"REDLC": "Redlight Chain",
|
||||
@ -6324,7 +6400,7 @@
|
||||
"REST": "Restore",
|
||||
"RET": "RealTract",
|
||||
"RETAIL": "Retail.Global",
|
||||
"RETH": "Realms of Ethernity",
|
||||
"RETH": "Rocket Pool ETH",
|
||||
"RETH2": "rETH2",
|
||||
"RETIRE": "Retire Token",
|
||||
"REU": "REUCOIN",
|
||||
@ -6351,6 +6427,7 @@
|
||||
"RGP": "Rigel Protocol",
|
||||
"RGT": "Rari Governance Token",
|
||||
"RHEA": "Rhea",
|
||||
"RHINO": "RHINO",
|
||||
"RHOC": "RChain",
|
||||
"RHP": "Rhypton Club",
|
||||
"RIC": "Riecoin",
|
||||
@ -6490,6 +6567,7 @@
|
||||
"RWE": "Real-World Evidence",
|
||||
"RWN": "Rowan Token",
|
||||
"RWS": "Robonomics Web Services",
|
||||
"RXD": "Radiant",
|
||||
"RXT": "RIMAUNANGIS",
|
||||
"RYC": "RoyalCoin",
|
||||
"RYCN": "RoyalCoin 2.0",
|
||||
@ -6564,6 +6642,7 @@
|
||||
"SBTC": "Super Bitcoin",
|
||||
"SC": "Siacoin",
|
||||
"SCA": "SiaClassic",
|
||||
"SCAM": "Scam Coin",
|
||||
"SCAP": "SafeCapital",
|
||||
"SCAR": "Velhalla",
|
||||
"SCASH": "SpaceCash",
|
||||
@ -6624,6 +6703,7 @@
|
||||
"SEER": "SEER",
|
||||
"SEI": "Sei",
|
||||
"SEL": "SelenCoin",
|
||||
"SELF": "SELFCrypto",
|
||||
"SEM": "Semux",
|
||||
"SEN": "Sentaro",
|
||||
"SENATE": "SENATE",
|
||||
@ -6665,6 +6745,7 @@
|
||||
"SGE": "Society of Galactic Exploration",
|
||||
"SGLY": "Singularity",
|
||||
"SGN": "Signals Network",
|
||||
"SGO": "SafuuGO",
|
||||
"SGOLD": "SpaceGold",
|
||||
"SGP": "SGPay",
|
||||
"SGR": "Sogur Currency",
|
||||
@ -6684,6 +6765,7 @@
|
||||
"SHEESH": "Sheesh it is bussin bussin",
|
||||
"SHEESHA": "Sheesha Finance",
|
||||
"SHELL": "Shell Token",
|
||||
"SHERA": "Shera Tokens",
|
||||
"SHFL": "SHUFFLE!",
|
||||
"SHFT": "Shyft Network",
|
||||
"SHI": "Shirtum",
|
||||
@ -6719,6 +6801,8 @@
|
||||
"SHR": "ShareToken",
|
||||
"SHREK": "ShrekCoin",
|
||||
"SHROOM": "Shroom.Finance",
|
||||
"SHROOMFOX": "Magic Shroom",
|
||||
"SHS": "SHEESH",
|
||||
"SHX": "Stronghold Token",
|
||||
"SI": "Siren",
|
||||
"SIB": "SibCoin",
|
||||
@ -7018,9 +7102,11 @@
|
||||
"STEN": "Steneum Coin",
|
||||
"STEP": "Step Finance",
|
||||
"STEPH": "Step Hero",
|
||||
"STEPR": "Step",
|
||||
"STEPS": "Steps",
|
||||
"STERLINGCOIN": "SterlingCoin",
|
||||
"STETH": "Staked Ether",
|
||||
"STEWIE": "Stewie Coin",
|
||||
"STEX": "STEX",
|
||||
"STF": "Structure Finance",
|
||||
"STFX": "STFX",
|
||||
@ -7055,7 +7141,7 @@
|
||||
"STR": "Sourceless",
|
||||
"STRAKS": "Straks",
|
||||
"STRAX": "Stratis",
|
||||
"STRAY": "Animal Token",
|
||||
"STRAY": "Stray Dog",
|
||||
"STREAM": "STREAMIT COIN",
|
||||
"STRIP": "Stripto",
|
||||
"STRK": "Strike",
|
||||
@ -7361,6 +7447,7 @@
|
||||
"TOM": "TOM Finance",
|
||||
"TOMAHAWKCOIN": "Tomahawkcoin",
|
||||
"TOMB": "Tomb",
|
||||
"TOMI": "tomiNet",
|
||||
"TOMO": "TomoChain",
|
||||
"TOMOE": "TomoChain ERC20",
|
||||
"TOMS": "TomTomCoin",
|
||||
@ -7385,6 +7472,7 @@
|
||||
"TOTM": "Totem",
|
||||
"TOWER": "Tower",
|
||||
"TOWN": "Town Star",
|
||||
"TOX": "INTOverse",
|
||||
"TOZ": "Tozex",
|
||||
"TP": "Token Swap",
|
||||
"TPAD": "TrustPad",
|
||||
@ -7600,6 +7688,7 @@
|
||||
"UNITY": "SuperNET",
|
||||
"UNIVRS": "Universe",
|
||||
"UNIX": "UniX",
|
||||
"UNLEASH": "UnleashClub",
|
||||
"UNN": "UNION Protocol Governance Token",
|
||||
"UNO": "Unobtanium",
|
||||
"UNORE": "UnoRe",
|
||||
@ -7673,6 +7762,7 @@
|
||||
"UTT": "United Traders Token",
|
||||
"UTU": "UTU Protocol",
|
||||
"UUU": "U Network",
|
||||
"UWU": "uwu",
|
||||
"UZUMAKI": "Uzumaki Inu",
|
||||
"VAB": "Vabble",
|
||||
"VADER": "Vader Protocol",
|
||||
@ -7695,6 +7785,7 @@
|
||||
"VCF": "Valencia CF Fan Token",
|
||||
"VCG": "VCGamers",
|
||||
"VCK": "28VCK",
|
||||
"VCORE": "VCORE",
|
||||
"VDG": "VeriDocGlobal",
|
||||
"VDL": "Vidulum",
|
||||
"VDO": "VidioCoin",
|
||||
@ -7710,6 +7801,7 @@
|
||||
"VEIL": "VEIL",
|
||||
"VELA": "Vela Token",
|
||||
"VELO": "Velo",
|
||||
"VELOD": "Velodrome Finance",
|
||||
"VELOX": "Velox",
|
||||
"VELOXPROJECT": "Velox",
|
||||
"VEMP": "vEmpire DDAO",
|
||||
@ -7782,6 +7874,7 @@
|
||||
"VNT": "VNT Chain",
|
||||
"VNTW": "Value Network Token",
|
||||
"VNX": "VisionX",
|
||||
"VNXAU": "VNX Gold",
|
||||
"VNXLU": "VNX Exchange",
|
||||
"VOCO": "Provoco",
|
||||
"VODKA": "Vodka Token",
|
||||
@ -7902,7 +7995,8 @@
|
||||
"WEC": "Whole Earth Coin",
|
||||
"WEGEN": "WeGen Platform",
|
||||
"WELD": "Weld",
|
||||
"WELL": "Well",
|
||||
"WELL": "Moonwell",
|
||||
"WELLTOKEN": "Well",
|
||||
"WELT": "Fabwelt",
|
||||
"WELUPS": "Welups Blockchain",
|
||||
"WEMIX": "WEMIX",
|
||||
@ -7958,6 +8052,7 @@
|
||||
"WIX": "Wixlar",
|
||||
"WIZ": "WIZ Protocol",
|
||||
"WKD": "Wakanda Inu",
|
||||
"WLD": "Worldcoin",
|
||||
"WLF": "Wolfs Group",
|
||||
"WLITI": "wLITI",
|
||||
"WLK": "Wolk",
|
||||
@ -7983,6 +8078,7 @@
|
||||
"WNZ": "Winerz",
|
||||
"WOA": "Wrapped Origin Axie",
|
||||
"WOD": "World of Defish",
|
||||
"WOID": "WORLD ID",
|
||||
"WOJ": "Wojak Finance",
|
||||
"WOLF": "Insanity Coin",
|
||||
"WOLFILAND": "Wolfiland",
|
||||
@ -8000,6 +8096,7 @@
|
||||
"WOOFY": "Woofy",
|
||||
"WOOL": "Wolf Game Wool",
|
||||
"WOONK": "Woonkly",
|
||||
"WOOO": "wooonen",
|
||||
"WOOP": "Woonkly Power",
|
||||
"WOP": "WorldPay",
|
||||
"WORLD": "World Token",
|
||||
@ -8010,6 +8107,7 @@
|
||||
"WOZX": "Efforce",
|
||||
"WPC": "WePiggy Coin",
|
||||
"WPE": "OPES (Wrapped PE)",
|
||||
"WPLS": "Wrapped Pulse",
|
||||
"WPP": "Green Energy Token",
|
||||
"WPR": "WePower",
|
||||
"WQT": "Work Quest",
|
||||
@ -8049,6 +8147,7 @@
|
||||
"WZEC": "Wrapped Zcash",
|
||||
"WZENIQ": "Wrapped Zeniq (ETH)",
|
||||
"WZRD": "Wizardia",
|
||||
"X": "AI-X",
|
||||
"X2": "X2Coin",
|
||||
"X2Y2": "X2Y2",
|
||||
"X42": "X42 Protocol",
|
||||
@ -8096,7 +8195,7 @@
|
||||
"XCI": "Cannabis Industry Coin",
|
||||
"XCLR": "ClearCoin",
|
||||
"XCM": "CoinMetro",
|
||||
"XCN": "Chain",
|
||||
"XCN": "Onyxcoin",
|
||||
"XCO": "XCoin",
|
||||
"XCONSOL": "X-Consoles",
|
||||
"XCP": "CounterParty",
|
||||
@ -8365,6 +8464,7 @@
|
||||
"YUANG": "Yuang Coin",
|
||||
"YUCJ": "Yu Coin",
|
||||
"YUCT": "Yucreat",
|
||||
"YUDI": "Yudi",
|
||||
"YUM": "Yumerium",
|
||||
"YUMMY": "Yummy",
|
||||
"YUP": "Crowdholding",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"CYBER24781": "CyberConnect",
|
||||
"LUNA1": "Terra",
|
||||
"LUNA2": "Terra",
|
||||
"SGB1": "Songbird",
|
||||
|
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -50,6 +50,110 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -142,6 +246,14 @@
|
||||
<loc>https://ghostfol.io/en/blog/2023/07/exploring-the-path-to-fire</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/08/ghostfolio-joins-oss-friends</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/09/ghostfolio-2</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -438,6 +550,110 @@
|
||||
<loc>https://ghostfol.io/it/risorse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -447,7 +663,111 @@
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/kenmerken</loc>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
@ -489,7 +809,11 @@
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
||||
<loc>https://ghostfol.io/nl/veelgestelde-vragen</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pt</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
|
@ -55,7 +55,6 @@ async function bootstrap() {
|
||||
|
||||
app.use(HtmlTemplateMiddleware);
|
||||
|
||||
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
|
||||
const HOST = configService.get<string>('HOST') || '0.0.0.0';
|
||||
const PORT = configService.get<number>('PORT') || 3333;
|
||||
|
||||
@ -63,15 +62,6 @@ async function bootstrap() {
|
||||
logLogo();
|
||||
Logger.log(`Listening at http://${HOST}:${PORT}`);
|
||||
Logger.log('');
|
||||
|
||||
if (BASE_CURRENCY) {
|
||||
Logger.warn(
|
||||
`The environment variable "BASE_CURRENCY" is deprecated and will be removed in Ghostfolio 2.0.`
|
||||
);
|
||||
Logger.warn(
|
||||
'Please use the currency converter in the activity dialog instead.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,14 @@ const locales = {
|
||||
'/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}`
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
|
||||
import { DEFAULT_CURRENCY, DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
|
||||
import { DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
|
||||
@ -12,10 +12,6 @@ export class ConfigurationService {
|
||||
this.environmentConfiguration = cleanEnv(process.env, {
|
||||
ACCESS_TOKEN_SALT: str(),
|
||||
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
|
||||
BASE_CURRENCY: str({
|
||||
choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'],
|
||||
default: DEFAULT_CURRENCY
|
||||
}),
|
||||
BETTER_UPTIME_API_KEY: str({ default: '' }),
|
||||
CACHE_QUOTES_TTL: num({ default: 1 }),
|
||||
CACHE_TTL: num({ default: 1 }),
|
||||
|
@ -127,12 +127,10 @@ export class DataGatheringService {
|
||||
uniqueAssets = await this.getUniqueAssets();
|
||||
}
|
||||
|
||||
const assetProfiles = await this.dataProviderService.getAssetProfiles(
|
||||
uniqueAssets
|
||||
);
|
||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
uniqueAssets
|
||||
);
|
||||
const assetProfiles =
|
||||
await this.dataProviderService.getAssetProfiles(uniqueAssets);
|
||||
const symbolProfiles =
|
||||
await this.symbolProfileService.getSymbolProfiles(uniqueAssets);
|
||||
|
||||
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
|
||||
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
||||
@ -147,7 +145,9 @@ export class DataGatheringService {
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
`Failed to enhance data for symbol ${symbol} by ${dataEnhancer.getName()}`,
|
||||
`Failed to enhance data for ${symbol} (${
|
||||
assetProfile.dataSource
|
||||
}) by ${dataEnhancer.getName()}`,
|
||||
error,
|
||||
'DataGatheringService'
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||
import * as Alphavantage from 'alphavantage';
|
||||
import { format, isAfter, isBefore, parse } from 'date-fns';
|
||||
|
||||
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
||||
@ -20,7 +21,7 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.alphaVantage = require('alphavantage')({
|
||||
this.alphaVantage = Alphavantage({
|
||||
key: this.configurationService.get('ALPHA_VANTAGE_API_KEY')
|
||||
});
|
||||
}
|
||||
@ -126,6 +127,9 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
return {
|
||||
items: result?.bestMatches?.map((bestMatch) => {
|
||||
return {
|
||||
assetClass: undefined,
|
||||
assetSubClass: undefined,
|
||||
currency: bestMatch['8. currency'],
|
||||
dataSource: this.getName(),
|
||||
name: bestMatch['2. name'],
|
||||
symbol: bestMatch['1. symbol']
|
||||
|
@ -1,10 +1,13 @@
|
||||
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 {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
@ -20,14 +23,9 @@ import got from 'got';
|
||||
|
||||
@Injectable()
|
||||
export class CoinGeckoService implements DataProviderInterface {
|
||||
private baseCurrency: string;
|
||||
private readonly URL = 'https://api.coingecko.com/api/v3';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
public constructor() {}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
return true;
|
||||
@ -39,13 +37,22 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
const response: Partial<SymbolProfile> = {
|
||||
assetClass: AssetClass.CASH,
|
||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
|
||||
try {
|
||||
const { name } = await got(`${this.URL}/coins/${aSymbol}`).json<any>();
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { name } = await got(`${this.URL}/coins/${aSymbol}`, {
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}).json<any>();
|
||||
|
||||
response.name = name;
|
||||
} catch (error) {
|
||||
@ -78,12 +85,22 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { prices } = await got(
|
||||
`${
|
||||
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
|
||||
)}&to=${getUnixTime(to)}`
|
||||
)}&to=${getUnixTime(to)}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
const result: {
|
||||
@ -127,19 +144,29 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/simple/price?ids=${aSymbols.join(
|
||||
','
|
||||
)}&vs_currencies=${this.baseCurrency.toLowerCase()}`
|
||||
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
for (const symbol in response) {
|
||||
if (Object.prototype.hasOwnProperty.call(response, symbol)) {
|
||||
results[symbol] = {
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.COINGECKO,
|
||||
marketPrice: response[symbol][this.baseCurrency.toLowerCase()],
|
||||
marketPrice: response[symbol][DEFAULT_CURRENCY.toLowerCase()],
|
||||
marketState: 'open'
|
||||
};
|
||||
}
|
||||
@ -165,9 +192,16 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
try {
|
||||
const { coins } = await got(
|
||||
`${this.URL}/search?query=${query}`
|
||||
).json<any>();
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { coins } = await got(`${this.URL}/search?query=${query}`, {
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}).json<any>();
|
||||
|
||||
items = coins.map(({ id: symbol, name }) => {
|
||||
return {
|
||||
@ -175,7 +209,7 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
symbol,
|
||||
assetClass: AssetClass.CASH,
|
||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
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 { Module } from '@nestjs/common';
|
||||
|
||||
import { DataEnhancerService } from './data-enhancer.service';
|
||||
|
||||
@Module({
|
||||
exports: [
|
||||
'DataEnhancers',
|
||||
DataEnhancerService,
|
||||
TrackinsightDataEnhancerService,
|
||||
YahooFinanceDataEnhancerService
|
||||
YahooFinanceDataEnhancerService,
|
||||
'DataEnhancers'
|
||||
],
|
||||
imports: [ConfigurationModule, CryptocurrencyModule],
|
||||
providers: [
|
||||
DataEnhancerService,
|
||||
TrackinsightDataEnhancerService,
|
||||
YahooFinanceDataEnhancerService,
|
||||
{
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { HttpException, Inject, Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
@Injectable()
|
||||
export class DataEnhancerService {
|
||||
public constructor(
|
||||
@Inject('DataEnhancers')
|
||||
private readonly dataEnhancers: DataEnhancerInterface[]
|
||||
) {}
|
||||
|
||||
public async enhance(aName: string) {
|
||||
const dataEnhancer = this.dataEnhancers.find((dataEnhancer) => {
|
||||
return dataEnhancer.getName() === aName;
|
||||
});
|
||||
|
||||
if (!dataEnhancer) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||
StatusCodes.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const assetProfile = await dataEnhancer.enhance({
|
||||
response: {
|
||||
assetClass: 'EQUITY',
|
||||
assetSubClass: 'ETF'
|
||||
},
|
||||
symbol: dataEnhancer.getTestSymbol()
|
||||
});
|
||||
|
||||
if (
|
||||
(assetProfile.countries as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||
(assetProfile.sectors as unknown as Prisma.JsonArray)?.length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@ -7,7 +8,7 @@ import got from 'got';
|
||||
|
||||
@Injectable()
|
||||
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 countriesMapping = {
|
||||
'Russian Federation': 'Russia'
|
||||
@ -32,30 +33,82 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
return response;
|
||||
}
|
||||
|
||||
let abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const profile = await got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
return {};
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
const isin = profile.isin?.split(';')?.[0];
|
||||
const isin = profile?.isin?.split(';')?.[0];
|
||||
|
||||
if (isin) {
|
||||
response.isin = isin;
|
||||
}
|
||||
|
||||
abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const holdings = await got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
|
||||
symbol.split('.')?.[0]
|
||||
}.json`
|
||||
);
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
if (holdings?.weight < 0.95) {
|
||||
@ -114,4 +167,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
public getName() {
|
||||
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 { YahooFinanceDataEnhancerService } from './yahoo-finance.service';
|
||||
@ -26,16 +25,13 @@ jest.mock(
|
||||
);
|
||||
|
||||
describe('YahooFinanceDataEnhancerService', () => {
|
||||
let configurationService: ConfigurationService;
|
||||
let cryptocurrencyService: CryptocurrencyService;
|
||||
let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService;
|
||||
|
||||
beforeAll(async () => {
|
||||
configurationService = new ConfigurationService();
|
||||
cryptocurrencyService = new CryptocurrencyService();
|
||||
|
||||
yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService(
|
||||
configurationService,
|
||||
cryptocurrencyService
|
||||
);
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
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 { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
DataSource,
|
||||
Prisma,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { countries } from 'countries-list';
|
||||
@ -16,23 +16,18 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa
|
||||
|
||||
@Injectable()
|
||||
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly cryptocurrencyService: CryptocurrencyService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||
let symbol = aYahooFinanceSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
new RegExp(`-${DEFAULT_CURRENCY}$`),
|
||||
DEFAULT_CURRENCY
|
||||
);
|
||||
|
||||
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
if (symbol.includes('=X') && !symbol.includes(DEFAULT_CURRENCY)) {
|
||||
symbol = `${DEFAULT_CURRENCY}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol.replace('=X', '');
|
||||
@ -47,21 +42,18 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
*/
|
||||
public convertToYahooFinanceSymbol(aSymbol: string) {
|
||||
if (
|
||||
aSymbol.includes(this.baseCurrency) &&
|
||||
aSymbol.length > this.baseCurrency.length
|
||||
aSymbol.includes(DEFAULT_CURRENCY) &&
|
||||
aSymbol.length > DEFAULT_CURRENCY.length
|
||||
) {
|
||||
if (
|
||||
isCurrency(
|
||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
|
||||
)
|
||||
) {
|
||||
return `${aSymbol}=X`;
|
||||
} else if (
|
||||
this.cryptocurrencyService.isCryptocurrency(
|
||||
aSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
)
|
||||
aSymbol.replace(new RegExp(`-${DEFAULT_CURRENCY}$`), DEFAULT_CURRENCY)
|
||||
)
|
||||
) {
|
||||
// Add a dash before the last three characters
|
||||
@ -69,8 +61,8 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
// DOGEUSD -> DOGE-USD
|
||||
// SOL1USD -> SOL1-USD
|
||||
return aSymbol.replace(
|
||||
new RegExp(`-?${this.baseCurrency}$`),
|
||||
`-${this.baseCurrency}`
|
||||
new RegExp(`-?${DEFAULT_CURRENCY}$`),
|
||||
`-${DEFAULT_CURRENCY}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -99,15 +91,14 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
yahooSymbol = quotes[0].symbol;
|
||||
}
|
||||
|
||||
const { countries, sectors, url } = await this.getAssetProfile(
|
||||
yahooSymbol
|
||||
);
|
||||
const { countries, sectors, url } =
|
||||
await this.getAssetProfile(yahooSymbol);
|
||||
|
||||
if (countries) {
|
||||
if ((countries as unknown as Prisma.JsonArray)?.length > 0) {
|
||||
response.countries = countries;
|
||||
}
|
||||
|
||||
if (sectors) {
|
||||
if ((sectors as unknown as Prisma.JsonArray)?.length > 0) {
|
||||
response.sectors = sectors;
|
||||
}
|
||||
|
||||
@ -234,6 +225,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
return DataSource.YAHOO;
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return 'AAPL';
|
||||
}
|
||||
|
||||
public parseAssetClass({
|
||||
quoteType,
|
||||
shortName
|
||||
|
@ -5,7 +5,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -18,19 +21,16 @@ import {
|
||||
import Big from 'big.js';
|
||||
import { format, isToday } from 'date-fns';
|
||||
import got from 'got';
|
||||
import ms from 'ms';
|
||||
|
||||
@Injectable()
|
||||
export class EodHistoricalDataService implements DataProviderInterface {
|
||||
private apiKey: string;
|
||||
private baseCurrency: string;
|
||||
private readonly URL = 'https://eodhistoricaldata.com/api';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
@ -78,6 +78,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
const symbol = this.convertToEodSymbol(aSymbol);
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/eod/${symbol}?api_token=${
|
||||
this.apiKey
|
||||
@ -86,9 +92,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
DATE_FORMAT
|
||||
)}&period={aGranularity}`,
|
||||
{
|
||||
timeout: {
|
||||
request: DEFAULT_REQUEST_TIMEOUT
|
||||
}
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
@ -138,14 +143,19 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const realTimeResponse = await got(
|
||||
`${this.URL}/real-time/${symbols[0]}?api_token=${
|
||||
this.apiKey
|
||||
}&fmt=json&s=${symbols.join(',')}`,
|
||||
{
|
||||
timeout: {
|
||||
request: DEFAULT_REQUEST_TIMEOUT
|
||||
}
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
@ -176,7 +186,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
})?.currency;
|
||||
|
||||
result[this.convertFromEodSymbol(code)] = {
|
||||
currency: currency ?? this.baseCurrency,
|
||||
currency: currency ?? DEFAULT_CURRENCY,
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||
@ -187,24 +197,24 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
{}
|
||||
);
|
||||
|
||||
if (response[`${this.baseCurrency}GBP`]) {
|
||||
response[`${this.baseCurrency}GBp`] = {
|
||||
...response[`${this.baseCurrency}GBP`],
|
||||
currency: `${this.baseCurrency}GBp`,
|
||||
if (response[`${DEFAULT_CURRENCY}GBP`]) {
|
||||
response[`${DEFAULT_CURRENCY}GBp`] = {
|
||||
...response[`${DEFAULT_CURRENCY}GBP`],
|
||||
currency: `${DEFAULT_CURRENCY}GBp`,
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}GBp`,
|
||||
value: response[`${this.baseCurrency}GBP`].marketPrice
|
||||
symbol: `${DEFAULT_CURRENCY}GBp`,
|
||||
value: response[`${DEFAULT_CURRENCY}GBP`].marketPrice
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
if (response[`${this.baseCurrency}ILS`]) {
|
||||
response[`${this.baseCurrency}ILA`] = {
|
||||
...response[`${this.baseCurrency}ILS`],
|
||||
currency: `${this.baseCurrency}ILA`,
|
||||
if (response[`${DEFAULT_CURRENCY}ILS`]) {
|
||||
response[`${DEFAULT_CURRENCY}ILA`] = {
|
||||
...response[`${DEFAULT_CURRENCY}ILS`],
|
||||
currency: `${DEFAULT_CURRENCY}ILA`,
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}ILA`,
|
||||
value: response[`${this.baseCurrency}ILS`].marketPrice
|
||||
symbol: `${DEFAULT_CURRENCY}ILA`,
|
||||
value: response[`${DEFAULT_CURRENCY}ILS`].marketPrice
|
||||
})
|
||||
};
|
||||
}
|
||||
@ -273,7 +283,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
if (symbol.endsWith('.FOREX')) {
|
||||
symbol = symbol.replace('GBX', 'GBp');
|
||||
symbol = symbol.replace('.FOREX', '');
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
symbol = `${DEFAULT_CURRENCY}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol;
|
||||
@ -286,17 +296,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
*/
|
||||
private convertToEodSymbol(aSymbol: string) {
|
||||
if (
|
||||
aSymbol.startsWith(this.baseCurrency) &&
|
||||
aSymbol.length > this.baseCurrency.length
|
||||
aSymbol.startsWith(DEFAULT_CURRENCY) &&
|
||||
aSymbol.length > DEFAULT_CURRENCY.length
|
||||
) {
|
||||
if (
|
||||
isCurrency(
|
||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
|
||||
)
|
||||
) {
|
||||
return `${aSymbol
|
||||
.replace('GBp', 'GBX')
|
||||
.replace(this.baseCurrency, '')}.FOREX`;
|
||||
.replace(DEFAULT_CURRENCY, '')}.FOREX`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,10 +320,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
symbol: string;
|
||||
value: number;
|
||||
}) {
|
||||
if (symbol === `${this.baseCurrency}GBp`) {
|
||||
if (symbol === `${DEFAULT_CURRENCY}GBp`) {
|
||||
// Convert GPB to GBp (pence)
|
||||
return new Big(value).mul(100).toNumber();
|
||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||
} else if (symbol === `${DEFAULT_CURRENCY}ILA`) {
|
||||
// Convert ILS to ILA
|
||||
return new Big(value).mul(100).toNumber();
|
||||
}
|
||||
@ -331,12 +341,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
let searchResult = [];
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
|
||||
{
|
||||
timeout: {
|
||||
request: DEFAULT_REQUEST_TIMEOUT
|
||||
}
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
|
@ -5,6 +5,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
@ -16,7 +20,6 @@ import got from 'got';
|
||||
@Injectable()
|
||||
export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
private apiKey: string;
|
||||
private baseCurrency: string;
|
||||
private readonly URL = 'https://financialmodelingprep.com/api/v3';
|
||||
|
||||
public constructor(
|
||||
@ -25,7 +28,6 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
this.apiKey = this.configurationService.get(
|
||||
'FINANCIAL_MODELING_PREP_API_KEY'
|
||||
);
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
@ -64,8 +66,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { historical } = await got(
|
||||
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`
|
||||
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
const result: {
|
||||
@ -111,13 +123,23 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`
|
||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
for (const { price, symbol } of response) {
|
||||
results[symbol] = {
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
||||
marketPrice: price,
|
||||
@ -145,8 +167,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const result = await got(
|
||||
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`
|
||||
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
items = result.map(({ currency, name, symbol }) => {
|
||||
|
@ -10,4 +10,6 @@ export interface DataEnhancerInterface {
|
||||
}): Promise<Partial<SymbolProfile>>;
|
||||
|
||||
getName(): string;
|
||||
|
||||
getTestSymbol(): string;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
extractNumberFromString,
|
||||
@ -95,7 +96,17 @@ export class ManualService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { body } = await got(url, { headers });
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { body } = await got(url, {
|
||||
headers,
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
|
@ -5,7 +5,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_REQUEST_TIMEOUT,
|
||||
ghostfolioFearAndGreedIndexSymbol
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -135,6 +138,12 @@ export class RapidApiService implements DataProviderInterface {
|
||||
oneYearAgo: { value: number; valueText: string };
|
||||
}> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { fgi } = await got(
|
||||
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
|
||||
{
|
||||
@ -142,7 +151,9 @@ export class RapidApiService implements DataProviderInterface {
|
||||
useQueryString: 'true',
|
||||
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
||||
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
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 { 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';
|
||||
@ -7,6 +6,7 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -18,15 +18,10 @@ import { Quote } from 'yahoo-finance2/dist/esm/src/modules/quote';
|
||||
|
||||
@Injectable()
|
||||
export class YahooFinanceService implements DataProviderInterface {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly cryptocurrencyService: CryptocurrencyService,
|
||||
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
return true;
|
||||
@ -212,50 +207,50 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
};
|
||||
|
||||
if (
|
||||
symbol === `${this.baseCurrency}GBP` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}GBp=X`)
|
||||
symbol === `${DEFAULT_CURRENCY}GBP` &&
|
||||
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}GBp=X`)
|
||||
) {
|
||||
// Convert GPB to GBp (pence)
|
||||
response[`${this.baseCurrency}GBp`] = {
|
||||
response[`${DEFAULT_CURRENCY}GBp`] = {
|
||||
...response[symbol],
|
||||
currency: 'GBp',
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}GBp`,
|
||||
symbol: `${DEFAULT_CURRENCY}GBp`,
|
||||
value: response[symbol].marketPrice
|
||||
})
|
||||
};
|
||||
} else if (
|
||||
symbol === `${this.baseCurrency}ILS` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}ILA=X`)
|
||||
symbol === `${DEFAULT_CURRENCY}ILS` &&
|
||||
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}ILA=X`)
|
||||
) {
|
||||
// Convert ILS to ILA
|
||||
response[`${this.baseCurrency}ILA`] = {
|
||||
response[`${DEFAULT_CURRENCY}ILA`] = {
|
||||
...response[symbol],
|
||||
currency: 'ILA',
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}ILA`,
|
||||
symbol: `${DEFAULT_CURRENCY}ILA`,
|
||||
value: response[symbol].marketPrice
|
||||
})
|
||||
};
|
||||
} else if (
|
||||
symbol === `${this.baseCurrency}ZAR` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}ZAc=X`)
|
||||
symbol === `${DEFAULT_CURRENCY}ZAR` &&
|
||||
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}ZAc=X`)
|
||||
) {
|
||||
// Convert ZAR to ZAc (cents)
|
||||
response[`${this.baseCurrency}ZAc`] = {
|
||||
response[`${DEFAULT_CURRENCY}ZAc`] = {
|
||||
...response[symbol],
|
||||
currency: 'ZAc',
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}ZAc`,
|
||||
symbol: `${DEFAULT_CURRENCY}ZAc`,
|
||||
value: response[symbol].marketPrice
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (yahooFinanceSymbols.includes(`${this.baseCurrency}USX=X`)) {
|
||||
if (yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}USX=X`)) {
|
||||
// Convert USD to USX (cent)
|
||||
response[`${this.baseCurrency}USX`] = {
|
||||
response[`${DEFAULT_CURRENCY}USX`] = {
|
||||
currency: 'USX',
|
||||
dataSource: this.getName(),
|
||||
marketPrice: new Big(1).mul(100).toNumber(),
|
||||
@ -303,8 +298,8 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
(quoteType === 'CRYPTOCURRENCY' &&
|
||||
this.cryptocurrencyService.isCryptocurrency(
|
||||
symbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
new RegExp(`-${DEFAULT_CURRENCY}$`),
|
||||
DEFAULT_CURRENCY
|
||||
)
|
||||
)) ||
|
||||
quoteTypes.includes(quoteType)
|
||||
@ -314,7 +309,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
if (quoteType === 'CRYPTOCURRENCY') {
|
||||
// Only allow cryptocurrencies in base currency to avoid having redundancy in the database.
|
||||
// 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') {
|
||||
// Allow GC=F, but not MGC=F
|
||||
return symbol.length === 4;
|
||||
@ -373,13 +368,13 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
symbol: string;
|
||||
value: number;
|
||||
}) {
|
||||
if (symbol === `${this.baseCurrency}GBp`) {
|
||||
if (symbol === `${DEFAULT_CURRENCY}GBp`) {
|
||||
// Convert GPB to GBp (pence)
|
||||
return new Big(value).mul(100).toNumber();
|
||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||
} else if (symbol === `${DEFAULT_CURRENCY}ILA`) {
|
||||
// Convert ILS to ILA
|
||||
return new Big(value).mul(100).toNumber();
|
||||
} else if (symbol === `${this.baseCurrency}ZAc`) {
|
||||
} else if (symbol === `${DEFAULT_CURRENCY}ZAc`) {
|
||||
// Convert ZAR to ZAc (cents)
|
||||
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 { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.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 { Injectable, Logger } from '@nestjs/common';
|
||||
import { format, isToday } from 'date-fns';
|
||||
@ -12,13 +14,11 @@ import { isNumber, uniq } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class ExchangeRateDataService {
|
||||
private baseCurrency: string;
|
||||
private currencies: string[] = [];
|
||||
private currencyPairs: IDataGatheringItem[] = [];
|
||||
private exchangeRates: { [currencyPair: string]: number } = {};
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly prismaService: PrismaService,
|
||||
@ -26,7 +26,7 @@ export class ExchangeRateDataService {
|
||||
) {}
|
||||
|
||||
public getCurrencies() {
|
||||
return this.currencies?.length > 0 ? this.currencies : [this.baseCurrency];
|
||||
return this.currencies?.length > 0 ? this.currencies : [DEFAULT_CURRENCY];
|
||||
}
|
||||
|
||||
public getCurrencyPairs() {
|
||||
@ -43,7 +43,6 @@ export class ExchangeRateDataService {
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
this.currencies = await this.prepareCurrencies();
|
||||
this.currencyPairs = [];
|
||||
this.exchangeRates = {};
|
||||
@ -113,9 +112,9 @@ export class ExchangeRateDataService {
|
||||
if (!this.exchangeRates[symbol]) {
|
||||
// Not found, calculate indirectly via base currency
|
||||
this.exchangeRates[symbol] =
|
||||
resultExtended[`${currency1}${this.baseCurrency}`]?.[date]
|
||||
resultExtended[`${currency1}${DEFAULT_CURRENCY}`]?.[date]
|
||||
?.marketPrice *
|
||||
resultExtended[`${this.baseCurrency}${currency2}`]?.[date]
|
||||
resultExtended[`${DEFAULT_CURRENCY}${currency2}`]?.[date]
|
||||
?.marketPrice;
|
||||
|
||||
// Calculate the opposite direction
|
||||
@ -144,9 +143,8 @@ export class ExchangeRateDataService {
|
||||
} else {
|
||||
// Calculate indirectly via base currency
|
||||
const factor1 =
|
||||
this.exchangeRates[`${aFromCurrency}${this.baseCurrency}`];
|
||||
const factor2 =
|
||||
this.exchangeRates[`${this.baseCurrency}${aToCurrency}`];
|
||||
this.exchangeRates[`${aFromCurrency}${DEFAULT_CURRENCY}`];
|
||||
const factor2 = this.exchangeRates[`${DEFAULT_CURRENCY}${aToCurrency}`];
|
||||
|
||||
factor = factor1 * factor2;
|
||||
|
||||
@ -204,28 +202,28 @@ export class ExchangeRateDataService {
|
||||
let marketPriceBaseCurrencyToCurrency: number;
|
||||
|
||||
try {
|
||||
if (this.baseCurrency === aFromCurrency) {
|
||||
if (aFromCurrency === DEFAULT_CURRENCY) {
|
||||
marketPriceBaseCurrencyFromCurrency = 1;
|
||||
} else {
|
||||
marketPriceBaseCurrencyFromCurrency = (
|
||||
await this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aFromCurrency}`
|
||||
symbol: `${DEFAULT_CURRENCY}${aFromCurrency}`
|
||||
})
|
||||
)?.marketPrice;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
if (this.baseCurrency === aToCurrency) {
|
||||
if (aToCurrency === DEFAULT_CURRENCY) {
|
||||
marketPriceBaseCurrencyToCurrency = 1;
|
||||
} else {
|
||||
marketPriceBaseCurrencyToCurrency = (
|
||||
await this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aToCurrency}`
|
||||
symbol: `${DEFAULT_CURRENCY}${aToCurrency}`
|
||||
})
|
||||
)?.marketPrice;
|
||||
}
|
||||
@ -295,14 +293,14 @@ export class ExchangeRateDataService {
|
||||
private prepareCurrencyPairs(aCurrencies: string[]) {
|
||||
return aCurrencies
|
||||
.filter((currency) => {
|
||||
return currency !== this.baseCurrency;
|
||||
return currency !== DEFAULT_CURRENCY;
|
||||
})
|
||||
.map((currency) => {
|
||||
return {
|
||||
currency1: this.baseCurrency,
|
||||
currency1: DEFAULT_CURRENCY,
|
||||
currency2: currency,
|
||||
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 {
|
||||
ACCESS_TOKEN_SALT: string;
|
||||
ALPHA_VANTAGE_API_KEY: string;
|
||||
BASE_CURRENCY: string;
|
||||
BETTER_UPTIME_API_KEY: string;
|
||||
CACHE_QUOTES_TTL: number;
|
||||
CACHE_TTL: number;
|
||||
|
@ -65,9 +65,8 @@ export class TwitterBotService {
|
||||
status += benchmarkListing;
|
||||
}
|
||||
|
||||
const { data: createdTweet } = await this.twitterClient.v2.tweet(
|
||||
status
|
||||
);
|
||||
const { data: createdTweet } =
|
||||
await this.twitterClient.v2.tweet(status);
|
||||
|
||||
Logger.log(
|
||||
`Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}`,
|
||||
|
@ -1,37 +1,26 @@
|
||||
<header>
|
||||
<gf-header
|
||||
class="position-fixed w-100"
|
||||
[currentRoute]="currentRoute"
|
||||
[info]="info"
|
||||
[pageTitle]="pageTitle"
|
||||
[user]="user"
|
||||
(signOut)="onSignOut()"
|
||||
></gf-header>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
<div
|
||||
*ngIf="canCreateAccount || (info?.systemMessage && user)"
|
||||
class="container info-message-container"
|
||||
class="info-message-container"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2 text-center">
|
||||
<div class="info-message-inner-container position-fixed w-100">
|
||||
<div class="align-items-center d-flex h-100 justify-content-center">
|
||||
<a
|
||||
*ngIf="canCreateAccount"
|
||||
class="text-center"
|
||||
[routerLink]="routerLinkRegister"
|
||||
>
|
||||
<div
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
||||
class="cursor-pointer d-inline-block info-message"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<span>You are using the Live Demo.</span>
|
||||
<span class="a ml-2">Create Account</span>
|
||||
<span i18n>You are using the Live Demo.</span>
|
||||
<span class="a ml-2" i18n>Create Account</span>
|
||||
</div></a
|
||||
>
|
||||
<div
|
||||
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2 text-truncate"
|
||||
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||
(click)="onShowSystemMessage()"
|
||||
>
|
||||
{{ info.systemMessage }}
|
||||
@ -40,6 +29,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-header
|
||||
class="position-fixed w-100"
|
||||
[currentRoute]="currentRoute"
|
||||
[hasTabs]="hasTabs"
|
||||
[info]="info"
|
||||
[pageTitle]="pageTitle"
|
||||
[user]="user"
|
||||
(signOut)="onSignOut()"
|
||||
></gf-header>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
|
@ -4,31 +4,47 @@
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
|
||||
&.has-info-message {
|
||||
header {
|
||||
height: calc(2 * var(--mat-toolbar-standard-height));
|
||||
|
||||
.info-message-container {
|
||||
height: var(--mat-toolbar-standard-height);
|
||||
|
||||
.info-message-inner-container {
|
||||
background-color: rgba(var(--palette-primary-500), 1);
|
||||
height: var(--mat-toolbar-standard-height);
|
||||
z-index: 999;
|
||||
|
||||
.info-message {
|
||||
color: rgba(var(--palette-foreground-text), 1);
|
||||
font-size: 80%;
|
||||
max-width: 100%;
|
||||
|
||||
.a {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: calc(100vh - 2 * var(--mat-toolbar-standard-height));
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
header {
|
||||
height: var(--mat-toolbar-standard-height);
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 100vh;
|
||||
padding-top: 5rem;
|
||||
|
||||
.info-message-container {
|
||||
height: 3.5rem;
|
||||
margin-top: -0.5rem;
|
||||
|
||||
.info-message {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
border-radius: 2rem;
|
||||
font-size: 80%;
|
||||
max-width: 100%;
|
||||
|
||||
.a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
min-height: calc(100vh - var(--mat-toolbar-standard-height));
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,12 +52,4 @@
|
||||
footer {
|
||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
||||
}
|
||||
|
||||
main {
|
||||
.info-message-container {
|
||||
.info-message {
|
||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
@ -28,14 +29,20 @@ import { UserService } from './services/user/user.service';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.has-info-message') get getHasMessage() {
|
||||
return this.hasInfoMessage;
|
||||
}
|
||||
|
||||
public canCreateAccount: boolean;
|
||||
public currentRoute: string;
|
||||
public currentYear = new Date().getFullYear();
|
||||
public deviceType: string;
|
||||
public hasInfoMessage: boolean;
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForStatistics: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public hasTabs = false;
|
||||
public info: InfoItem;
|
||||
public pageTitle: string;
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
@ -103,6 +110,14 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
const urlSegments = urlSegmentGroup.segments;
|
||||
this.currentRoute = urlSegments[0].path;
|
||||
|
||||
this.hasTabs =
|
||||
(this.currentRoute === this.routerLinkAbout[0].slice(1) ||
|
||||
this.currentRoute === 'admin' ||
|
||||
this.currentRoute === 'home' ||
|
||||
this.currentRoute === 'portfolio' ||
|
||||
this.currentRoute === 'zen') &&
|
||||
this.deviceType !== 'mobile';
|
||||
|
||||
this.showFooter =
|
||||
(this.currentRoute === 'blog' ||
|
||||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
|
||||
@ -140,6 +155,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
permissions.createUserAccount
|
||||
);
|
||||
|
||||
this.hasInfoMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!this.info.systemMessage;
|
||||
|
||||
this.initializeTheme(this.user?.settings.colorScheme);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
@ -1,3 +1,15 @@
|
||||
<div *ngIf="hasPermissionToCreateAccess" class="d-flex justify-content-end">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
Add Access
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="alias">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
||||
|
@ -19,6 +19,7 @@ import { Access } from '@ghostfolio/common/interfaces';
|
||||
})
|
||||
export class AccessTableComponent implements OnChanges, OnInit {
|
||||
@Input() accesses: Access[];
|
||||
@Input() hasPermissionToCreateAccess = false;
|
||||
@Input() showActions: boolean;
|
||||
|
||||
@Output() accessDeleted = new EventEmitter<string>();
|
||||
|
@ -3,13 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AccessTableComponent } from './access-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccessTableComponent],
|
||||
exports: [AccessTableComponent],
|
||||
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPortfolioAccessTableModule {}
|
||||
|
@ -29,13 +29,13 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./account-detail-dialog.component.scss']
|
||||
})
|
||||
export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
public accountType: string;
|
||||
public balance: number;
|
||||
public currency: string;
|
||||
public equity: number;
|
||||
public name: string;
|
||||
public orders: OrderWithAccount[];
|
||||
public platformName: string;
|
||||
public transactionCount: number;
|
||||
public user: User;
|
||||
public valueInBaseCurrency: number;
|
||||
|
||||
@ -65,15 +65,14 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(
|
||||
({
|
||||
accountType,
|
||||
balance,
|
||||
currency,
|
||||
name,
|
||||
Platform,
|
||||
transactionCount,
|
||||
value,
|
||||
valueInBaseCurrency
|
||||
}) => {
|
||||
this.accountType = translate(accountType);
|
||||
this.balance = balance;
|
||||
this.currency = currency;
|
||||
|
||||
@ -85,6 +84,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
|
||||
this.name = name;
|
||||
this.platformName = Platform?.name ?? '-';
|
||||
this.transactionCount = transactionCount;
|
||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
@ -44,8 +44,8 @@
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value i18n size="medium" [value]="accountType"
|
||||
>Account Type</gf-value
|
||||
<gf-value i18n size="medium" [value]="transactionCount"
|
||||
>Activities</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
|
@ -85,7 +85,7 @@
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1 text-right"
|
||||
class="justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="transactionCount"
|
||||
>
|
||||
@ -93,9 +93,7 @@
|
||||
<span class="d-none d-sm-block" i18n>Activities</span>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
|
||||
element.transactionCount
|
||||
}}</ng-container>
|
||||
{{ element.transactionCount }}
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||
{{ transactionCount }}
|
||||
|
@ -29,7 +29,7 @@
|
||||
}"
|
||||
[title]="
|
||||
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
| date : defaultDateFormat) ?? ''
|
||||
| date: defaultDateFormat) ?? ''
|
||||
"
|
||||
(click)="
|
||||
onOpenMarketDataDetail({
|
||||
|
@ -8,7 +8,6 @@ import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-admin-settings',
|
||||
styleUrls: ['./admin-settings.component.scss'],
|
||||
templateUrl: './admin-settings.component.html'
|
||||
|
@ -1,14 +1,17 @@
|
||||
<mat-toolbar class="px-2">
|
||||
<mat-toolbar class="px-0">
|
||||
<ng-container *ngIf="user">
|
||||
<a
|
||||
class="align-items-center d-flex h-100 no-min-width px-2 rounded-0"
|
||||
mat-button
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo [label]="pageTitle"></gf-logo>
|
||||
</a>
|
||||
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
|
||||
<a
|
||||
class="align-items-center justify-content-start rounded-0"
|
||||
mat-button
|
||||
[ngClass]="{ 'w-100': hasTabs }"
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo class="px-2" [label]="pageTitle"></gf-logo>
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<ul class="alig-items-center d-flex list-inline m-0">
|
||||
<ul class="alig-items-center d-flex list-inline m-0 px-2">
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
@ -26,7 +29,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -39,7 +42,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -52,7 +55,7 @@
|
||||
</li>
|
||||
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -65,7 +68,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -83,7 +86,7 @@
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -96,7 +99,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -129,33 +132,37 @@
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<ng-container *ngIf="user?.access?.length > 0">
|
||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||
<ion-icon
|
||||
*ngIf="user?.access?.length > 0"
|
||||
class="mr-2"
|
||||
[name]="
|
||||
impersonationId
|
||||
? 'radio-button-off-outline'
|
||||
: 'radio-button-on-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span i18n>Me</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
*ngIf="user?.access?.length > 0"
|
||||
class="mr-2"
|
||||
[name]="
|
||||
impersonationId
|
||||
? 'radio-button-off-outline'
|
||||
: 'radio-button-on-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span i18n>Me</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let accessItem of user?.access"
|
||||
mat-menu-item
|
||||
(click)="impersonateAccount(accessItem.id)"
|
||||
>
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
@ -242,21 +249,25 @@
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="user === null">
|
||||
<a
|
||||
class="align-items-center d-flex h-100 mx-2 no-min-width px-2 rounded-0"
|
||||
mat-button
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo
|
||||
[label]="pageTitle"
|
||||
[showLabel]="currentRoute !== 'register'"
|
||||
></gf-logo>
|
||||
</a>
|
||||
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
|
||||
<a
|
||||
class="align-items-center justify-content-start rounded-0"
|
||||
mat-button
|
||||
[ngClass]="{ 'w-100': hasTabs }"
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo
|
||||
class="px-2"
|
||||
[label]="pageTitle"
|
||||
[showLabel]="currentRoute !== 'register'"
|
||||
></gf-logo>
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<ul class="alig-items-center d-flex list-inline m-0">
|
||||
<ul class="alig-items-center d-flex list-inline m-0 px-2">
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -269,7 +280,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -282,6 +293,7 @@
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription" class="list-inline-item">
|
||||
<a
|
||||
class="d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -297,7 +309,7 @@
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -317,13 +329,13 @@
|
||||
></a>
|
||||
</li>
|
||||
<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>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser"
|
||||
class="list-inline-item"
|
||||
class="list-inline-item ml-1"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
|
@ -7,8 +7,18 @@
|
||||
.mat-toolbar {
|
||||
background-color: var(--light-background);
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
.logo-container {
|
||||
&.filled {
|
||||
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-inline-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mdc-button {
|
||||
@ -24,11 +34,21 @@
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-toolbar {
|
||||
background-color: var(--dark-background);
|
||||
|
||||
.logo-container {
|
||||
&.filled {
|
||||
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import { catchError, takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class HeaderComponent implements OnChanges {
|
||||
@Input() currentRoute: string;
|
||||
@Input() hasTabs: boolean;
|
||||
@Input() info: InfoItem;
|
||||
@Input() pageTitle: string;
|
||||
@Input() user: User;
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { Directive, HostListener, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[gfFileDrop]'
|
||||
})
|
||||
export class FileDropDirective {
|
||||
@Output() filesDropped = new EventEmitter<FileList>();
|
||||
|
||||
@HostListener('dragenter', ['$event']) onDragEnter(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@HostListener('dragover', ['$event']) onDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@HostListener('drop', ['$event']) onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Prevent the browser's default behavior for handling the file drop
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
|
||||
this.filesDropped.emit(event.dataTransfer.files);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { FileDropDirective } from './file-drop.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FileDropDirective],
|
||||
exports: [FileDropDirective]
|
||||
})
|
||||
export class GfFileDropModule {}
|
@ -1,10 +1,11 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { paths } from '@ghostfolio/client/app-routing.module';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
import { paths } from '@ghostfolio/client/app-routing.module';
|
||||
import * as path from 'path';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -1,28 +1,20 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-about-page',
|
||||
styleUrls: ['./about-page.scss'],
|
||||
templateUrl: './about-page.html'
|
||||
})
|
||||
export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public deviceType: string;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
@ -32,9 +24,10 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { globalPermissions, systemMessage } = this.dataService.fetchInfo();
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
globalPermissions,
|
||||
@ -71,12 +64,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
this.user = state.user;
|
||||
|
||||
this.hasMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!systemMessage;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
@ -88,7 +75,9 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
|
@ -2,7 +2,12 @@
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
@ -14,7 +19,10 @@
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -2,27 +2,6 @@
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.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;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -2,7 +2,6 @@ import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-changelog-page',
|
||||
styleUrls: ['./changelog-page.scss'],
|
||||
templateUrl: './changelog-page.html'
|
||||
|
@ -2,7 +2,6 @@ import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-license-page',
|
||||
styleUrls: ['./license-page.scss'],
|
||||
templateUrl: './license-page.html'
|
||||
|
@ -1,138 +1,15 @@
|
||||
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 = [
|
||||
{
|
||||
description: 'Build custom software on top of your data.',
|
||||
name: 'Appsmith',
|
||||
url: 'https://www.appsmith.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'BoxyHQ’s suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.',
|
||||
name: 'BoxyHQ',
|
||||
url: 'https://boxyhq.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.',
|
||||
name: 'Cal.com',
|
||||
url: 'https://cal.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Centralize community, product, and customer data to understand which companies are engaging with your open source project.',
|
||||
name: 'Crowd.dev',
|
||||
url: 'https://www.crowd.dev'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.',
|
||||
name: 'Documenso',
|
||||
url: 'https://documenso.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.',
|
||||
name: 'Erxes',
|
||||
url: 'https://erxes.io'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.',
|
||||
name: 'Formbricks',
|
||||
url: 'https://formbricks.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'GitWonk is an open-source technical documentation tool, designed and built focusing on the developer experience.',
|
||||
name: 'GitWonk',
|
||||
url: 'https://gitwonk.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.',
|
||||
name: 'Hanko',
|
||||
url: 'https://www.hanko.io'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.',
|
||||
name: 'HTMX',
|
||||
url: 'https://htmx.org'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.',
|
||||
name: 'Infisical',
|
||||
url: 'https://infisical.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Mockoon is the easiest and quickest way to design and run mock REST APIs.',
|
||||
name: 'Mockoon',
|
||||
url: 'https://mockoon.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.',
|
||||
name: 'Novu',
|
||||
url: 'https://novu.co'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.',
|
||||
name: 'OpenBB',
|
||||
url: 'https://openbb.co'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.',
|
||||
name: 'Sniffnet',
|
||||
url: 'https://www.sniffnet.net'
|
||||
},
|
||||
{
|
||||
description: 'Software localization from A to Z made really easy.',
|
||||
name: 'Tolgee',
|
||||
url: 'https://tolgee.io'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Create long-running Jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.',
|
||||
name: 'Trigger.dev',
|
||||
url: 'https://trigger.dev'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.',
|
||||
name: 'Typebot',
|
||||
url: 'https://typebot.io'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'A modern CRM offering the flexibility of open-source, advanced features and sleek design.',
|
||||
name: 'Twenty',
|
||||
url: 'https://twenty.com'
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.',
|
||||
name: 'Webiny',
|
||||
url: 'https://www.webiny.com'
|
||||
},
|
||||
{
|
||||
description: 'Webstudio is an open source alternative to Webflow',
|
||||
name: 'Webstudio',
|
||||
url: 'https://webstudio.is'
|
||||
}
|
||||
];
|
||||
public ossFriends = ossFriends.data;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
|
@ -14,26 +14,27 @@
|
||||
*ngFor="let ossFriend of ossFriends"
|
||||
class="col-xs-12 col-md-4 mb-3"
|
||||
>
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="h4"
|
||||
><a target="_blank" [href]="ossFriend.url"
|
||||
>{{ ossFriend.name }}</a
|
||||
></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.url">
|
||||
<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 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>
|
||||
|
@ -1,10 +1,10 @@
|
||||
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';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
@NgModule({
|
||||
declarations: [OpenSourceSoftwareFriendsPageComponent],
|
||||
|
@ -1,3 +1,9 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-mdc-card {
|
||||
&:hover {
|
||||
border-color: var(--gf-theme-primary-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-about-overview-page',
|
||||
styleUrls: ['./about-overview-page.scss'],
|
||||
templateUrl: './about-overview-page.html'
|
||||
|
@ -1,7 +1,9 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center">About Ghostfolio</h1>
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center">
|
||||
<ng-container i18n>About Ghostfolio</ng-container>
|
||||
</h1>
|
||||
<div class="about-container">
|
||||
<p>
|
||||
Ghostfolio is a lightweight wealth management application for
|
||||
|
@ -2,7 +2,6 @@ import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-privacy-policy-page',
|
||||
styleUrls: ['./privacy-policy-page.scss'],
|
||||
templateUrl: './privacy-policy-page.html'
|
||||
|
@ -10,7 +10,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Account as AccountModel, AccountType } from '@prisma/client';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -151,7 +151,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public openUpdateAccountDialog({
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
@ -163,7 +162,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
data: {
|
||||
account: {
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
@ -232,7 +230,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
data: {
|
||||
account: {
|
||||
accountType: AccountType.SECURITIES,
|
||||
balance: 0,
|
||||
comment: null,
|
||||
currency: this.user?.settings?.baseCurrency,
|
||||
|
@ -8,15 +8,6 @@
|
||||
<input matInput name="name" required [(ngModel)]="data.account.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
<mat-select name="type" required [(value)]="data.account.accountType">
|
||||
<mat-option i18n value="CASH">Cash</mat-option>
|
||||
<mat-option i18n value="SECURITIES">Securities</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Currency</mat-label>
|
||||
|
@ -1,30 +1,25 @@
|
||||
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { TabConfiguration } from '@ghostfolio/common/interfaces';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-admin-page',
|
||||
styleUrls: ['./admin-page.scss'],
|
||||
templateUrl: './admin-page.html'
|
||||
})
|
||||
export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public deviceType: string;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(private dataService: DataService) {
|
||||
const { systemMessage } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasMessage = !!systemMessage;
|
||||
}
|
||||
public constructor(private deviceService: DeviceDetectorService) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.tabs = [
|
||||
{
|
||||
iconName: 'reader-outline',
|
||||
|
@ -2,7 +2,12 @@
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
@ -14,7 +19,10 @@
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -2,27 +2,6 @@
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.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;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div class="mb-3 text-muted"><small>2023-07-01</small></div>
|
||||
<img
|
||||
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"
|
||||
title="Exploring the Path to Financial Independence and Retiring Early (FIRE)"
|
||||
/>
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [MatButtonModule, RouterModule],
|
||||
selector: 'gf-ghostfolio-joins-oss-friends-page',
|
||||
standalone: true,
|
||||
templateUrl: './ghostfolio-joins-oss-friends-page.html'
|
||||
})
|
||||
export class GhostfolioJoinsOssFriendsPageComponent {
|
||||
public routerLinkAboutOssFriends = ['/' + $localize`about`, 'oss-friends'];
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Ghostfolio joins OSS Friends</h1>
|
||||
<div class="mb-3 text-muted"><small>2023-08-23</small></div>
|
||||
<img
|
||||
alt="Ghostfolio joins OSS Friends Teaser"
|
||||
class="rounded w-100"
|
||||
src="../assets/images/blog/ghostfolio-joins-oss-friends.png"
|
||||
title="Ghostfolio joins OSS Friends"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
We are excited to announce that Ghostfolio is now part of the
|
||||
<a [routerLink]="routerLinkAboutOssFriends">OSS Friends</a>. This
|
||||
new initiative is all about helping open source projects grow and
|
||||
become more popular.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">The Story of OSS Friends</h2>
|
||||
<p>
|
||||
OSS Friends started as a simple
|
||||
<a
|
||||
href="https://twitter.com/formbricks/status/1660735970281508878"
|
||||
target="_blank"
|
||||
>post</a
|
||||
>
|
||||
on X (formerly known as <i>Twitter</i>). The idea came from
|
||||
<a href="https://formbricks.com" target="_blank">Formbricks</a>, an
|
||||
open source experience management platform to create surveys in
|
||||
minutes, and is all about giving open source projects a boost.
|
||||
</p>
|
||||
<p>
|
||||
If you are excited about the OSS Friends movement and want to bring
|
||||
your own open source project along, just take a moment to fill out
|
||||
<a
|
||||
href="https://app.formbricks.com/s/clhys1p9r001cpr0hu65rwh17"
|
||||
target="_blank"
|
||||
>this form</a
|
||||
>. Let’s work and learn together – all the open source way.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">
|
||||
Ghostfolio – Next Generation Software for your Personal Finances
|
||||
</h2>
|
||||
<p>
|
||||
Money management can be tricky, especially when you have various
|
||||
investments like cryptocurrencies, ETFs and stocks in your
|
||||
portfolio. But guess what? There are cooler ways than staring at
|
||||
boring spreadsheets. Say hello to Ghostfolio, a privacy-first, open
|
||||
source dashboard for your personal finances.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4 py-3">
|
||||
<h2 class="h4 mb-0 text-center">
|
||||
Would you like to simplify asset tracking?
|
||||
</h2>
|
||||
<p class="lead mb-2 text-center">
|
||||
Ghostfolio empowers you to make informed investment decisions.
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<a color="primary" href="https://ghostfol.io" mat-flat-button>
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Asset</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Collaboration</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Cryptocurrency</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Community</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Dashboard</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">ETF</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Initiative</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Innovation</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS Friends</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Personal Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Platform</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Privacy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Stock</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Technology</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Tracking</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Web3</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Ghostfolio joins OSS Friends
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [MatButtonModule, RouterModule],
|
||||
selector: 'gf-ghostfolio-2-page',
|
||||
standalone: true,
|
||||
templateUrl: './ghostfolio-2-page.html'
|
||||
})
|
||||
export class Ghostfolio2PageComponent {
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkAboutChangelog = ['/' + $localize`about`, 'changelog'];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkMarkets = ['/' + $localize`markets`];
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Ghostfolio 2.0</h1>
|
||||
<div class="mb-3 text-muted"><small>2023-09-09</small></div>
|
||||
<img
|
||||
alt="Ghostfolio 2.0 Teaser"
|
||||
class="border rounded w-100"
|
||||
src="../assets/images/blog/ghostfolio-2.jpg"
|
||||
title="Announcing Ghostfolio 2.0"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
Since late 2020, when
|
||||
<a [routerLink]="routerLinkAbout">Ghostfolio</a> took shape, the
|
||||
main goal has remained the same: to simplify investment tracking
|
||||
while prioritizing user privacy and enable investors to make
|
||||
informed decisions. Our journey so far has been incredible, with
|
||||
over 300 releases since the
|
||||
<a href="../en/blog/2021/07/hello-ghostfolio">first major release</a
|
||||
>, close to 300’000 pulls on Docker Hub, and collaboration with 50+
|
||||
contributors from around the globe. Ghostfolio was recently featured
|
||||
on
|
||||
<a
|
||||
href="https://news.ycombinator.com/item?id=37337482"
|
||||
target="_blank"
|
||||
>Hacker News</a
|
||||
>, where it ranked on the front page and briefly hit the #1 spot.
|
||||
Shortly after, the project’s repository was trending on GitHub.
|
||||
These achievements emphasize the growing recognition for our project
|
||||
and the path we are on.
|
||||
</p>
|
||||
<p>
|
||||
Now, we are thrilled to present Ghostfolio 2.0, another milestone in
|
||||
our journey to empower investors.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Introducing Ghostfolio 2.0</h2>
|
||||
<p>
|
||||
Ghostfolio 2.0 is the evolution of our
|
||||
<a [routerLink]="routerLinkFeatures"
|
||||
>open source wealth management software</a
|
||||
>, elevating both user and developer experiences. We have extended
|
||||
data import capabilities, added comprehensive analytics, increased
|
||||
stability, and utilized the latest technology to deliver these
|
||||
improvements. Here is a closer look at a selection of the
|
||||
improvements you can expect from this
|
||||
<a [routerLink]="routerLinkAboutChangelog">release</a>, alongside
|
||||
uncounted smaller additions and enhancements.
|
||||
</p>
|
||||
<h3 class="h5">Extended Data Import Capabilities</h3>
|
||||
<p>
|
||||
Importing account activities is an important aspect of any portfolio
|
||||
management software. With Ghostfolio 2.0, we have extended our data
|
||||
import functionality, ensuring a seamless experience for users. Our
|
||||
system supports multiple formats to make the experience more
|
||||
seamless. Additionally, there is an API, providing you with even
|
||||
greater flexibility and control over how you import transactions.
|
||||
</p>
|
||||
<h3 class="h5">Comprehensive Analytics</h3>
|
||||
<p>
|
||||
Understanding your wealth is key. The latest release offers more
|
||||
comprehensive analytics to categorize your securities, providing
|
||||
invaluable insights into your investment portfolio for making
|
||||
informed decisions and optimizing diversification.
|
||||
</p>
|
||||
<h3 class="h5">Reliable Stability</h3>
|
||||
<p>
|
||||
Ensuring the stability of software is crucial, and a platform for
|
||||
managing your wealth is no exception. The increased robustness of
|
||||
our architecture means that you can count on Ghostfolio to be there
|
||||
when you need it most, no matter the
|
||||
<a [routerLink]="routerLinkMarkets">market conditions</a>.
|
||||
</p>
|
||||
<h3 class="h5">Cutting-Edge Technology Stack</h3>
|
||||
<p>
|
||||
Ghostfolio 2.0 leverages the latest tech stack to deliver a superior
|
||||
user and developer experience. We have upgraded to
|
||||
<a href="https://angular.io" target="_blank">Angular 16</a>,
|
||||
<a href="https://nestjs.com" target="_blank">Nest.js 10</a>,
|
||||
<a href="https://www.prisma.io" target="_blank">Prisma 5</a>, and
|
||||
<a href="https://nx.dev" target="_blank">Nx 16</a>, ensuring that
|
||||
the software is at the forefront of innovation. This upgrade allows
|
||||
us to provide you with the best possible user experience, making
|
||||
investment tracking more intuitive than ever before.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Thriving Ghostfolio Community</h2>
|
||||
<p>
|
||||
In open source software (OSS) projects like Ghostfolio, the
|
||||
community is the driving force behind its success. Without the
|
||||
incredible support of our users and contributors, it would not have
|
||||
been possible. As we celebrate the launch of Ghostfolio 2.0, we are
|
||||
delighted to showcase the growth of the Ghostfolio community:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Ghostfolio has accumulated <strong>2500+ stars</strong> on
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
target="_blank"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>, highlighting the appreciation and adoption of our platform by
|
||||
the community.
|
||||
</li>
|
||||
<li>
|
||||
Our
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
target="_blank"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community has expanded to over <strong>350 members</strong>,
|
||||
creating a space for like-minded investors to connect, share
|
||||
insights, and collaborate.
|
||||
</li>
|
||||
<li>
|
||||
On
|
||||
<a href="https://twitter.com/ghostfolio_" target="_blank">X</a>
|
||||
(formerly Twitter), over
|
||||
<strong>300 investors and personal finance enthusiasts</strong>
|
||||
follow Ghostfolio, keen to stay updated on the latest
|
||||
developments.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
This is just the beginning. Ghostfolio is dedicated to ongoing
|
||||
improvement and helping grow a vibrant community of investors. We
|
||||
invite you to join us on this exciting journey.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Join our Slack community</strong>: Connect with fellow
|
||||
investors, share your insights, and stay updated on the latest news
|
||||
by joining our
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
target="_blank"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community. It is a dynamic space where you can learn, collaborate,
|
||||
and grow together with us.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Follow us on X</strong>: For release updates and market
|
||||
insights, follow
|
||||
<a href="https://twitter.com/ghostfolio_" target="_blank"
|
||||
>Ghostfolio on X</a
|
||||
>. It is the perfect place to stay informed and connect with our
|
||||
team.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Give us a Star</strong>: If you have found value in
|
||||
Ghostfolio or appreciate our commitment to simplifying investment
|
||||
tracking, please consider giving us a star on
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
target="_blank"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>. Your support helps us reach a wider audience and make a
|
||||
difference in the world of wealth management.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Become a contributor</strong>: If you are a developer
|
||||
passionate about open source projects and personal finance, we
|
||||
welcome your contributions.
|
||||
<a href="https://github.com/ghostfolio/ghostfolio" target="_blank"
|
||||
>Join our developer community</a
|
||||
>, collaborate with like-minded people, and help shape the future of
|
||||
Ghostfolio.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<p>
|
||||
Ghostfolio 2.0 represents a major step forward in our mission to
|
||||
empower investors, and we could not be more excited about the future
|
||||
of the project. Together, we can build an outstanding tool that
|
||||
makes our lives easier. Thank you for being a part of the Ghostfolio
|
||||
community.
|
||||
</p>
|
||||
<p>Thomas from Ghostfolio</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Angular</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Announcement</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Collaboration</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Community</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Contribution</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Evolution</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio 2.0</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Nest.js</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Nx</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Personal Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Platform</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Prisma</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Privacy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Release</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Stack</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Technology</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Web3</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Announcing Ghostfolio 2.0
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -136,6 +136,24 @@ const routes: Routes = [
|
||||
'./2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component'
|
||||
).then((c) => c.ExploringThePathToFirePageComponent),
|
||||
title: 'Exploring the Path to FIRE'
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
path: '2023/08/ghostfolio-joins-oss-friends',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component'
|
||||
).then((c) => c.GhostfolioJoinsOssFriendsPageComponent),
|
||||
title: 'Ghostfolio joins OSS Friends'
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
path: '2023/09/ghostfolio-2',
|
||||
loadComponent: () =>
|
||||
import('./2023/09/ghostfolio-2/ghostfolio-2-page.component').then(
|
||||
(c) => c.Ghostfolio2PageComponent
|
||||
),
|
||||
title: 'Ghostfolio 2.0'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -8,6 +8,58 @@
|
||||
finance</small
|
||||
>
|
||||
</h1>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
href="../en/blog/2023/09/ghostfolio-2"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
Announcing Ghostfolio 2.0
|
||||
</div>
|
||||
<div class="d-flex text-muted">2023-09-09</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
href="../en/blog/2023/08/ghostfolio-joins-oss-friends"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
Ghostfolio joins OSS Friends
|
||||
</div>
|
||||
<div class="d-flex text-muted">2023-08-23</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
|
@ -1,28 +1,20 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-home-page',
|
||||
styleUrls: ['./home-page.scss'],
|
||||
templateUrl: './home-page.html'
|
||||
})
|
||||
export class HomePageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public deviceType: string;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
@ -32,9 +24,10 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { globalPermissions, systemMessage } = this.dataService.fetchInfo();
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
globalPermissions,
|
||||
@ -70,18 +63,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
];
|
||||
this.user = state.user;
|
||||
|
||||
this.hasMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!systemMessage;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
|
@ -2,7 +2,12 @@
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
@ -14,7 +19,10 @@
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -2,27 +2,6 @@
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.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;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -1,9 +1,17 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h1 class="font-weight-bold intro mt-5" i18n>
|
||||
Manage your wealth like a boss
|
||||
</h1>
|
||||
<div>
|
||||
<div class="badge badge-light badge-pill border mb-3 px-3 py-2">
|
||||
<a href="../en/blog/2023/09/ghostfolio-2"
|
||||
><span class="mr-1 text-uppercase" i18n>New</span>
|
||||
<span class="font-weight-normal">Ghostfolio 2.0</span></a
|
||||
>
|
||||
</div>
|
||||
<h1 class="font-weight-bold intro" i18n>
|
||||
Manage your wealth like a boss
|
||||
</h1>
|
||||
</div>
|
||||
<p class="lead mb-4" i18n>
|
||||
Ghostfolio is a privacy-first, open source dashboard for your personal
|
||||
finances. Break down your asset allocation, know your net worth and make
|
||||
@ -134,6 +142,14 @@
|
||||
title="DEV Community - A constructive and inclusive social network for software developers"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-hacker-news mask"
|
||||
href="https://news.ycombinator.com"
|
||||
target="_blank"
|
||||
title="Hacker News"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-openstartup"
|
||||
|
@ -53,6 +53,10 @@
|
||||
mask-image: url('/assets/images/logo-dev-community.svg');
|
||||
}
|
||||
|
||||
&.logo-hacker-news {
|
||||
mask-image: url('/assets/images/logo-hacker-news.svg');
|
||||
}
|
||||
|
||||
&.logo-openstartup {
|
||||
background-image: url('/assets/images/logo-openstartup.png');
|
||||
background-position: center;
|
||||
@ -128,6 +132,7 @@
|
||||
&.logo-agplv3,
|
||||
&.logo-alternative-to,
|
||||
&.logo-dev-community,
|
||||
&.logo-hacker-news,
|
||||
&.logo-privacy-tools,
|
||||
&.logo-reddit,
|
||||
&.logo-sackgeld,
|
||||
|
@ -32,6 +32,7 @@
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 24 hours)"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -42,6 +43,7 @@
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -52,6 +54,7 @@
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -119,6 +122,7 @@
|
||||
<a class="d-block" href="https://status.ghostfol.io">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 90 days)"
|
||||
[isPercent]="true"
|
||||
|
@ -24,7 +24,6 @@ import { ImportActivitiesDialog } from './import-activities-dialog/import-activi
|
||||
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-activities-page',
|
||||
styleUrls: ['./activities-page.scss'],
|
||||
templateUrl: './activities-page.html'
|
||||
@ -238,9 +237,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateActivityDialog, {
|
||||
data: {
|
||||
activity,
|
||||
accounts: this.user?.accounts?.filter((account) => {
|
||||
return account.accountType === 'SECURITIES';
|
||||
}),
|
||||
accounts: this.user?.accounts,
|
||||
user: this.user
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
@ -282,9 +279,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateActivityDialog, {
|
||||
data: {
|
||||
accounts: this.user?.accounts?.filter((account) => {
|
||||
return account.accountType === 'SECURITIES';
|
||||
}),
|
||||
accounts: this.user?.accounts,
|
||||
activity: {
|
||||
...aActivity,
|
||||
accountId: aActivity?.accountId ?? this.defaultAccountId,
|
||||
|
@ -137,6 +137,20 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public onFilesDropped({
|
||||
files,
|
||||
stepper
|
||||
}: {
|
||||
files: FileList;
|
||||
stepper: MatStepper;
|
||||
}): void {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleFile({ stepper, file: files[0] });
|
||||
}
|
||||
|
||||
public onImportStepChange(event: StepperSelectionEvent) {
|
||||
if (event.selectedIndex === ImportStep.UPLOAD_FILE) {
|
||||
this.importStep = ImportStep.UPLOAD_FILE;
|
||||
@ -175,97 +189,15 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
aStepper.reset();
|
||||
}
|
||||
|
||||
public onSelectFile(aStepper: MatStepper) {
|
||||
public onSelectFile(stepper: MatStepper) {
|
||||
const input = document.createElement('input');
|
||||
input.accept = 'application/JSON, .csv';
|
||||
input.type = 'file';
|
||||
|
||||
input.onchange = (event) => {
|
||||
this.snackBar.open('⏳ ' + $localize`Validating data...`);
|
||||
|
||||
// Getting the file reference
|
||||
const file = (event.target as HTMLInputElement).files[0];
|
||||
|
||||
// Setting up the reader
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
||||
reader.onload = async (readerEvent) => {
|
||||
const fileContent = readerEvent.target.result as string;
|
||||
|
||||
try {
|
||||
if (file.name.endsWith('.json')) {
|
||||
const content = JSON.parse(fileContent);
|
||||
|
||||
this.accounts = content.accounts;
|
||||
|
||||
if (!isArray(content.activities)) {
|
||||
if (isArray(content.orders)) {
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: {
|
||||
error: {
|
||||
message: [`orders needs to be renamed to activities`]
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { activities } =
|
||||
await this.importActivitiesService.importJson({
|
||||
accounts: content.accounts,
|
||||
activities: content.activities,
|
||||
isDryRun: true
|
||||
});
|
||||
this.activities = activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({ error, activities: content.activities });
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
try {
|
||||
const data = await this.importActivitiesService.importCsv({
|
||||
fileContent,
|
||||
isDryRun: true,
|
||||
userAccounts: this.data.user.accounts
|
||||
});
|
||||
this.activities = data.activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: error?.activities ?? [],
|
||||
error: {
|
||||
error: { message: error?.error?.message ?? [error?.message] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: { error: { message: ['Unexpected format'] } }
|
||||
});
|
||||
} finally {
|
||||
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
||||
this.snackBar.dismiss();
|
||||
|
||||
aStepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
this.handleFile({ file, stepper });
|
||||
};
|
||||
|
||||
input.click();
|
||||
@ -282,6 +214,97 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private async handleFile({
|
||||
file,
|
||||
stepper
|
||||
}: {
|
||||
file: File;
|
||||
stepper: MatStepper;
|
||||
}): Promise<void> {
|
||||
this.snackBar.open('⏳ ' + $localize`Validating data...`);
|
||||
|
||||
// Setting up the reader
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
||||
reader.onload = async (readerEvent) => {
|
||||
const fileContent = readerEvent.target.result as string;
|
||||
|
||||
try {
|
||||
if (file.name.endsWith('.json')) {
|
||||
const content = JSON.parse(fileContent);
|
||||
|
||||
this.accounts = content.accounts;
|
||||
|
||||
if (!isArray(content.activities)) {
|
||||
if (isArray(content.orders)) {
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: {
|
||||
error: {
|
||||
message: [`orders needs to be renamed to activities`]
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { activities } =
|
||||
await this.importActivitiesService.importJson({
|
||||
accounts: content.accounts,
|
||||
activities: content.activities,
|
||||
isDryRun: true
|
||||
});
|
||||
this.activities = activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({ error, activities: content.activities });
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
try {
|
||||
const data = await this.importActivitiesService.importCsv({
|
||||
fileContent,
|
||||
isDryRun: true,
|
||||
userAccounts: this.data.user.accounts
|
||||
});
|
||||
this.activities = data.activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: error?.activities ?? [],
|
||||
error: {
|
||||
error: { message: error?.error?.message ?? [error?.message] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: { error: { message: ['Unexpected format'] } }
|
||||
});
|
||||
} finally {
|
||||
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
||||
this.snackBar.dismiss();
|
||||
|
||||
stepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleImportError({
|
||||
activities,
|
||||
error
|
||||
|
@ -70,29 +70,38 @@
|
||||
<ng-template #selectFile>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
class="py-4"
|
||||
color="primary"
|
||||
mat-stroked-button
|
||||
class="drop-area p-4 text-center text-muted"
|
||||
gfFileDrop
|
||||
(click)="onSelectFile(stepper)"
|
||||
(filesDropped)="onFilesDropped({stepper, files: $event})"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Choose File</span>
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center"
|
||||
>
|
||||
<ion-icon
|
||||
class="cloud-icon"
|
||||
name="cloud-upload-outline"
|
||||
></ion-icon>
|
||||
<span i18n>Choose or drop a file here</span>
|
||||
</div>
|
||||
</button>
|
||||
<p class="mb-0 mt-4 text-center">
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
<p class="mb-0 mt-3 text-center">
|
||||
<small>
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -109,7 +118,7 @@
|
||||
>
|
||||
</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
||||
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1"
|
||||
[activities]="activities"
|
||||
|
@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
||||
@ -23,6 +24,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||
GfActivitiesTableModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfFileDropModule,
|
||||
GfSymbolModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
|
@ -32,4 +32,32 @@
|
||||
right: 1.5rem;
|
||||
top: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||
border: 1px dashed
|
||||
rgba(
|
||||
var(--palette-foreground-divider),
|
||||
var(--palette-foreground-divider-alpha)
|
||||
);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--palette-primary-500), 1) !important;
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
}
|
||||
|
||||
.cloud-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.drop-area {
|
||||
border-color: rgba(
|
||||
var(--palette-foreground-divider-dark),
|
||||
var(--palette-foreground-divider-alpha-dark)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user