Compare commits

...

75 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
42274917e0 Release 1.297.4 (#2209) 2023-08-05 19:57:39 +02:00
8ba50f2729 Release 1.297.3 (#2208) 2023-08-05 16:08:06 +02:00
f22071f061 Release 1.297.2 (#2207) 2023-08-05 14:51:10 +02:00
d2312371a6 Release 1.297.1 (#2205) 2023-08-05 14:35:13 +02:00
208 changed files with 32068 additions and 13644 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,7 +5,133 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.297.0 - 2023-08-05
## 2.0.0 - 2023-09-09
### Added
- Added support for the cryptocurrency _CyberConnect_
- Added a blog post: _Announcing Ghostfolio 2.0_
### Changed
- **Breaking Change**: Removed the deprecated environment variable `BASE_CURRENCY`
- Improved the validation in the activities import
- Deactivated _Internet Identity_ as a social login provider for the account registration
- Improved the language localization for German (`de`)
- Refreshed the cryptocurrencies list
- Changed the version in the `docker-compose` files from `3.7` to `3.9`
- Upgraded `yahoo-finance2` from version `2.4.4` to `2.5.0`
### Fixed
- Fixed an issue in the _Yahoo Finance_ data enhancer where countries and sectors have been removed
## 1.305.0 - 2023-09-03
### Added
- Added _Hacker News_ to the _As seen in_ section on the landing page
### Changed
- Shortened the page titles
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `4.16.2` to `5.2.0`
- Upgraded `replace-in-file` from version `6.3.5` to `7.0.1`
- Upgraded `yahoo-finance2` from version `2.4.3` to `2.4.4`
### Fixed
- Fixed the alignment in the header navigation
- Fixed the alignment in the menu of the impersonation mode
## 1.304.0 - 2023-08-27
### Added
- Added health check endpoints for data enhancers
### Changed
- Upgraded `Nx` from version `16.7.2` to `16.7.4`
- Upgraded `prettier` from version `2.8.4` to `3.0.2`
## 1.303.0 - 2023-08-23
### Added
- Added a blog post: _Ghostfolio joins OSS Friends_
### Changed
- Refreshed the cryptocurrencies list
- Improved the _OSS Friends_ page
### Fixed
- Fixed an issue with the _Trackinsight_ data enhancer for asset profile data
## 1.302.0 - 2023-08-20
### Changed
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `16.1.8` to `16.2.1`
- Upgraded `Nx` from version `16.6.0` to `16.7.2`
## 1.301.1 - 2023-08-19
### Added
- Added the data export feature to the user account page
- Added a currencies preset to the historical market data table of the admin control panel
- Added the _OSS Friends_ page
### Changed
- Improved the localized meta data in `html` files
### Fixed
- Fixed the rows with cash positions in the holdings table
- Fixed an issue with the date parsing in the historical market data editor of the admin control panel
## 1.300.0 - 2023-08-11
### Added
- Added more durations in the coupon system
### Changed
- Migrated the remaining requests from `bent` to `got`
## 1.299.1 - 2023-08-10
### Changed
- Optimized the activities import by allowing a different currency than the asset's official one
- Added a timeout to the _EOD Historical Data_ requests
- Migrated the requests from `bent` to `got` in the _EOD Historical Data_ service
### Fixed
- Fixed the editing of the emergency fund
- Fixed the historical data gathering interval for asset profiles used as benchmarks having activities
## 1.298.0 - 2023-08-06
### Changed
- Improved the language localization for German (`de`)
- Upgraded `ng-extract-i18n-merge` from version `2.6.0` to `2.7.0`
- Upgraded `Nx` from version `16.5.5` to `16.6.0`
### Fixed
- Fixed the styles of various components (card, progress, tab) after the upgrade to `@angular/material` `16`
## 1.297.4 - 2023-08-05
### Added
@ -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

@ -58,4 +58,4 @@ RUN apt update && apt install -y \
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
WORKDIR /ghostfolio/apps/api
EXPOSE ${PORT:-3333}
CMD [ "yarn", "start:production" ]
CMD [ "yarn", "start:production" ]

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

@ -7,11 +7,16 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import {
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull';
import { 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';
import { StatusCodes } from 'http-status-codes';
import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module';
@ -23,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';
@ -32,14 +36,10 @@ import { OrderModule } from './order/order.module';
import { PlatformModule } from './platform/platform.module';
import { PortfolioModule } from './portfolio/portfolio.module';
import { RedisCacheModule } from './redis-cache/redis-cache.module';
import { SitemapModule } from './sitemap/sitemap.module';
import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module';
import { UserModule } from './user/user.module';
import {
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { StatusCodes } from 'http-status-codes';
@Module({
imports: [
@ -75,6 +75,8 @@ import { StatusCodes } from 'http-status-codes';
RedisCacheModule,
ScheduleModule.forRoot(),
ServeStaticModule.forRoot({
exclude: ['/api*', '/sitemap.xml'],
rootPath: join(__dirname, '..', 'client'),
serveStaticOptions: {
setHeaders: (res) => {
if (res.req?.path === '/') {
@ -94,10 +96,9 @@ import { StatusCodes } from 'http-status-codes';
res.statusCode = StatusCodes.MOVED_PERMANENTLY;
}
}
},
rootPath: join(__dirname, '..', 'client'),
exclude: ['/api*']
}
}),
SitemapModule,
SubscriptionModule,
SymbolModule,
TwitterBotModule,
@ -106,10 +107,4 @@ import { StatusCodes } from 'http-status-codes';
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,255 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { format } from 'date-fns';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = '';
public indexHtmlEn = '';
public indexHtmlEs = '';
public indexHtmlFr = '';
public indexHtmlIt = '';
public indexHtmlNl = '';
public indexHtmlPt = '';
public sitemapXml = '';
private static readonly DEFAULT_DESCRIPTION =
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
public constructor(
private readonly configurationService: ConfigurationService
) {
try {
this.indexHtmlDe = fs.readFileSync(
this.getPathOfIndexHtmlFile('de'),
'utf8'
);
this.indexHtmlEn = fs.readFileSync(
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8'
);
this.indexHtmlEs = fs.readFileSync(
this.getPathOfIndexHtmlFile('es'),
'utf8'
);
this.indexHtmlFr = fs.readFileSync(
this.getPathOfIndexHtmlFile('fr'),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
this.indexHtmlNl = fs.readFileSync(
this.getPathOfIndexHtmlFile('nl'),
'utf8'
);
this.indexHtmlPt = fs.readFileSync(
this.getPathOfIndexHtmlFile('pt'),
'utf8'
);
this.sitemapXml = fs.readFileSync(
path.join(__dirname, 'assets', 'sitemap.xml'),
'utf8'
);
} catch {}
}
public use(request: Request, response: Response, next: NextFunction) {
const currentDate = format(new Date(), DATE_FORMAT);
let featureGraphicPath = 'assets/cover.png';
let title = 'Ghostfolio Open Source Wealth Management Software';
if (request.path.startsWith('/en/blog/2022/08/500-stars-on-github')) {
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
title = `500 Stars - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/10/hacktoberfest-2022')) {
featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png';
title = `Hacktoberfest 2022 - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/11/black-friday-2022')) {
featureGraphicPath = 'assets/images/blog/black-friday-2022.jpg';
title = `Black Friday 2022 - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances'
)
) {
featureGraphicPath = 'assets/images/blog/20221226.jpg';
title = `The importance of tracking your personal finances - ${title}`;
} else if (
request.path.startsWith(
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt'
)
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png';
title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`;
} else if (
request.path.startsWith('/en/blog/2023/02/ghostfolio-meets-umbrel')
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
title = `Ghostfolio meets Umbrel - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
)
) {
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
title = `Ghostfolio reaches 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 === '/sitemap.xml') {
response.setHeader('content-type', 'application/xml');
response.send(
this.interpolate(this.sitemapXml, {
currentDate: format(getYesterday(), DATE_FORMAT)
})
);
} else if (request.path === '/de' || request.path.startsWith('/de/')) {
response.send(
this.interpolate(this.indexHtmlDe, {
currentDate,
featureGraphicPath,
title,
description:
'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
languageCode: 'de',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/es' || request.path.startsWith('/es/')) {
response.send(
this.interpolate(this.indexHtmlEs, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
languageCode: 'es',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/fr' || request.path.startsWith('/fr/')) {
response.send(
this.interpolate(this.indexHtmlFr, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
languageCode: 'fr',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/it' || request.path.startsWith('/it/')) {
response.send(
this.interpolate(this.indexHtmlIt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
languageCode: 'it',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/nl' || request.path.startsWith('/nl/')) {
response.send(
this.interpolate(this.indexHtmlNl, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, 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(
this.interpolate(this.indexHtmlPt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
languageCode: 'pt',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
response.send(
this.interpolate(this.indexHtmlEn, {
currentDate,
featureGraphicPath,
title,
description: FrontendMiddleware.DEFAULT_DESCRIPTION,
languageCode: DEFAULT_LANGUAGE_CODE,
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
}
}
private getPathOfIndexHtmlFile(aLocale: string) {
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
}
private interpolate(template: string, context: any) {
return template.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
const properties = objectPath.split('.');
return properties.reduce(
(previous, current) => previous?.[current],
context
);
});
}
private isFileRequest(filename: string) {
if (filename === '/assets/LICENSE') {
return true;
} else if (
filename === '/sitemap.xml' ||
filename.includes('auth/ey') ||
filename.includes(
'personal-finance-tools/open-source-alternative-to-markets.sh'
)
) {
return false;
}
return filename.split('.').pop() !== filename;
}
}

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,17 +249,20 @@ export class ImportService {
const activities: Activity[] = [];
for (const {
accountId,
comment,
date,
error,
fee,
quantity,
SymbolProfile,
type,
unitPrice
} of activitiesExtendedWithErrors) {
for (let [
index,
{
accountId,
comment,
date,
error,
fee,
quantity,
SymbolProfile,
type,
unitPrice
}
] 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,
{
Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY'
)}`
}
);
const { data } = await get();
{
headers: {
Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY'
)}`
}
}
).json<any>();
return data.attributes.availability / 100;
} 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

@ -2,7 +2,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common';
import { redisStore } from 'cache-manager-redis-store';
import * as redisStore from 'cache-manager-redis-store';
import type { RedisClientOptions } from 'redis';
import { RedisCacheService } from './redis-cache.service';

View File

@ -0,0 +1,36 @@
import * as fs from 'fs';
import * as path from 'path';
import {
DATE_FORMAT,
getYesterday,
interpolate
} from '@ghostfolio/common/helper';
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
import { format } from 'date-fns';
import { Response } from 'express';
@Controller('sitemap.xml')
export class SitemapController {
public sitemapXml = '';
public constructor() {
try {
this.sitemapXml = fs.readFileSync(
path.join(__dirname, 'assets', 'sitemap.xml'),
'utf8'
);
} catch {}
}
@Get()
@Version(VERSION_NEUTRAL)
public async flushCache(@Res() response: Response): Promise<void> {
response.setHeader('content-type', 'application/xml');
response.send(
interpolate(this.sitemapXml, {
currentDate: format(getYesterday(), DATE_FORMAT)
})
);
}
}

View File

@ -0,0 +1,24 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
import { SitemapController } from './sitemap.controller';
@Module({
controllers: [SitemapController],
imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule
]
})
export class SitemapModule {}

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);
@ -23,7 +24,7 @@ async function bootstrap() {
defaultVersion: '1',
type: VersioningType.URI
});
app.setGlobalPrefix('api');
app.setGlobalPrefix('api', { exclude: ['sitemap.xml'] });
app.useGlobalPipes(
new ValidationPipe({
forbidNonWhitelisted: true,
@ -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,27 +32,43 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response;
}
const profile = await getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
).catch(() => {
return {};
});
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) {
// Skip if data is inaccurate
@ -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,
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
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

@ -110,6 +110,9 @@
{
"command": "cp apps/client/src/assets/favicon.ico dist/apps/client"
},
{
"command": "cp apps/client/src/assets/index.html dist/apps/client"
},
{
"command": "cp apps/client/src/assets/robots.txt dist/apps/client"
},

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

@ -29,7 +29,7 @@
}"
[title]="
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
| date : defaultDateFormat) ?? ''
| date: defaultDateFormat) ?? ''
"
(click)="
onOpenMarketDataDetail({

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,33 +129,37 @@
<mat-menu #accountMenu="matMenu" xPosition="before">
<ng-container *ngIf="user?.access?.length > 0">
<button mat-menu-item (click)="impersonateAccount(null)">
<ion-icon
*ngIf="user?.access?.length > 0"
class="mr-2"
[name]="
impersonationId
? 'radio-button-off-outline'
: 'radio-button-on-outline'
"
></ion-icon>
<span i18n>Me</span>
<span class="align-items-center d-flex">
<ion-icon
*ngIf="user?.access?.length > 0"
class="mr-2"
[name]="
impersonationId
? 'radio-button-off-outline'
: 'radio-button-on-outline'
"
></ion-icon>
<span i18n>Me</span>
</span>
</button>
<button
*ngFor="let accessItem of user?.access"
mat-menu-item
(click)="impersonateAccount(accessItem.id)"
>
<ion-icon
class="mr-2"
name="square-outline"
[name]="
accessItem.id === impersonationId
? 'radio-button-on-outline'
: 'radio-button-off-outline'
"
></ion-icon>
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
<span *ngIf="!accessItem.alias" i18n>User</span>
<span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="square-outline"
[name]="
accessItem.id === impersonationId
? 'radio-button-on-outline'
: 'radio-button-off-outline'
"
></ion-icon>
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
<span *ngIf="!accessItem.alias" i18n>User</span>
</span>
</button>
<hr class="m-0" />
</ng-container>
@ -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) => {
this.tabs = [
{
iconName: 'reader-outline',
label: $localize`About`,
path: ['/' + $localize`about`]
},
{
iconName: 'sparkles-outline',
label: $localize`Changelog`,
path: ['/' + $localize`about`, 'changelog']
},
{
iconName: 'ribbon-outline',
label: $localize`License`,
path: ['/' + $localize`about`, $localize`license`]
}
];
if (state?.user) {
this.tabs = [
{
iconName: 'reader-outline',
label: $localize`About`,
path: ['/about']
},
{
iconName: 'sparkles-outline',
label: $localize`Changelog`,
path: ['/about', 'changelog']
},
{
iconName: 'ribbon-outline',
label: $localize`License`,
path: ['/about', 'license']
},
{
iconName: 'shield-checkmark-outline',
label: $localize`Privacy Policy`,
path: ['/about', 'privacy-policy'],
showCondition: this.hasPermissionForSubscription
}
];
this.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

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