Compare commits

...

71 Commits

Author SHA1 Message Date
b088df2fa3 Release 2.0.0 (#2310) 2023-09-09 08:29:18 +02:00
f45d8f616a Bugfix/fix blog post ghostfolio 2 (#2307)
* Fix month

* Update sitemap.xml

* Update locales
2023-09-08 21:36:01 +02:00
d8300502ce Feature/upgrade yahoo finance2 to version 2.5.0 (#2306)
* Upgrade yahoo-finance2 to version 2.5.0

* Update changelog
2023-09-08 21:22:57 +02:00
502d51ad29 Feature/add blog post ghostfolio 2 (#2269)
* Add blog post: Ghostfolio 2.0

* Update changelog
2023-09-08 20:45:31 +02:00
bc33e5f147 Feature/remove deprecated environment variable base currency (#2255)
* Remove the deprecated environment variable BASE_CURRENCY

* Update changelog
2023-09-08 20:43:23 +02:00
48ba8f936b Feature/deactivate internet identity for account registration (#2293)
* Deactivate Internet Identity

* Update changelog
2023-09-08 20:23:22 +02:00
05ec4cce05 Add Portuguese landing page (#2301) 2023-09-08 17:06:58 +02:00
d74f283707 Eliminate prisma service (#2286)
* Eliminate prisma service
2023-09-08 17:05:42 +02:00
0f8bc7db32 Bugfix/do not remove countries and sectors in yahoo finance data enhancer (#2297)
* Do not remove countries and sectors

* Update changelog
2023-09-08 15:07:17 +02:00
431500f28a Update docker compose files to version 3.9 (#2299)
* Update docker compose files to version 3.9

* Format yml files

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-09-07 20:44:52 +02:00
9672de174e Feature/improve language localization for german 20230903 (#2294)
* Update locales

* Update changelog
2023-09-07 19:20:30 +02:00
c6aa06b933 Feature/improve import validation (#2305)
* Improve import validation

* Update changelog

Co-authored-by: httpiga <36515569+httpiga@users.noreply.github.com>
2023-09-07 18:28:47 +02:00
1f46a6b6f3 Clean up (#2291) 2023-09-05 19:44:45 +02:00
1bed940bc0 Feature/refresh cryptocurrencies list 20230903 (#2290)
* Refresh cryptocurrencies list

* Add CyberConnect

* Update changelog
2023-09-04 09:07:06 +02:00
f9eb3cc3c5 Release 1.305.0 (#2289) 2023-09-03 08:16:02 +02:00
2519c3ffb0 Bugfix/fix alignment in menu of impersonation mode (#2284)
* Fix alignment

* Update changelog
2023-09-02 17:57:50 +02:00
91013d1d10 Bugfix/fix alignment in header navigation (#2285)
* Fix alignment

* Update changelog
2023-09-02 17:13:13 +02:00
6deefb9c43 Update OSS Friends (#2282) 2023-09-02 11:52:29 +02:00
d0744e07df Feature/upgrade replace in file to version 7.0.1 (#2277)
* Upgrade replace-in-file to version 7.0.1

* Update changelog
2023-09-02 08:58:25 +02:00
93e1ee3ba7 Feature/improve localization of personal finance tools (#2274)
* Improve localization

* Update changelog
2023-09-02 08:58:10 +02:00
dceaa55a6c Feature/add hacker news logo to landing page (#2281)
* Add Hacker News

* Update changelog
2023-09-02 08:39:32 +02:00
8b4d55925d Feature/upgrade yahoo finance2 to version 2.4.4 (#2276)
* Upgrade yahoo-finance2 to version 2.4.4

* Update changelog
2023-09-02 08:36:46 +02:00
754b49e50f Feature/shorten page titles (#2273)
* Shorten page titles

* Update changelog
2023-08-31 18:22:22 +02:00
6ccbda8169 Feature/upgrade prisma to version 5.2.0 (#2217)
* Upgrade prisma to version 5.2.0

* Update changelog
2023-08-29 13:44:53 +02:00
b0fb986208 Release 1.304.0 (#2272) 2023-08-27 11:19:14 +02:00
0b59fc639d Feature/upgrade prettier to version 3 (#2163)
* Upgrade prettier to version 3.0.2

* Prettify code

* Update changelog
2023-08-27 11:13:11 +02:00
7ddd6f27b5 Feature/upgrade nx to version 16.7.4 (#2271)
* Upgrade Nx to version 16.7.4

* Update changelog
2023-08-27 10:44:06 +02:00
c5d56f4b47 Fix border (#2268) 2023-08-27 10:20:36 +02:00
2f2b712999 Fix breadcrumb (#2267) 2023-08-27 10:20:21 +02:00
c2fd31f5e5 Feature/add health check endpoints for data enhancers (#2265)
* Add health check for data enhancers

* Update changelog
2023-08-27 10:19:53 +02:00
f2d70f9070 Sort imports (#2266) 2023-08-26 11:22:19 +02:00
f41dd9cd8e Fix lint script (#2264) 2023-08-25 15:13:04 +02:00
7d238b4935 Release 1.303.0 (#2261) 2023-08-23 18:52:59 +02:00
da6591fca0 Bugfix/fix base url in trackinsight data enhancer (#2258)
* Fix base url

* Update changelog
2023-08-23 18:51:02 +02:00
1f9b9e9998 Feature/blog post ghostfolio joins oss friends (#2260)
* Add blog post: Ghostfolio joins OSS Friends

* Update changelog
2023-08-23 18:49:53 +02:00
49c4ea306d Feature/improve oss friends page (#2257)
* Improve OSS Friends page

* Update changelog
2023-08-22 09:02:39 +02:00
ccb5c664ef Feature/refresh cryptocurrencies list 20230821 (#2256)
* Update cryptocurrencies.json

* Update changelog
2023-08-21 20:20:59 +02:00
97e165ff69 Improve localization (#2254) 2023-08-21 18:05:08 +02:00
45aefb6a45 Reorder charts (#2253) 2023-08-21 18:04:49 +02:00
2435535975 Release 1.302.0 (#2252) 2023-08-20 10:35:13 +02:00
bd3d43bf05 Feature/upgrade nx to version 16.7.2 (#2251)
* Upgrade Nx and Angular dependencies

* Update changelog
2023-08-20 10:32:52 +02:00
02dc7c52b1 Localize routes (#2250)
* Localize about path

* Localize faq path

* Localize features path

* Localize markets path

* Localize pricing path

* Localize register path

* Localize resources path

* Extend sitemap
2023-08-20 10:01:40 +02:00
ff59fd4196 Feature/improve language localization for german 20230819 (#2249)
* Improve language localization

* Update changelog
2023-08-19 19:49:40 +02:00
4955555ddd Release 1.301.1 (#2247) 2023-08-19 08:49:31 +02:00
a98c788a26 Release 1.301.0 (#2246) 2023-08-18 20:46:58 +02:00
9c16af81c7 Feature/setup oss friends page (#2245)
* Setup OSS Friends page

* Update changelog
2023-08-18 20:45:10 +02:00
2df27100f0 Add middleware (#2239)
* Add middleware

* Update changelog
2023-08-18 20:27:19 +02:00
6cf6538719 Feature/add currencies preset to historical market data table (#2243)
* Add currencies preset

* Update locales

* Update changelog
2023-08-18 19:33:00 +02:00
0fd3db3228 Bugfix/fix cash position rows in holdings table (#2237)
* Fix cash position rows

* Update changelog
2023-08-17 20:23:23 +02:00
18835149e2 Add repository (#2189) 2023-08-16 20:32:54 +02:00
6c9779fb0d Bugfix/change date creation from string using parse iso (#2236)
* Change date creation using parseISO

parseISO provides consistent date parsing across different time zones

* Update changelog
2023-08-15 19:24:31 +02:00
3e98f097ef Refactor account page to user account page (#2235)
* Refactor account page to user account page
2023-08-13 09:24:54 +02:00
183ac8fa2b Feature/add data export to user account page (#2234)
* Add data export

* Update changelog
2023-08-12 21:51:35 +02:00
9036f53e7d Reset benchmark in user settings (#2233) 2023-08-12 21:50:01 +02:00
f7c04e469a Release 1.300.0 (#2232) 2023-08-11 20:24:23 +02:00
b5f01c0d15 Feature/migrate requests from bent to got (#2231)
* Migrate requests from bent to got

* Update changelog
2023-08-11 20:20:35 +02:00
5a23cd34ad Replace variables (#2229) 2023-08-11 18:29:39 +02:00
6e87f34c6f Feature/add more durations in coupon system (#2228)
* Add 90 and 180 days

* Update changelog
2023-08-10 20:49:06 +02:00
6618aa2e9b Release 1.299.1 (#2227) 2023-08-10 07:58:34 +02:00
0d25a96f7e Release 1.299.0 (#2225) 2023-08-09 20:59:52 +02:00
4f6d9d3a76 Feature/add timeout to eod historical data requests (#2222)
* Add timeout to requests using got

* Update changelog
2023-08-09 20:58:00 +02:00
928f6f0c45 Bugfix/fix historical data gathering interval for benchmarks with activity (#2221)
* Fix historical data gathering interval for asset profiles used as benchmarks having activities

* Update changelog
2023-08-09 20:43:03 +02:00
09e95ddcee Bugfix/fix editing of emergency fund (#2220)
* Fix editing of emergency fund

* Update changelog
2023-08-09 19:41:42 +02:00
2d003225bc Allow custom currency in activity import (#2215)
* Allow custom currency in activity import

* Extend import test files

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-08-08 20:00:55 +02:00
de93cabd69 Release 1.298.0 (#2214) 2023-08-06 09:13:04 +02:00
51489cca81 Feature/upgrade ng extract i18n merge to version 2.7.0 (#2155)
* Upgrade ng-extract-i18n-merge to version 2.7.0

* Update changelog
2023-08-06 09:10:14 +02:00
f7f4c3afb1 Feature/localize open startup page (#2213)
* Localize Open Startup page

* Update changelog
2023-08-06 09:09:21 +02:00
0821086e41 Bugfix/fix various styles after angular material 16 upgrade (#2212)
* Fix styles

* Update changelog
2023-08-06 08:52:45 +02:00
7a905fde63 Clean up (#2210) 2023-08-06 08:32:06 +02:00
d2882b1119 Clean up (#2206) 2023-08-06 08:31:48 +02:00
3a500598c5 Feature/upgrade nx to version 16.6.0 (#2211)
* Upgrade Nx to version 16.6.0

* Update changelog
2023-08-06 08:30:28 +02:00
204 changed files with 31942 additions and 13541 deletions

View File

@ -9,6 +9,7 @@
],
"attributeSort": "ASC",
"endOfLine": "auto",
"plugins": ["prettier-plugin-organize-attributes"],
"printWidth": 80,
"singleQuote": true,
"tabWidth": 2,

View File

@ -5,6 +5,132 @@ 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.0.0 - 2023-09-09
### Added
- Added support for the cryptocurrency _CyberConnect_
- Added a blog post: _Announcing Ghostfolio 2.0_
### Changed
- **Breaking Change**: Removed the deprecated environment variable `BASE_CURRENCY`
- Improved the validation in the activities import
- Deactivated _Internet Identity_ as a social login provider for the account registration
- Improved the language localization for German (`de`)
- Refreshed the cryptocurrencies list
- Changed the version in the `docker-compose` files from `3.7` to `3.9`
- Upgraded `yahoo-finance2` from version `2.4.4` to `2.5.0`
### Fixed
- Fixed an issue in the _Yahoo Finance_ data enhancer where countries and sectors have been removed
## 1.305.0 - 2023-09-03
### Added
- Added _Hacker News_ to the _As seen in_ section on the landing page
### Changed
- Shortened the page titles
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `4.16.2` to `5.2.0`
- Upgraded `replace-in-file` from version `6.3.5` to `7.0.1`
- Upgraded `yahoo-finance2` from version `2.4.3` to `2.4.4`
### Fixed
- Fixed the alignment in the header navigation
- Fixed the alignment in the menu of the impersonation mode
## 1.304.0 - 2023-08-27
### Added
- Added health check endpoints for data enhancers
### Changed
- Upgraded `Nx` from version `16.7.2` to `16.7.4`
- Upgraded `prettier` from version `2.8.4` to `3.0.2`
## 1.303.0 - 2023-08-23
### Added
- Added a blog post: _Ghostfolio joins OSS Friends_
### Changed
- Refreshed the cryptocurrencies list
- Improved the _OSS Friends_ page
### Fixed
- Fixed an issue with the _Trackinsight_ data enhancer for asset profile data
## 1.302.0 - 2023-08-20
### Changed
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `16.1.8` to `16.2.1`
- Upgraded `Nx` from version `16.6.0` to `16.7.2`
## 1.301.1 - 2023-08-19
### Added
- Added the data export feature to the user account page
- Added a currencies preset to the historical market data table of the admin control panel
- Added the _OSS Friends_ page
### Changed
- Improved the localized meta data in `html` files
### Fixed
- Fixed the rows with cash positions in the holdings table
- Fixed an issue with the date parsing in the historical market data editor of the admin control panel
## 1.300.0 - 2023-08-11
### Added
- Added more durations in the coupon system
### Changed
- Migrated the remaining requests from `bent` to `got`
## 1.299.1 - 2023-08-10
### Changed
- Optimized the activities import by allowing a different currency than the asset's official one
- Added a timeout to the _EOD Historical Data_ requests
- Migrated the requests from `bent` to `got` in the _EOD Historical Data_ service
### Fixed
- Fixed the editing of the emergency fund
- Fixed the historical data gathering interval for asset profiles used as benchmarks having activities
## 1.298.0 - 2023-08-06
### Changed
- Improved the language localization for German (`de`)
- Upgraded `ng-extract-i18n-merge` from version `2.6.0` to `2.7.0`
- Upgraded `Nx` from version `16.5.5` to `16.6.0`
### Fixed
- Fixed the styles of various components (card, progress, tab) after the upgrade to `@angular/material` `16`
## 1.297.4 - 2023-08-05
### Added
@ -576,7 +702,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Changed the slide toggles to checkboxes on the account page
- Changed the slide toggles to checkboxes on the user account page
- Changed the slide toggles to checkboxes in the admin control panel
- Increased the density of the theme
- Migrated the style of various components to `@angular/material` `15` (mdc)
@ -1138,7 +1264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the language selector on the account page
- Improved the language selector on the user account page
- Improved the wording in the _X-ray_ section (net worth instead of investment)
- Extended the asset profile details dialog in the admin control panel
- Updated the browserslist database
@ -1556,7 +1682,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a language selector to the account page
- Added a language selector to the user account page
- Added support for translated labels in the value component
### Changed
@ -1885,7 +2011,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the user id to the account page
- Added the user id to the user account page
- Added a new view with jobs of the queue to the admin control panel
### Changed
@ -3540,7 +3666,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Respected the cash balance on the analysis page
- Improved the settings selectors on the account page
- Improved the settings selectors on the user account page
- Harmonized the slogan to "Open Source Wealth Management Software"
### Fixed
@ -4006,7 +4132,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a gradient to the line charts
- Added a selector to set the base currency on the account page
- Added a selector to set the base currency on the user account page
## 0.81.0 - 06.04.2021
@ -4320,7 +4446,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added the membership status to the account page
- Added the membership status to the user account page
### Fixed

View File

@ -38,7 +38,7 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client';
import { isDate } from 'date-fns';
import { isDate, parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service';
@ -233,7 +233,7 @@ export class AdminController {
);
}
const date = new Date(dateString);
const date = parseISO(dateString);
if (!isDate(date)) {
throw new HttpException(
@ -333,7 +333,7 @@ export class AdminController {
);
}
const date = new Date(dateString);
const date = parseISO(dateString);
return this.marketDataService.updateMarketData({
data: { marketPrice: data.marketPrice, state: 'CLOSE' },

View File

@ -7,13 +7,14 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
DEFAULT_PAGE_SIZE,
DEFAULT_CURRENCY,
PROPERTY_CURRENCIES
} from '@ghostfolio/common/config';
import {
AdminData,
AdminMarketData,
AdminMarketDataDetails,
AdminMarketDataItem,
Filter,
UniqueAsset
} from '@ghostfolio/common/interfaces';
@ -25,8 +26,6 @@ import { groupBy } from 'lodash';
@Injectable()
export class AdminService {
private baseCurrency: string;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
@ -36,9 +35,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,
@ -82,15 +79,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
)
};
@ -121,7 +118,9 @@ export class AdminService {
[{ symbol: 'asc' }];
const where: Prisma.SymbolProfileWhereInput = {};
if (
if (presetId === 'CURRENCIES') {
return this.getMarketDataForCurrencies();
} else if (
presetId === 'ETF_WITHOUT_COUNTRIES' ||
presetId === 'ETF_WITHOUT_SECTORS'
) {
@ -313,6 +312,36 @@ export class AdminService {
return response;
}
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
const marketDataItems = await this.prismaService.marketData.groupBy({
_count: true,
by: ['dataSource', 'symbol']
});
const marketData: AdminMarketDataItem[] = this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
return {
dataSource,
marketDataItemCount,
symbol,
assetClass: 'CASH',
countriesCount: 0,
sectorsCount: 0
};
});
return { marketData, count: marketData.length };
}
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
let orderBy: any = {
createdAt: 'desc'

View File

@ -12,7 +12,7 @@ import {
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
@ -28,7 +28,6 @@ import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module';
import { FrontendMiddleware } from './frontend.middleware';
import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
@ -75,12 +74,6 @@ import { UserModule } from './user/user.module';
PrismaModule,
RedisCacheModule,
ScheduleModule.forRoot(),
...SUPPORTED_LANGUAGE_CODES.map((languageCode) => {
return ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client', languageCode),
serveRoot: `/${languageCode}`
});
}),
ServeStaticModule.forRoot({
exclude: ['/api*', '/sitemap.xml'],
rootPath: join(__dirname, '..', 'client'),
@ -114,10 +107,4 @@ import { UserModule } from './user/user.module';
controllers: [AppController],
providers: [CronService]
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(FrontendMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}
export class AppModule {}

View File

@ -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(

View File

@ -55,7 +55,7 @@ export class AuthService {
const isUserSignupEnabled =
await this.propertyService.isUserSignupEnabled();
if (!isUserSignupEnabled) {
if (!isUserSignupEnabled || true) {
throw new Error('Sign up forbidden');
}

View File

@ -7,6 +7,7 @@ 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';
@ -23,7 +24,7 @@ export class ExchangeRateController {
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString);
const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({
date,

View File

@ -1,232 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { format } from 'date-fns';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = '';
public indexHtmlEn = '';
public indexHtmlEs = '';
public indexHtmlFr = '';
public indexHtmlIt = '';
public indexHtmlNl = '';
public indexHtmlPt = '';
private static readonly DEFAULT_DESCRIPTION =
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
public constructor(
private readonly configurationService: ConfigurationService
) {
try {
this.indexHtmlDe = fs.readFileSync(
this.getPathOfIndexHtmlFile('de'),
'utf8'
);
this.indexHtmlEn = fs.readFileSync(
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8'
);
this.indexHtmlEs = fs.readFileSync(
this.getPathOfIndexHtmlFile('es'),
'utf8'
);
this.indexHtmlFr = fs.readFileSync(
this.getPathOfIndexHtmlFile('fr'),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
this.indexHtmlNl = fs.readFileSync(
this.getPathOfIndexHtmlFile('nl'),
'utf8'
);
this.indexHtmlPt = fs.readFileSync(
this.getPathOfIndexHtmlFile('pt'),
'utf8'
);
} catch {}
}
public use(request: Request, response: Response, next: NextFunction) {
const currentDate = format(new Date(), DATE_FORMAT);
let featureGraphicPath = 'assets/cover.png';
let title = 'Ghostfolio Open Source Wealth Management Software';
if (request.path.startsWith('/en/blog/2022/08/500-stars-on-github')) {
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
title = `500 Stars - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/10/hacktoberfest-2022')) {
featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png';
title = `Hacktoberfest 2022 - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/11/black-friday-2022')) {
featureGraphicPath = 'assets/images/blog/black-friday-2022.jpg';
title = `Black Friday 2022 - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances'
)
) {
featureGraphicPath = 'assets/images/blog/20221226.jpg';
title = `The importance of tracking your personal finances - ${title}`;
} else if (
request.path.startsWith(
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt'
)
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png';
title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`;
} else if (
request.path.startsWith('/en/blog/2023/02/ghostfolio-meets-umbrel')
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
title = `Ghostfolio meets Umbrel - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
)
) {
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
title = `Ghostfolio reaches 1000 Stars on GitHub - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio'
)
) {
featureGraphicPath = 'assets/images/blog/20230520.jpg';
title = `Unlock your Financial Potential with Ghostfolio - ${title}`;
} else if (
request.path.startsWith('/en/blog/2023/07/exploring-the-path-to-fire')
) {
featureGraphicPath = 'assets/images/blog/20230701.jpg';
title = `Exploring the Path to FIRE - ${title}`;
}
if (
request.path.startsWith('/api/') ||
this.isFileRequest(request.url) ||
!environment.production
) {
// Skip
next();
} else if (request.path === '/de' || request.path.startsWith('/de/')) {
response.send(
interpolate(this.indexHtmlDe, {
currentDate,
featureGraphicPath,
title,
description:
'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
languageCode: 'de',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/es' || request.path.startsWith('/es/')) {
response.send(
interpolate(this.indexHtmlEs, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
languageCode: 'es',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/fr' || request.path.startsWith('/fr/')) {
response.send(
interpolate(this.indexHtmlFr, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
languageCode: 'fr',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/it' || request.path.startsWith('/it/')) {
response.send(
interpolate(this.indexHtmlIt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
languageCode: 'it',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/nl' || request.path.startsWith('/nl/')) {
response.send(
interpolate(this.indexHtmlNl, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETFs of cryptocurrencies over meerdere platforms bij te houden.',
languageCode: 'nl',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/pt' || request.path.startsWith('/pt/')) {
response.send(
interpolate(this.indexHtmlPt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
languageCode: 'pt',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
response.send(
interpolate(this.indexHtmlEn, {
currentDate,
featureGraphicPath,
title,
description: FrontendMiddleware.DEFAULT_DESCRIPTION,
languageCode: DEFAULT_LANGUAGE_CODE,
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
}
}
private getPathOfIndexHtmlFile(aLocale: string) {
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
}
private isFileRequest(filename: string) {
if (filename === '/assets/LICENSE') {
return true;
} else if (
filename.includes('auth/ey') ||
filename.includes(
'personal-finance-tools/open-source-alternative-to-markets.sh'
)
) {
return false;
}
return filename.split('.').pop() !== filename;
}
}

View File

@ -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(

View File

@ -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 {}

View File

@ -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);
}

View File

@ -13,6 +13,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
parseDate
} from '@ghostfolio/common/helper';
@ -24,7 +25,7 @@ import {
import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import Big from 'big.js';
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
import { endOfToday, format, isAfter, isSameDay, parseISO } from 'date-fns';
import { uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
@ -248,7 +249,9 @@ export class ImportService {
const activities: Activity[] = [];
for (const {
for (let [
index,
{
accountId,
comment,
date,
@ -258,7 +261,8 @@ export class ImportService {
SymbolProfile,
type,
unitPrice
} of activitiesExtendedWithErrors) {
}
] of activitiesExtendedWithErrors.entries()) {
const assetProfile = assetProfiles[
getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource,
@ -296,6 +300,35 @@ export class ImportService {
Account?: { id: string; name: string };
});
if (SymbolProfile.currency !== assetProfile.currency) {
// Convert the unit price and fee to the asset currency if the imported
// activity is in a different currency
unitPrice = await this.exchangeRateDataService.toCurrencyAtDate(
unitPrice,
SymbolProfile.currency,
assetProfile.currency,
date
);
if (!unitPrice) {
throw new Error(
`activities.${index} historical exchange rate at ${format(
date,
DATE_FORMAT
)} is not available from "${SymbolProfile.currency}" to "${
assetProfile.currency
}"`
);
}
fee = await this.exchangeRateDataService.toCurrencyAtDate(
fee,
SymbolProfile.currency,
assetProfile.currency,
date
);
}
if (isDryRun) {
order = {
comment,
@ -533,15 +566,21 @@ export class ImportService {
])
)?.[symbol];
if (assetProfile === undefined) {
if (!assetProfile?.name) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
);
}
if (assetProfile.currency !== currency) {
if (
assetProfile.currency !== currency &&
!this.exchangeRateDataService.hasCurrencyPair(
currency,
assetProfile.currency
)
) {
throw new Error(
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}"`
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
);
}

View File

@ -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]
})

View File

@ -1,12 +1,13 @@
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,
PROPERTY_BETTER_UPTIME_MONITOR_ID,
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
PROPERTY_DEMO_USER_ID,
@ -30,9 +31,9 @@ import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOffer } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bent from 'bent';
import * as cheerio from 'cheerio';
import { format, subDays } from 'date-fns';
import got from 'got';
@Injectable()
export class InfoService {
@ -44,10 +45,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 +140,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,17 +168,13 @@ export class InfoService {
private async countDockerHubPulls(): Promise<number> {
try {
const get = bent(
const { pull_count } = await got(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{
'User-Agent': 'request'
headers: { 'User-Agent': 'request' }
}
);
).json<any>();
const { pull_count } = await get();
return pull_count;
} catch (error) {
Logger.error(error, 'InfoService');
@ -193,16 +185,9 @@ export class InfoService {
private async countGitHubContributors(): Promise<number> {
try {
const get = bent(
'https://github.com/ghostfolio/ghostfolio',
'GET',
'string',
200,
{}
);
const { body } = await got('https://github.com/ghostfolio/ghostfolio');
const html = await get();
const $ = cheerio.load(html);
const $ = cheerio.load(body);
return extractNumberFromString(
$(
@ -218,17 +203,13 @@ export class InfoService {
private async countGitHubStargazers(): Promise<number> {
try {
const get = bent(
const { stargazers_count } = await got(
`https://api.github.com/repos/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{
'User-Agent': 'request'
headers: { 'User-Agent': 'request' }
}
);
).json<any>();
const { stargazers_count } = await get();
return stargazers_count;
} catch (error) {
Logger.error(error, 'InfoService');
@ -238,10 +219,7 @@ export class InfoService {
}
private async countNewUsers(aDays: number) {
return await this.prismaService.user.count({
orderBy: {
createdAt: 'desc'
},
return this.userService.count({
where: {
AND: [
{
@ -332,11 +310,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> {
@ -346,22 +323,21 @@ export class InfoService {
PROPERTY_BETTER_UPTIME_MONITOR_ID
)) as string;
const get = bent(
const { data } = await got(
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90),
DATE_FORMAT
)}&to${format(new Date(), DATE_FORMAT)}`,
'GET',
'json',
200,
{
headers: {
Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY'
)}`
}
);
}
).json<any>();
const { data } = await get();
return data.attributes.availability / 100;
} catch (error) {
Logger.error(error, 'InfoService');

View File

@ -2,7 +2,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/sy
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import * as bent from 'bent';
import got from 'got';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable()
@ -41,15 +41,11 @@ export class LogoService {
}
private getBuffer(aUrl: string) {
const get = bent(
return got(
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
'GET',
'buffer',
200,
{
'User-Agent': 'request'
headers: { 'User-Agent': 'request' }
}
);
return get();
).buffer();
}
}

View File

@ -105,7 +105,6 @@ describe('CurrentRateService', () => {
null,
null,
null,
null,
null
);
marketDataService = new MarketDataService(null);

View File

@ -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);

View File

@ -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
);
}

View File

@ -10,7 +10,7 @@ import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
import { format } from 'date-fns';
import { Response } from 'express';
@Controller('/sitemap.xml')
@Controller('sitemap.xml')
export class SitemapController {
public sitemapXml = '';

View File

@ -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,

View File

@ -15,6 +15,7 @@ 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';
@ -93,7 +94,7 @@ export class SymbolController {
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString);
const date = parseISO(dateString);
if (!isDate(date)) {
throw new HttpException(

View File

@ -4,7 +4,11 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
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 { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
import {
DEFAULT_CURRENCY,
PROPERTY_IS_READ_ONLY_MODE,
locale
} from '@ghostfolio/common/config';
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
import {
getPermissions,
@ -21,18 +25,16 @@ const crypto = require('crypto');
@Injectable()
export class UserService {
public static DEFAULT_CURRENCY = 'USD';
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(
@ -145,8 +147,7 @@ export class UserService {
// Set default value for base currency
if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
(user.Settings.settings as UserSettings).baseCurrency =
UserService.DEFAULT_CURRENCY;
(user.Settings.settings as UserSettings).baseCurrency = DEFAULT_CURRENCY;
}
// Set default value for date range
@ -186,6 +187,9 @@ export class UserService {
if (Analytics?.activityCount % frequency === 1) {
currentPermissions.push(permissions.enableSubscriptionInterstitial);
}
// Reset benchmark
user.Settings.settings.benchmark = undefined;
}
if (user.subscription?.type === 'Premium') {
@ -263,7 +267,7 @@ export class UserService {
...data,
Account: {
create: {
currency: this.baseCurrency,
currency: DEFAULT_CURRENCY,
isDefault: true,
name: 'Default Account'
}
@ -271,7 +275,7 @@ export class UserService {
Settings: {
create: {
settings: {
currency: this.baseCurrency
currency: DEFAULT_CURRENCY
}
}
}

View File

@ -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 Peoples 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",

View File

@ -1,4 +1,5 @@
{
"CYBER24781": "CyberConnect",
"LUNA1": "Terra",
"LUNA2": "Terra",
"SGB1": "Songbird",

View File

@ -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>
@ -66,6 +170,10 @@
<loc>https://ghostfol.io/de/ueber-uns/lizenz</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ueber-uns/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -82,6 +190,10 @@
<loc>https://ghostfol.io/en/about/license</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/about/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -134,6 +246,14 @@
<loc>https://ghostfol.io/en/blog/2023/07/exploring-the-path-to-fire</loc>
<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>
@ -314,6 +434,10 @@
<loc>https://ghostfol.io/es/sobre/licencia</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/es/sobre/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -334,6 +458,10 @@
<loc>https://ghostfol.io/fr/a-propos/licence</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/fr/a-propos/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -388,12 +516,16 @@
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/informazioni-su/licenza</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
<loc>https://ghostfol.io/it/informazioni-su/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
@ -452,6 +584,10 @@
<loc>https://ghostfol.io/nl/over/licentie</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/over/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/over/privacybeleid</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -468,6 +604,10 @@
<loc>https://ghostfol.io/nl/vaak-gestelde-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>
<loc>https://ghostfol.io/pt/blog</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -512,6 +652,10 @@
<loc>https://ghostfol.io/pt/sobre/licenca</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/pt/sobre/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -7,6 +7,7 @@ import helmet from 'helmet';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware';
async function bootstrap() {
const configApp = await NestFactory.create(AppModule);
@ -40,6 +41,7 @@ async function bootstrap() {
helmet({
contentSecurityPolicy: {
directives: {
connectSrc: ["'self'", 'https://js.stripe.com'], // Allow connections to Stripe
frameSrc: ["'self'", 'https://js.stripe.com'], // Allow loading frames from Stripe
scriptSrc: ["'self'", "'unsafe-inline'", 'https://js.stripe.com'], // Allow inline scripts and scripts from Stripe
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
@ -51,7 +53,8 @@ async function bootstrap() {
);
}
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
app.use(HtmlTemplateMiddleware);
const HOST = configService.get<string>('HOST') || '0.0.0.0';
const PORT = configService.get<number>('PORT') || 3333;
@ -59,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.'
);
}
});
}

View File

@ -0,0 +1,136 @@
import * as fs from 'fs';
import { join } from 'path';
import { environment } from '@ghostfolio/api/environments/environment';
import {
DEFAULT_LANGUAGE_CODE,
DEFAULT_ROOT_URL,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
import { format } from 'date-fns';
import { NextFunction, Request, Response } from 'express';
const descriptions = {
de: 'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
en: 'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.',
es: 'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
fr: 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
it: 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
nl: 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETFs of cryptocurrencies over meerdere platforms bij te houden.',
pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.'
};
const title = 'Ghostfolio Open Source Wealth Management Software';
const titleShort = 'Ghostfolio';
let indexHtmlMap: { [languageCode: string]: string } = {};
try {
indexHtmlMap = SUPPORTED_LANGUAGE_CODES.reduce(
(map, languageCode) => ({
...map,
[languageCode]: fs.readFileSync(
join(__dirname, '..', 'client', languageCode, 'index.html'),
'utf8'
)
}),
{}
);
} catch {}
const locales = {
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt': {
featureGraphicPath: 'assets/images/blog/ghostfolio-x-sackgeld.png',
title: `Ghostfolio auf Sackgeld.com vorgestellt - ${titleShort}`
},
'/en/blog/2022/08/500-stars-on-github': {
featureGraphicPath: 'assets/images/blog/500-stars-on-github.jpg',
title: `500 Stars - ${titleShort}`
},
'/en/blog/2022/10/hacktoberfest-2022': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2022.png',
title: `Hacktoberfest 2022 - ${titleShort}`
},
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances': {
featureGraphicPath: 'assets/images/blog/20221226.jpg',
title: `The importance of tracking your personal finances - ${titleShort}`
},
'/en/blog/2023/02/ghostfolio-meets-umbrel': {
featureGraphicPath: 'assets/images/blog/ghostfolio-x-umbrel.png',
title: `Ghostfolio meets Umbrel - ${titleShort}`
},
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github': {
featureGraphicPath: 'assets/images/blog/1000-stars-on-github.jpg',
title: `Ghostfolio reaches 1000 Stars on GitHub - ${titleShort}`
},
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio': {
featureGraphicPath: 'assets/images/blog/20230520.jpg',
title: `Unlock your Financial Potential with Ghostfolio - ${titleShort}`
},
'/en/blog/2023/07/exploring-the-path-to-fire': {
featureGraphicPath: 'assets/images/blog/20230701.jpg',
title: `Exploring the Path to FIRE - ${titleShort}`
},
'/en/blog/2023/08/ghostfolio-joins-oss-friends': {
featureGraphicPath: 'assets/images/blog/ghostfolio-joins-oss-friends.png',
title: `Ghostfolio joins OSS Friends - ${titleShort}`
},
'/en/blog/2023/09/ghostfolio-2': {
featureGraphicPath: 'assets/images/blog/ghostfolio-2.jpg',
title: `Announcing Ghostfolio 2.0 - ${titleShort}`
}
};
const isFileRequest = (filename: string) => {
if (filename === '/assets/LICENSE') {
return true;
} else if (
filename.includes('auth/ey') ||
filename.includes(
'personal-finance-tools/open-source-alternative-to-markets.sh'
)
) {
return false;
}
return filename.split('.').pop() !== filename;
};
export const HtmlTemplateMiddleware = async (
request: Request,
response: Response,
next: NextFunction
) => {
const path = request.originalUrl.replace(/\/$/, '');
let languageCode = path.substr(1, 2);
if (!SUPPORTED_LANGUAGE_CODES.includes(languageCode)) {
languageCode = DEFAULT_LANGUAGE_CODE;
}
const currentDate = format(new Date(), DATE_FORMAT);
const rootUrl = process.env.ROOT_URL || DEFAULT_ROOT_URL;
if (
path.startsWith('/api/') ||
isFileRequest(path) ||
!environment.production
) {
// Skip
next();
} else {
const indexHtml = interpolate(indexHtmlMap[languageCode], {
currentDate,
languageCode,
path,
rootUrl,
description: descriptions[languageCode],
featureGraphicPath:
locales[path]?.featureGraphicPath ?? 'assets/cover.png',
title: locales[path]?.title ?? title
});
return response.send(indexHtml);
}
};

View File

@ -1,4 +1,5 @@
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
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';
@ -11,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: 'USD'
}),
BETTER_UPTIME_API_KEY: str({ default: '' }),
CACHE_QUOTES_TTL: num({ default: 1 }),
CACHE_TTL: num({ default: 1 }),
@ -46,7 +43,7 @@ export class ConfigurationService {
REDIS_HOST: str({ default: 'localhost' }),
REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port({ default: 6379 }),
ROOT_URL: str({ default: 'http://localhost:4200' }),
ROOT_URL: str({ default: DEFAULT_ROOT_URL }),
STRIPE_PUBLIC_KEY: str({ default: '' }),
STRIPE_SECRET_KEY: str({ default: '' }),
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),

View File

@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull';
@ -28,6 +29,7 @@ import { DataGatheringProcessor } from './data-gathering.processor';
ExchangeRateDataModule,
MarketDataModule,
PrismaModule,
PropertyModule,
SymbolProfileModule
],
providers: [DataGatheringProcessor, DataGatheringService],

View File

@ -4,18 +4,20 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
DATA_GATHERING_QUEUE,
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
PROPERTY_BENCHMARKS
} from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
resetHours
} from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { BenchmarkProperty, UniqueAsset } from '@ghostfolio/common/interfaces';
import { InjectQueue } from '@nestjs/bull';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@ -34,6 +36,7 @@ export class DataGatheringService {
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -124,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) => {
@ -255,6 +256,10 @@ export class DataGatheringService {
});
}
private getEarliestDate(aStartDate: Date) {
return min([aStartDate, subYears(new Date(), 10)]);
}
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
const startDate = subDays(resetHours(new Date()), 7);
@ -321,6 +326,14 @@ export class DataGatheringService {
}
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
const benchmarkAssetProfileIdMap: { [key: string]: boolean } = {};
(
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? []
).forEach(({ symbolProfileId }) => {
benchmarkAssetProfileIdMap[symbolProfileId] = true;
});
const startDate =
(
await this.prismaService.order.findFirst({
@ -334,7 +347,7 @@ export class DataGatheringService {
return {
dataSource,
symbol,
date: min([startDate, subYears(new Date(), 10)])
date: this.getEarliestDate(startDate)
};
});
@ -343,6 +356,7 @@ export class DataGatheringService {
orderBy: [{ symbol: 'asc' }],
select: {
dataSource: true,
id: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
@ -364,9 +378,15 @@ export class DataGatheringService {
);
})
.map((symbolProfile) => {
let date = symbolProfile.Order?.[0]?.date ?? startDate;
if (benchmarkAssetProfileIdMap[symbolProfile.id]) {
date = this.getEarliestDate(startDate);
}
return {
...symbolProfile,
date: symbolProfile.Order?.[0]?.date ?? startDate
date
};
});

View File

@ -1,10 +1,10 @@
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 } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types';
@ -15,19 +15,14 @@ import {
DataSource,
SymbolProfile
} from '@prisma/client';
import bent from 'bent';
import { format, fromUnixTime, getUnixTime } from 'date-fns';
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,14 +34,13 @@ 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 get = bent(`${this.URL}/coins/${aSymbol}`, 'GET', 'json', 200);
const { name } = await get();
const { name } = await got(`${this.URL}/coins/${aSymbol}`).json<any>();
response.name = name;
} catch (error) {
@ -79,17 +73,13 @@ export class CoinGeckoService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> {
try {
const get = bent(
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)}`,
'GET',
'json',
200
);
const { prices } = await get();
)}&to=${getUnixTime(to)}`
).json<any>();
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@ -132,23 +122,19 @@ export class CoinGeckoService implements DataProviderInterface {
}
try {
const get = bent(
const response = await got(
`${this.URL}/simple/price?ids=${aSymbols.join(
','
)}&vs_currencies=${this.baseCurrency.toLowerCase()}`,
'GET',
'json',
200
);
const response = await get();
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`
).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'
};
}
@ -174,8 +160,9 @@ export class CoinGeckoService implements DataProviderInterface {
let items: LookupItem[] = [];
try {
const get = bent(`${this.URL}/search?query=${query}`, 'GET', 'json', 200);
const { coins } = await get();
const { coins } = await got(
`${this.URL}/search?query=${query}`
).json<any>();
items = coins.map(({ id: symbol, name }) => {
return {
@ -183,7 +170,7 @@ export class CoinGeckoService implements DataProviderInterface {
symbol,
assetClass: AssetClass.CASH,
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
currency: this.baseCurrency,
currency: DEFAULT_CURRENCY,
dataSource: this.getName()
};
});

View File

@ -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,
{

View File

@ -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;
}
}

View File

@ -3,13 +3,11 @@ import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import bent from 'bent';
const getJSON = bent('json');
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'
@ -34,26 +32,42 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response;
}
const profile = await getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
).catch(() => {
const profile = await got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`
)
.json<any>()
.catch(() => {
return got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
'.'
)?.[0]}.json`
)
.json<any>()
.catch(() => {
return {};
});
});
const isin = profile.isin?.split(';')?.[0];
const isin = profile?.isin?.split(';')?.[0];
if (isin) {
response.isin = isin;
}
const holdings = await getJSON(
const holdings = await got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
).catch(() => {
return getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
symbol.split('.')?.[0]
}.json`
);
)
.json<any>()
.catch(() => {
return got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
'.'
)?.[0]}.json`
)
.json<any>()
.catch(() => {
return {};
});
});
if (holdings?.weight < 0.95) {
@ -112,4 +126,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
public getName() {
return 'TRACKINSIGHT';
}
public getTestSymbol() {
return 'QQQ';
}
}

View File

@ -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
);
});

View File

@ -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

View File

@ -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, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
@ -14,21 +18,19 @@ import {
DataSource,
SymbolProfile
} from '@prisma/client';
import bent from 'bent';
import Big from 'big.js';
import { format, isToday } from 'date-fns';
import got from 'got';
@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) {
@ -76,19 +78,19 @@ export class EodHistoricalDataService implements DataProviderInterface {
const symbol = this.convertToEodSymbol(aSymbol);
try {
const get = bent(
const response = await got(
`${this.URL}/eod/${symbol}?api_token=${
this.apiKey
}&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format(
to,
DATE_FORMAT
)}&period={aGranularity}`,
'GET',
'json',
200
);
const response = await get();
{
timeout: {
request: DEFAULT_REQUEST_TIMEOUT
}
}
).json<any>();
return response.reduce(
(result, historicalItem, index, array) => {
@ -136,16 +138,16 @@ export class EodHistoricalDataService implements DataProviderInterface {
}
try {
const get = bent(
const realTimeResponse = await got(
`${this.URL}/real-time/${symbols[0]}?api_token=${
this.apiKey
}&fmt=json&s=${symbols.join(',')}`,
'GET',
'json',
200
);
const realTimeResponse = await get();
{
timeout: {
request: DEFAULT_REQUEST_TIMEOUT
}
}
).json<any>();
const quotes =
symbols.length === 1 ? [realTimeResponse] : realTimeResponse;
@ -174,7 +176,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'
@ -185,24 +187,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
})
};
}
@ -271,7 +273,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;
@ -284,17 +286,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`;
}
}
@ -308,10 +310,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();
}
@ -329,13 +331,14 @@ export class EodHistoricalDataService implements DataProviderInterface {
let searchResult = [];
try {
const get = bent(
const response = await got(
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
'GET',
'json',
200
);
const response = await get();
{
timeout: {
request: DEFAULT_REQUEST_TIMEOUT
}
}
).json<any>();
searchResult = response.map(
({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {

View File

@ -5,18 +5,18 @@ import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import bent from 'bent';
import { format, isAfter, isBefore, isSameDay } from 'date-fns';
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 +25,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,13 +63,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> {
try {
const get = bent(
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
'GET',
'json',
200
);
const { historical } = await get();
const { historical } = await got(
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`
).json<any>();
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@ -115,17 +110,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
try {
const get = bent(
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
'GET',
'json',
200
);
const response = await get();
const response = await got(
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`
).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,
@ -153,13 +144,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let items: LookupItem[] = [];
try {
const get = bent(
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
'GET',
'json',
200
);
const result = await get();
const result = await got(
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`
).json<any>();
items = result.map(({ currency, name, symbol }) => {
return {

View File

@ -10,4 +10,6 @@ export interface DataEnhancerInterface {
}): Promise<Partial<SymbolProfile>>;
getName(): string;
getTestSymbol(): string;
}

View File

@ -14,10 +14,10 @@ import {
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import bent from 'bent';
import * as cheerio from 'cheerio';
import { isUUID } from 'class-validator';
import { addDays, format, isBefore } from 'date-fns';
import got from 'got';
@Injectable()
export class ManualService implements DataProviderInterface {
@ -95,10 +95,9 @@ export class ManualService implements DataProviderInterface {
return {};
}
const get = bent(url, 'GET', 'string', 200, headers);
const { body } = await got(url, { headers });
const html = await get();
const $ = cheerio.load(html);
const $ = cheerio.load(body);
const value = extractNumberFromString($(selector).text());

View File

@ -10,8 +10,8 @@ import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import bent from 'bent';
import { format } from 'date-fns';
import got from 'got';
@Injectable()
export class RapidApiService implements DataProviderInterface {
@ -135,19 +135,17 @@ export class RapidApiService implements DataProviderInterface {
oneYearAgo: { value: number; valueText: string };
}> {
try {
const get = bent(
const { fgi } = await got(
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
'GET',
'json',
200,
{
useQueryString: true,
headers: {
useQueryString: 'true',
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
}
);
}
).json<any>();
const { fgi } = await get();
return fgi;
} catch (error) {
Logger.error(error, 'RapidApiService');

View File

@ -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();
}

View File

@ -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,15 +26,23 @@ 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() {
return this.currencyPairs;
}
public hasCurrencyPair(currency1: string, currency2: string) {
return this.currencyPairs.some(({ symbol }) => {
return (
symbol === `${currency1}${currency2}` ||
symbol === `${currency2}${currency1}`
);
});
}
public async initialize() {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
this.currencies = await this.prepareCurrencies();
this.currencyPairs = [];
this.exchangeRates = {};
@ -104,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
@ -135,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;
@ -195,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;
}
@ -286,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}`
};
});
}

View File

@ -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;

View File

@ -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}`,

View File

@ -4,25 +4,29 @@ import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strate
import { ModulePreloadService } from './core/module-preload.service';
export const paths = {
about: $localize`about`,
faq: $localize`faq`,
features: $localize`features`,
license: $localize`license`,
markets: $localize`markets`,
pricing: $localize`pricing`,
privacyPolicy: $localize`privacy-policy`,
register: $localize`register`,
resources: $localize`resources`
};
const routes: Routes = [
...[
'about',
/////
'a-propos',
'informazioni-su',
'over',
'sobre',
'ueber-uns'
].map((path) => ({
path,
{
path: paths.about,
loadChildren: () =>
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
})),
},
{
path: 'account',
loadChildren: () =>
import('./pages/account/account-page.module').then(
(m) => m.AccountPageModule
import('./pages/user-account/user-account-page.module').then(
(m) => m.UserAccountPageModule
)
},
{
@ -42,64 +46,40 @@ const routes: Routes = [
loadChildren: () =>
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
},
...['blog'].map((path) => ({
path,
{
path: 'blog',
loadChildren: () =>
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
})),
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
...[
'faq',
/////
'domande-piu-frequenti',
'foire-aux-questions',
'haeufig-gestellte-fragen',
'perguntas-mais-frequentes',
'preguntas-mas-frecuentes',
'vaak-gestelde-vragen'
].map((path) => ({
path,
{
path: paths.faq,
loadChildren: () =>
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
})),
...[
'features',
/////
'fonctionnalites',
'funcionalidades',
'funzionalita',
'kenmerken'
].map((path) => ({
path,
},
{
path: paths.features,
loadChildren: () =>
import('./pages/features/features-page.module').then(
(m) => m.FeaturesPageModule
)
})),
},
{
path: 'home',
loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
},
...[
'markets',
/////
'maerkte',
'marches',
'markten',
'mercados',
'mercati'
].map((path) => ({
path,
{
path: paths.markets,
loadChildren: () =>
import('./pages/markets/markets-page.module').then(
(m) => m.MarketsPageModule
)
})),
},
{
path: 'open',
loadChildren: () =>
@ -119,53 +99,27 @@ const routes: Routes = [
(m) => m.PortfolioPageModule
)
},
...[
'pricing',
/////
'precios',
'precos',
'preise',
'prezzi',
'prijzen',
'prix'
].map((path) => ({
path,
{
path: paths.pricing,
loadChildren: () =>
import('./pages/pricing/pricing-page.module').then(
(m) => m.PricingPageModule
)
})),
...[
'register',
/////
'enregistrement',
'iscrizione',
'registo',
'registratie',
'registrierung',
'registro'
].map((path) => ({
path,
},
{
path: paths.register,
loadChildren: () =>
import('./pages/register/register-page.module').then(
(m) => m.RegisterPageModule
)
})),
...[
'resources',
/////
'bronnen',
'recursos',
'ressourcen',
'ressources',
'risorse'
].map((path) => ({
path,
},
{
path: paths.resources,
loadChildren: () =>
import('./pages/resources/resources-page.module').then(
(m) => m.ResourcesPageModule
)
})),
},
{
path: 'start',
loadChildren: () =>

View File

@ -19,7 +19,7 @@
<a
*ngIf="canCreateAccount"
class="text-center"
[routerLink]="['/register']"
[routerLink]="routerLinkRegister"
>
<div
class="cursor-pointer d-inline-block info-message px-3 py-2"
@ -43,22 +43,7 @@
<router-outlet></router-outlet>
</main>
<footer
*ngIf="
(currentRoute === 'blog' ||
currentRoute === 'faq' ||
currentRoute === 'features' ||
currentRoute === 'markets' ||
currentRoute === 'open' ||
currentRoute === 'p' ||
currentRoute === 'pricing' ||
currentRoute === 'resources' ||
currentRoute === 'register' ||
currentRoute === 'start') &&
deviceType !== 'mobile'
"
class="d-flex justify-content-center py-4 w-100"
>
<footer *ngIf="showFooter" class="d-flex justify-content-center py-4 w-100">
<div class="container">
<div class="mb-3 row">
<div class="col-sm">
@ -68,36 +53,38 @@
<div class="h6 mt-2" i18n>Personal Finance</div>
<ul class="list-unstyled">
<li *ngIf="hasPermissionToAccessFearAndGreedIndex">
<a i18n [routerLink]="['/markets']">Markets</a>
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
</li>
<li><a i18n [routerLink]="['/resources']">Resources</a></li>
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
</ul>
</div>
<div class="col-sm">
<div class="h6 mt-2">Ghostfolio</div>
<ul class="list-unstyled">
<li><a i18n [routerLink]="['/about']">About</a></li>
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
<li *ngIf="hasPermissionForBlog">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li>
<a i18n [routerLink]="['/about', 'changelog']">Changelog</a>
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
</li>
<li><a i18n [routerLink]="['/features']">Features</a></li>
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
<li *ngIf="hasPermissionForSubscription">
<a i18n [routerLink]="['/faq']">Frequently Asked Questions (FAQ)</a>
<a i18n [routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a
>
</li>
<li>
<a i18n [routerLink]="['/about', 'license']">License</a>
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
</li>
<li *ngIf="hasPermissionForStatistics">
<a [routerLink]="['/open']">Open Startup</a>
</li>
<li *ngIf="hasPermissionForSubscription">
<a i18n [routerLink]="['/pricing']">Pricing</a>
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
</li>
<li *ngIf="hasPermissionForSubscription">
<a i18n [routerLink]="['/about', 'privacy-policy']"
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
>Privacy Policy</a
>
</li>

View File

@ -38,6 +38,20 @@ export class AppComponent implements OnDestroy, OnInit {
public hasPermissionToAccessFearAndGreedIndex: boolean;
public info: InfoItem;
public pageTitle: string;
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkAboutChangelog = ['/' + $localize`about`, 'changelog'];
public routerLinkAboutLicense = ['/' + $localize`about`, $localize`license`];
public routerLinkAboutPrivacyPolicy = [
'/' + $localize`about`,
$localize`privacy-policy`
];
public routerLinkFaq = ['/' + $localize`faq`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkRegister = ['/' + $localize`register`];
public routerLinkResources = ['/' + $localize`resources`];
public showFooter = false;
public user: User;
public version = environment.version;
@ -89,6 +103,19 @@ export class AppComponent implements OnDestroy, OnInit {
const urlSegments = urlSegmentGroup.segments;
this.currentRoute = urlSegments[0].path;
this.showFooter =
(this.currentRoute === 'blog' ||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
this.currentRoute === this.routerLinkFeatures[0].slice(1) ||
this.currentRoute === this.routerLinkMarkets[0].slice(1) ||
this.currentRoute === 'open' ||
this.currentRoute === 'p' ||
this.currentRoute === this.routerLinkPricing[0].slice(1) ||
this.currentRoute === this.routerLinkRegister[0].slice(1) ||
this.currentRoute === this.routerLinkResources[0].slice(1) ||
this.currentRoute === 'start') &&
this.deviceType !== 'mobile';
if (this.deviceType === 'mobile') {
setTimeout(() => {
const index = this.title.getTitle().indexOf('');

View File

@ -154,7 +154,7 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
day: string;
yearMonth: string;
}) {
const date = new Date(`${yearMonth}-${day}`);
const date = parseISO(`${yearMonth}-${day}`);
const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice;
if (isSameDay(date, new Date())) {

View File

@ -60,6 +60,11 @@ export class AdminMarketDataComponent
};
})
.concat([
{
id: 'CURRENCIES',
label: $localize`Currencies`,
type: <Filter['type']>'PRESET_ID'
},
{
id: 'ETF_WITHOUT_COUNTRIES',
label: $localize`ETFs without Countries`,

View File

@ -169,6 +169,8 @@
<mat-option value="7 days">7 Days</mat-option>
<mat-option value="14 days">14 Days</mat-option>
<mat-option value="30 days">30 Days</mat-option>
<mat-option value="90 days">90 Days</mat-option>
<mat-option value="180 days">180 Days</mat-option>
<mat-option value="1 year">1 Year</mat-option>
</mat-select>
</mat-form-field>

View File

@ -1,14 +1,14 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h3 class="text-center" i18n>Platforms</h3>
<h2 class="text-center" i18n>Platforms</h2>
<gf-admin-platform></gf-admin-platform>
</div>
</div>
<!--
<div class="row">
<div class="col">
<h3 class="text-center" i18n>Tags</h3>
<h2 class="text-center" i18n>Tags</h2>
</div>
</div>
-->

View File

@ -26,7 +26,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 +39,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 +52,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,14 +65,14 @@
</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]="{
'font-weight-bold': currentRoute === 'resources',
'text-decoration-underline': currentRoute === 'resources'
'font-weight-bold': currentRoute === routeResources,
'text-decoration-underline': currentRoute === routeResources
}"
[routerLink]="['/resources']"
[routerLink]="routerLinkResources"
>Resources</a
>
</li>
@ -83,27 +83,27 @@
class="list-inline-item"
>
<a
class="d-none d-sm-block mx-1"
class="d-none d-sm-block"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'pricing',
'text-decoration-underline': currentRoute === 'pricing'
'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === routePricing
}"
[routerLink]="['/pricing']"
[routerLink]="routerLinkPricing"
>Pricing</a
>
</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]="{
'font-weight-bold': currentRoute === 'about',
'text-decoration-underline': currentRoute === 'about'
'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === routeAbout
}"
[routerLink]="['/about']"
[routerLink]="routerLinkAbout"
>About</a
>
</li>
@ -129,6 +129,7 @@
<mat-menu #accountMenu="matMenu" xPosition="before">
<ng-container *ngIf="user?.access?.length > 0">
<button mat-menu-item (click)="impersonateAccount(null)">
<span class="align-items-center d-flex">
<ion-icon
*ngIf="user?.access?.length > 0"
class="mr-2"
@ -139,12 +140,14 @@
"
></ion-icon>
<span i18n>Me</span>
</span>
</button>
<button
*ngFor="let accessItem of user?.access"
mat-menu-item
(click)="impersonateAccount(accessItem.id)"
>
<span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="square-outline"
@ -156,6 +159,7 @@
></ion-icon>
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
<span *ngIf="!accessItem.alias" i18n>User</span>
</span>
</button>
<hr class="m-0" />
</ng-container>
@ -210,9 +214,9 @@
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === 'resources'
'font-weight-bold': currentRoute === routeResources
}"
[routerLink]="['/resources']"
[routerLink]="routerLinkResources"
>Resources</a
>
<a
@ -223,16 +227,16 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'pricing' }"
[routerLink]="['/pricing']"
[ngClass]="{ 'font-weight-bold': currentRoute === routePricing }"
[routerLink]="routerLinkPricing"
>Pricing</a
>
<a
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }"
[routerLink]="['/about']"
[ngClass]="{ 'font-weight-bold': currentRoute === routeAbout }"
[routerLink]="routerLinkAbout"
>About Ghostfolio</a
>
<hr class="d-flex d-sm-none m-0" />
@ -256,39 +260,40 @@
<ul class="alig-items-center d-flex list-inline m-0">
<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]="{
'font-weight-bold': currentRoute === 'features',
'text-decoration-underline': currentRoute === 'features'
'font-weight-bold': currentRoute === routeFeatures,
'text-decoration-underline': currentRoute === routeFeatuers
}"
[routerLink]="['/features']"
[routerLink]="routerLinkFeatures"
>Features</a
>
</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]="{
'font-weight-bold': currentRoute === 'about',
'text-decoration-underline': currentRoute === 'about'
'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === routeAbout
}"
[routerLink]="['/about']"
[routerLink]="routerLinkAbout"
>About</a
>
</li>
<li *ngIf="hasPermissionForSubscription" class="list-inline-item">
<a
class="d-sm-block"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'pricing',
'text-decoration-underline': currentRoute === 'pricing'
'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === routePricing
}"
[routerLink]="['/pricing']"
[routerLink]="routerLinkPricing"
>Pricing</a
>
</li>
@ -297,14 +302,14 @@
class="list-inline-item"
>
<a
class="d-none d-sm-block mx-1"
class="d-none d-sm-block"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'markets',
'text-decoration-underline': currentRoute === 'markets'
'font-weight-bold': currentRoute === routeMarkets,
'text-decoration-underline': currentRoute === routeMarkets
}"
[routerLink]="['/markets']"
[routerLink]="routerLinkMarkets"
>Markets</a
>
</li>
@ -317,19 +322,19 @@
></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"
color="primary"
mat-flat-button
[routerLink]="['/register']"
[routerLink]="routerLinkRegister"
><ng-container i18n>Get started</ng-container>
</a>
</li>

View File

@ -7,8 +7,8 @@
.mat-toolbar {
background-color: var(--light-background);
.spacer {
flex: 1 1 auto;
.list-inline-item {
margin: 0;
}
.mdc-button {
@ -24,6 +24,10 @@
font-size: 1.5rem;
}
}
.spacer {
flex: 1 1 auto;
}
}
}

View File

@ -42,6 +42,17 @@ export class HeaderComponent implements OnChanges {
public hasPermissionToCreateUser: boolean;
public impersonationId: string;
public isMenuOpen: boolean;
public routeAbout = $localize`about`;
public routeFeatures = $localize`features`;
public routeMarkets = $localize`markets`;
public routePricing = $localize`pricing`;
public routeResources = $localize`resources`;
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkRegister = ['/' + $localize`register`];
public routerLinkResources = ['/' + $localize`resources`];
private unsubscribeSubject = new Subject<void>();

View File

@ -1,5 +1,5 @@
<div class="container">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Markets</h3>
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
<div class="mb-5 row">
<div class="col-xs-12 col-md-8 offset-md-2">
<div class="mb-2 text-center text-muted">

View File

@ -101,7 +101,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.isLoading = true;
this.dataService
.fetchPortfolioDetails({})
.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ summary }) => {
this.summary = summary;
@ -121,7 +121,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
});
this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']);
this.router.navigate(['/' + $localize`pricing`]);
});
}

View File

@ -1,5 +1,5 @@
<div class="container pb-3 px-3">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3>
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Summary</h1>
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card appearance="outlined">

View File

@ -50,7 +50,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
public onEditEmergencyFund() {
const emergencyFundInput = prompt(
$localize`Please enter the amount of your emergency fund:`,
this.summary.emergencyFund?.toString() ?? '0'
this.summary.emergencyFund?.total?.toString() ?? '0'
);
const emergencyFund = parseFloat(emergencyFundInput?.trim());

View File

@ -11,6 +11,8 @@ import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
templateUrl: 'subscription-interstitial-dialog.html'
})
export class SubscriptionInterstitialDialog {
public routerLinkPricing = ['/' + $localize`pricing`];
public constructor(
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>

View File

@ -56,7 +56,7 @@
<a
color="primary"
mat-flat-button
[routerLink]="['/pricing']"
[routerLink]="routerLinkPricing"
(click)="closeDialog()"
>
<span i18n>Upgrade Plan</span>

View File

@ -4,6 +4,7 @@ import {
Router,
RouterStateSnapshot
} from '@angular/router';
import { paths } from '@ghostfolio/client/app-routing.module';
import { DataService } from '@ghostfolio/client/services/data.service';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -13,21 +14,17 @@ import { catchError } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AuthGuard {
private static PUBLIC_PAGE_ROUTES = [
'/about',
'/about/changelog',
'/about/privacy-policy',
`/${paths.about}`,
'/blog',
'/de/blog',
'/demo',
'/en/blog',
'/faq',
'/features',
'/markets',
`/${paths.faq}`,
`/${paths.features}`,
`/${paths.markets}`,
'/open',
'/p',
'/pricing',
'/register',
'/resources'
`/${paths.pricing}`,
`/${paths.register}`,
`/${paths.resources}`
];
constructor(
@ -53,7 +50,7 @@ export class AuthGuard {
this.router.navigate(['/demo']);
resolve(false);
} else if (utmSource === 'trusted-web-activity') {
this.router.navigate(['/register']);
this.router.navigate(['/' + $localize`register`]);
resolve(false);
} else if (
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) =>

View File

@ -77,7 +77,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
});
this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']);
this.router.navigate(['/' + $localize`pricing`]);
});
}
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {

View File

@ -1,5 +1,8 @@
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';
@ -22,38 +25,27 @@ const routes: Routes = [
(m) => m.ChangelogPageModule
)
},
...[
'license',
/////
'licenca',
'licence',
'licencia',
'licentie',
'lizenz',
'licenza'
].map((path) => ({
path,
{
path: paths.license,
loadChildren: () =>
import('./license/license-page.module').then(
(m) => m.LicensePageModule
)
})),
...[
'privacy-policy',
/////
'datenschutzbestimmungen',
'informativa-sulla-privacy',
'politique-de-confidentialite',
'politica-de-privacidad',
'politica-de-privacidade',
'privacybeleid'
].map((path) => ({
path,
},
{
path: 'oss-friends',
loadChildren: () =>
import('./oss-friends/oss-friends-page.module').then(
(m) => m.OpenSourceSoftwareFriendsPageModule
)
},
{
path: paths.privacyPolicy,
loadChildren: () =>
import('./privacy-policy/privacy-policy-page.module').then(
(m) => m.PrivacyPolicyPageModule
)
}))
}
],
component: AboutPageComponent,
path: '',

View File

@ -44,30 +44,31 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.tabs = [
{
iconName: 'reader-outline',
label: $localize`About`,
path: ['/about']
path: ['/' + $localize`about`]
},
{
iconName: 'sparkles-outline',
label: $localize`Changelog`,
path: ['/about', 'changelog']
path: ['/' + $localize`about`, 'changelog']
},
{
iconName: 'ribbon-outline',
label: $localize`License`,
path: ['/about', 'license']
},
{
iconName: 'shield-checkmark-outline',
label: $localize`Privacy Policy`,
path: ['/about', 'privacy-policy'],
showCondition: this.hasPermissionForSubscription
path: ['/' + $localize`about`, $localize`license`]
}
];
if (state?.user) {
this.tabs.push({
iconName: 'shield-checkmark-outline',
label: $localize`Privacy Policy`,
path: ['/' + $localize`about`, $localize`privacy-policy`],
showCondition: this.hasPermissionForSubscription
});
this.user = state.user;
this.hasMessage =
@ -78,6 +79,12 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
}
this.tabs.push({
iconName: 'happy-outline',
label: 'OSS Friends',
path: ['/' + $localize`about`, 'oss-friends']
});
});
}

View File

@ -11,14 +11,9 @@
padding-bottom: constant(safe-area-inset-bottom);
::ng-deep {
gf-about-page,
gf-changelog-page,
gf-privacy-policy-page {
flex: 1 1 auto;
overflow-y: auto;
}
.mat-mdc-tab-link-container {
--mat-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 {

View File

@ -1,7 +1,7 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Changelog</h1>
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Changelog</h1>
<div class="changelog">
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
</div>

View File

@ -1,7 +1,7 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>License</h1>
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>License</h1>
<div>
<markdown [src]="'../assets/LICENSE'"></markdown>
</div>

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { OpenSourceSoftwareFriendsPageComponent } from './oss-friends-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: OpenSourceSoftwareFriendsPageComponent,
path: '',
title: 'OSS Friends'
}
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)]
})
export class OpenSourceSoftwareFriendsPageRoutingModule {}

View File

@ -0,0 +1,23 @@
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
const ossFriends = require('../../../../assets/oss-friends.json');
@Component({
host: { class: 'page' },
selector: 'gf-oss-friends-page',
styleUrls: ['./oss-friends-page.scss'],
templateUrl: './oss-friends-page.html'
})
export class OpenSourceSoftwareFriendsPageComponent implements OnDestroy {
public ossFriends = ossFriends.data;
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,42 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="h3 mb-4 text-center">
<span class="d-none d-sm-block"
><ng-container i18n>Our</ng-container> OSS Friends</span
>
<small class="text-muted" i18n
>Discover other exciting Open Source Software projects</small
>
</h1>
<div class="row">
<div
*ngFor="let ossFriend of ossFriends"
class="col-xs-12 col-md-4 mb-3"
>
<a target="_blank" [href]="ossFriend.href">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-header>
<mat-card-title class="h4">{{ ossFriend.name }}</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-grow-1">
<p>{{ ossFriend.description }}</p>
</mat-card-content>
<mat-card-actions class="justify-content-end">
<a mat-button target="_blank" [href]="ossFriend.href">
<span
><ng-container i18n>Visit</ng-container> {{ ossFriend.name
}}</span
><ion-icon
class="ml-1"
name="arrow-forward-outline"
></ion-icon>
</a>
</mat-card-actions>
</mat-card>
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,19 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { OpenSourceSoftwareFriendsPageRoutingModule } from './oss-friends-page-routing.module';
import { OpenSourceSoftwareFriendsPageComponent } from './oss-friends-page.component';
@NgModule({
declarations: [OpenSourceSoftwareFriendsPageComponent],
imports: [
CommonModule,
MatButtonModule,
MatCardModule,
OpenSourceSoftwareFriendsPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class OpenSourceSoftwareFriendsPageModule {}

View File

@ -0,0 +1,9 @@
:host {
display: block;
.mat-mdc-card {
&:hover {
border-color: var(--gf-theme-primary-500);
}
}
}

View File

@ -18,6 +18,8 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit {
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;
public isLoggedIn: boolean;
public routerLinkFaq = ['/' + $localize`faq`];
public routerLinkFeatures = ['/' + $localize`features`];
public user: User;
public version = environment.version;

View File

@ -1,7 +1,9 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h3 class="d-none d-sm-block mb-3 text-center">About Ghostfolio</h3>
<h1 class="d-none d-sm-block h3 mb-4 text-center">
<ng-container i18n>About Ghostfolio</ng-container>
</h1>
<div class="about-container">
<p>
Ghostfolio is a lightweight wealth management application for
@ -46,7 +48,7 @@
<p>
If you encounter a bug or would like to suggest an improvement or a
new
<a [routerLink]="['/features']">feature</a>, please join the
<a [routerLink]="routerLinkFeatures">feature</a>, please join the
Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
@ -139,7 +141,7 @@
class="py-4 w-100"
color="primary"
mat-flat-button
[routerLink]="['/faq']"
[routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a
>
</div>

View File

@ -1,7 +1,7 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Privacy Policy</h3>
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Privacy Policy</h1>
<markdown [src]="'../assets/privacy-policy.md'"></markdown>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div class="container">
<div class="row">
<div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Accounts</h3>
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Accounts</h1>
<div class="accounts">
<gf-accounts-table
[accounts]="accounts"

View File

@ -11,16 +11,9 @@
padding-bottom: constant(safe-area-inset-bottom);
::ng-deep {
gf-admin-jobs,
gf-admin-market-data,
gf-admin-overview,
gf-admin-settings,
gf-admin-users {
flex: 1 1 auto;
overflow-y: auto;
}
.mat-mdc-tab-link-container {
--mat-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 {

View File

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './hallo-ghostfolio-page.html'
})
export class HalloGhostfolioPageComponent {}
export class HalloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkResources = ['/' + $localize`resources`];
}

View File

@ -19,7 +19,7 @@
Aufgrund der steigenden Inflation und den Negativzinsen befasse ich
mich seit einiger Zeit, wie ich mein Vermögen möglichst
diversifiziert anlegen kann. Konkret verfolge ich eine
<a [routerLink]="['/resources']">Buy and Hold Strategie</a> mit
<a [routerLink]="routerLinkResources">Buy and Hold Strategie</a> mit
Investitionen in verschiedene Anlageklassen verteilt auf
unterschiedliche Plattformen. Deshalb suchte ich nach einer App, die
mein Portfolio ganzheitlich zusammenfasst. Bei meiner
@ -119,7 +119,7 @@
Anlagestrategie? Ich freue mich über alle, die Ghostfolio
ausprobieren. Bist du überzeugt vom Potential der Software? Jede
Unterstützung für Ghostfolio ist willkommen. Sei es mit einer
<a [routerLink]="['/pricing']">Ghostfolio Premium</a>
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
Subscription zur Finanzierung des Hostings, einem positiven Rating
im
<a

View File

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './hello-ghostfolio-page.html'
})
export class HelloGhostfolioPageComponent {}
export class HelloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkResources = ['/' + $localize`resources`];
}

View File

@ -19,7 +19,7 @@
Due to rising inflation and negative interest rates, I have been
looking for some time at how I can invest my assets in the most
diversified way possible. Specifically, I follow a
<a [routerLink]="['/resources']">buy and hold strategy</a> with
<a [routerLink]="routerLinkResources">buy and hold strategy</a> with
investments in different asset classes spread across different
platforms. Therefore, I was looking for an app that would
holistically aggregate my portfolio. During my research on the
@ -115,7 +115,7 @@
strategy? I'm happy for everyone who tries Ghostfolio. Are you
convinced of its potential? Any support for Ghostfolio is welcome.
Be it with a
<a [routerLink]="['/pricing']">Ghostfolio Premium</a>
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
Subscription to finance the hosting, a positive rating in the
<a
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"

View File

@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './first-months-in-open-source-page.html'
})
export class FirstMonthsInOpenSourcePageComponent {}
export class FirstMonthsInOpenSourcePageComponent {
public routerLinkPricing = ['/' + $localize`pricing`];
}

View File

@ -86,7 +86,7 @@
</p>
<p>
My personal goal is to reach break-even with the Saas offering (<a
[routerLink]="['/pricing']"
[routerLink]="routerLinkPricing"
>Ghostfolio Premium</a
>) and regularly report about the progress and my learnings on this
exciting journey.

View File

@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
})
export class HowDoIGetMyFinancesInOrderPageComponent {}
export class HowDoIGetMyFinancesInOrderPageComponent {
public routerLinkResources = ['/' + $localize`resources`];
}

View File

@ -9,9 +9,9 @@
<section class="mb-4">
<p>
Before you can think of
<a [routerLink]="['/resources']">long-term investing</a>, you have
to get your finances in order. Take a look at Peter's journey to see
how you can achieve it, too.
<a [routerLink]="routerLinkResources">long-term investing</a>, you
have to get your finances in order. Take a look at Peter's journey
to see how you can achieve it, too.
</p>
<p>
Peter enjoys life, but sometimes he overspends a bit. He realizes it

View File

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './500-stars-on-github-page.html'
})
export class FiveHundredStarsOnGitHubPageComponent {}
export class FiveHundredStarsOnGitHubPageComponent {
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
}

View File

@ -71,10 +71,10 @@
<h2 class="h4">Break-even Point</h2>
<p>
Despite the complicated
<a [routerLink]="['/markets']">economic situation</a> at this time,
the goal set at the beginning of the year to build a sustainable
business and reach break-even with the SaaS offering (<a
[routerLink]="['/pricing']"
<a [routerLink]="routerLinkMarkets">economic situation</a> at this
time, the goal set at the beginning of the year to build a
sustainable business and reach break-even with the SaaS offering (<a
[routerLink]="routerLinkPricing"
>Ghostfolio Premium</a
>) has been achieved. We will continue to leverage the revenue to
further improve the fully managed cloud offering for our paying

View File

@ -11,5 +11,6 @@ import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
templateUrl: './black-friday-2022-page.html'
})
export class BlackFriday2022PageComponent {
public constructor() {}
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkPricing = ['/' + $localize`pricing`];
}

View File

@ -35,17 +35,18 @@
software presents the current assets (stocks, ETFs,
cryptocurrencies, commodities etc.) in real time to make solid,
data-driven investment decisions. Check out the numerous
<a [routerLink]="['/features']">features</a> to manage your wealth.
<a [routerLink]="routerLinkFeatures">features</a> to manage your
wealth.
</p>
</section>
<section class="mb-4">
<p>
Snap the limited Black Friday 2022 deal before its gone. For
detailed information on plans and pricing, please visit our
<a [routerLink]="['/pricing']">pricing page</a>.
<a [routerLink]="routerLinkPricing">pricing page</a>.
</p>
<p class="text-center">
<a color="primary" mat-flat-button [routerLink]="['/pricing']"
<a color="primary" mat-flat-button [routerLink]="routerLinkPricing"
>Get the Deal</a
>
</p>

View File

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './1000-stars-on-github-page.html'
})
export class ThousandStarsOnGitHubPageComponent {}
export class ThousandStarsOnGitHubPageComponent {
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkPricing = ['/' + $localize`pricing`];
}

View File

@ -92,7 +92,7 @@
<p>
These self-hosting platforms allow users to run applications on
their own hardware rather than rely on a
<a [routerLink]="['/pricing']">SaaS offering</a>. As a result,
<a [routerLink]="routerLinkPricing">SaaS offering</a>. As a result,
Ghostfolio has become accessible to an even wider range of users who
would like to take control of their wealth management.
</p>
@ -108,9 +108,9 @@
As the project continues to evolve, we can expect to see even more
exciting developments and innovations around Ghostfolio which guides
users through the process of
<a [routerLink]="['/features']">tracking their assets</a>, such as
stocks, ETFs, or cryptocurrencies. Especially in the areas of data
import and portfolio analysis.
<a [routerLink]="routerLinkFeatures">tracking their assets</a>, such
as stocks, ETFs, or cryptocurrencies. Especially in the areas of
data import and portfolio analysis.
</p>
<p>
We are honored to be a part of this vibrant and growing community,

View File

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
})
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {}
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResources = ['/' + $localize`resources`];
}

View File

@ -44,13 +44,13 @@
<h2 class="h4">Empowering Buy & Hold Strategies</h2>
<p>
For those committed to a
<a [routerLink]="['/resources']">buy & hold strategy</a>, Ghostfolio
provides an intuitive interface to monitor long-term investments.
Users can track performance over time, gaining insights into
portfolio growth and stability. With strong visualizations and
reporting <a [routerLink]="['/features']">features</a>, Ghostfolio
equips users to make well-informed decisions aligned with their
long-term investment goals.
<a [routerLink]="routerLinkResources">buy & hold strategy</a>,
Ghostfolio provides an intuitive interface to monitor long-term
investments. Users can track performance over time, gaining insights
into portfolio growth and stability. With strong visualizations and
reporting <a [routerLink]="routerLinkFeatures">features</a>,
Ghostfolio equips users to make well-informed decisions aligned with
their long-term investment goals.
</p>
</section>
<section class="mb-4">
@ -91,7 +91,7 @@
<h2 class="h4">Driving Financial Independence (FIRE)</h2>
<p>
Achieving
<a [routerLink]="['/resources']">financial independence</a>
<a [routerLink]="routerLinkResources">financial independence</a>
including early retirement (<a
href="../en/blog/2023/07/exploring-the-path-to-fire"
>FIRE</a

View File

@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
standalone: true,
templateUrl: './exploring-the-path-to-fire-page.html'
})
export class ExploringThePathToFirePageComponent {}
export class ExploringThePathToFirePageComponent {
public routerLinkFeatures = ['/' + $localize`features`];
}

View File

@ -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)"
/>
@ -135,10 +135,10 @@
track your investments, and make informed decisions to accelerate
your progress towards financial independence. Ghostfolio also
provides a dedicated
<a [routerLink]="['/features']">FIRE calculator</a>, allowing you to
simulate your customized plan to achieve FIRE. You get the tools to
optimize your financial journey and confidently strive for a future
that is both personally fulfilling and financially secure.
<a [routerLink]="routerLinkFeatures">FIRE calculator</a>, allowing
you to simulate your customized plan to achieve FIRE. You get the
tools to optimize your financial journey and confidently strive for
a future that is both personally fulfilling and financially secure.
</p>
</section>
<section class="mb-4 py-3">

View File

@ -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'];
}

View File

@ -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
>. Lets 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>

View File

@ -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`];
}

Some files were not shown because too many files have changed in this diff Show More