Feature/add support to set the base currency via env variable (#948)

* Set base currency via environment variable

* Update changelog
pull/949/head
Thomas Kaul 2 years ago committed by GitHub
parent f48832c671
commit 332203b9e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Added support to set the base currency as an environment variable (`BASE_CURRENCY`)
### Fixed
- Fixed an issue with the missing conversion of countries in the symbol profile overrides

@ -6,7 +6,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data.service'
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
import {
AdminData,
AdminMarketData,
@ -20,6 +20,8 @@ import { differenceInDays } from 'date-fns';
@Injectable()
export class AdminService {
private baseCurrency: string;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
@ -29,7 +31,9 @@ export class AdminService {
private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService,
private readonly symbolProfileService: SymbolProfileService
) {}
) {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
}
public async deleteProfileData({ dataSource, symbol }: UniqueAsset) {
await this.marketDataService.deleteMany({ dataSource, symbol });
@ -43,15 +47,15 @@ export class AdminService {
exchangeRates: this.exchangeRateDataService
.getCurrencies()
.filter((currency) => {
return currency !== baseCurrency;
return currency !== this.baseCurrency;
})
.map((currency) => {
return {
label1: baseCurrency,
label1: this.baseCurrency,
label2: currency,
value: this.exchangeRateDataService.toCurrency(
1,
baseCurrency,
this.baseCurrency,
currency
)
};

@ -103,6 +103,7 @@ export class InfoService {
isReadOnlyMode,
platforms,
systemMessage,
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
currencies: this.exchangeRateDataService.getCurrencies(),
demoAuthToken: this.getDemoAuthToken(),
lastDataGathering: await this.getLastDataGathering(),

@ -74,7 +74,12 @@ describe('CurrentRateService', () => {
beforeAll(async () => {
dataProviderService = new DataProviderService(null, [], null);
exchangeRateDataService = new ExchangeRateDataService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
marketDataService = new MarketDataService(null);
await exchangeRateDataService.initialize();

@ -8,7 +8,6 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { baseCurrency } from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper';
import {
Filter,
@ -43,6 +42,8 @@ import { PortfolioService } from './portfolio.service';
@Controller('portfolio')
export class PortfolioController {
private baseCurrency: string;
public constructor(
private readonly accessService: AccessService,
private readonly configurationService: ConfigurationService,
@ -50,7 +51,9 @@ 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('chart')
@UseGuards(AuthGuard('jwt'))
@ -327,7 +330,7 @@ export class PortfolioController {
return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency,
this.request.user?.Settings?.currency ?? baseCurrency
this.request.user?.Settings?.currency ?? this.baseCurrency
);
})
.reduce((a, b) => a + b, 0);

@ -15,6 +15,7 @@ import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '@ghostfolio/ap
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
@ -22,8 +23,7 @@ import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbo
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import {
ASSET_SUB_CLASS_EMERGENCY_FUND,
UNKNOWN_KEY,
baseCurrency
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import {
@ -82,8 +82,11 @@ const emergingMarkets = require('../../assets/countries/emerging-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,
@ -93,7 +96,9 @@ 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(aUserId: string): Promise<AccountWithValue[]> {
const [accounts, details] = await Promise.all([
@ -320,7 +325,7 @@ export class PortfolioService {
const userCurrency =
user.Settings?.currency ??
this.request.user?.Settings?.currency ??
baseCurrency;
this.baseCurrency;
const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
@ -1213,7 +1218,8 @@ export class PortfolioService {
orders: OrderWithAccount[];
portfolioOrders: PortfolioOrder[];
}> {
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const userCurrency =
this.request.user?.Settings?.currency ?? this.baseCurrency;
const orders = await this.orderService.getOrders({
filters,

@ -3,11 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
import { PrismaService } from '@ghostfolio/api/services/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,
baseCurrency,
locale
} from '@ghostfolio/common/config';
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
import {
getPermissions,
@ -26,13 +22,17 @@ const crypto = require('crypto');
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 getUser(
{
@ -224,14 +224,14 @@ export class UserService {
...data,
Account: {
create: {
currency: baseCurrency,
currency: this.baseCurrency,
isDefault: true,
name: 'Default Account'
}
},
Settings: {
create: {
currency: baseCurrency
currency: this.baseCurrency
}
}
}

@ -12,6 +12,7 @@ export class ConfigurationService {
this.environmentConfiguration = cleanEnv(process.env, {
ACCESS_TOKEN_SALT: str(),
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
BASE_CURRENCY: str({ default: 'USD' }),
CACHE_TTL: num({ default: 1 }),
DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }),
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }),

@ -1,3 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { YahooFinanceService } from './yahoo-finance.service';
@ -25,13 +26,18 @@ jest.mock(
);
describe('YahooFinanceService', () => {
let configurationService: ConfigurationService;
let cryptocurrencyService: CryptocurrencyService;
let yahooFinanceService: YahooFinanceService;
beforeAll(async () => {
configurationService = new ConfigurationService();
cryptocurrencyService = new CryptocurrencyService();
yahooFinanceService = new YahooFinanceService(cryptocurrencyService);
yahooFinanceService = new YahooFinanceService(
configurationService,
cryptocurrencyService
);
});
it('convertFromYahooFinanceSymbol', async () => {

@ -1,11 +1,11 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { baseCurrency } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
@ -23,9 +23,14 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa
@Injectable()
export class YahooFinanceService implements DataProviderInterface {
private baseCurrency: string;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService
) {}
) {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
}
public canHandle(symbol: string) {
return true;
@ -33,8 +38,8 @@ export class YahooFinanceService implements DataProviderInterface {
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
const symbol = aYahooFinanceSymbol.replace(
new RegExp(`-${baseCurrency}$`),
baseCurrency
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
);
return symbol.replace('=X', '');
}
@ -47,12 +52,15 @@ export class YahooFinanceService implements DataProviderInterface {
* DOGEUSD -> DOGE-USD
*/
public convertToYahooFinanceSymbol(aSymbol: string) {
if (aSymbol.includes(baseCurrency) && aSymbol.length >= 6) {
if (aSymbol.includes(this.baseCurrency) && aSymbol.length >= 6) {
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
return `${aSymbol}=X`;
} else if (
this.cryptocurrencyService.isCryptocurrency(
aSymbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency)
aSymbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
)
)
) {
// Add a dash before the last three characters
@ -60,8 +68,8 @@ export class YahooFinanceService implements DataProviderInterface {
// DOGEUSD -> DOGE-USD
// SOL1USD -> SOL1-USD
return aSymbol.replace(
new RegExp(`-?${baseCurrency}$`),
`-${baseCurrency}`
new RegExp(`-?${this.baseCurrency}$`),
`-${this.baseCurrency}`
);
}
}
@ -255,7 +263,10 @@ export class YahooFinanceService implements DataProviderInterface {
return (
(quoteType === 'CRYPTOCURRENCY' &&
this.cryptocurrencyService.isCryptocurrency(
symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency)
symbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
)
)) ||
['EQUITY', 'ETF', 'FUTURE', 'MUTUALFUND'].includes(quoteType)
);
@ -264,7 +275,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(baseCurrency);
return symbol.includes(this.baseCurrency);
} else if (quoteType === 'FUTURE') {
// Allow GC=F, but not MGC=F
return symbol.length === 4;

@ -1,12 +1,18 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma.module';
import { PropertyModule } from './property/property.module';
@Module({
imports: [DataProviderModule, PrismaModule, PropertyModule],
imports: [
ConfigurationModule,
DataProviderModule,
PrismaModule,
PropertyModule
],
providers: [ExchangeRateDataService],
exports: [ExchangeRateDataService]
})

@ -1,9 +1,10 @@
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common';
import { format } from 'date-fns';
import { isNumber, uniq } from 'lodash';
import { ConfigurationService } from './configuration.service';
import { DataProviderService } from './data-provider/data-provider.service';
import { IDataGatheringItem } from './interfaces/interfaces';
import { PrismaService } from './prisma.service';
@ -11,11 +12,13 @@ import { PropertyService } from './property/property.service';
@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 prismaService: PrismaService,
private readonly propertyService: PropertyService
@ -24,7 +27,7 @@ export class ExchangeRateDataService {
}
public getCurrencies() {
return this.currencies?.length > 0 ? this.currencies : [baseCurrency];
return this.currencies?.length > 0 ? this.currencies : [this.baseCurrency];
}
public getCurrencyPairs() {
@ -32,6 +35,7 @@ export class ExchangeRateDataService {
}
public async initialize() {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
this.currencies = await this.prepareCurrencies();
this.currencyPairs = [];
this.exchangeRates = {};
@ -212,14 +216,14 @@ export class ExchangeRateDataService {
private prepareCurrencyPairs(aCurrencies: string[]) {
return aCurrencies
.filter((currency) => {
return currency !== baseCurrency;
return currency !== this.baseCurrency;
})
.map((currency) => {
return {
currency1: baseCurrency,
currency1: this.baseCurrency,
currency2: currency,
dataSource: this.dataProviderService.getPrimaryDataSource(),
symbol: `${baseCurrency}${currency}`
symbol: `${this.baseCurrency}${currency}`
};
});
}

@ -3,6 +3,7 @@ import { CleanedEnvAccessors } from 'envalid';
export interface Environment extends CleanedEnvAccessors {
ACCESS_TOKEN_SALT: string;
ALPHA_VANTAGE_API_KEY: string;
BASE_CURRENCY: string;
CACHE_TTL: number;
DATA_SOURCE_PRIMARY: string;
DATA_SOURCES: string | string[]; // string is not correct, error in envalid?

@ -17,6 +17,7 @@ import { DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialogParams } from '../position/position-detail-dialog/interfaces/interfaces';
@Component({

@ -1,7 +1,6 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { baseCurrency } from '@ghostfolio/common/config';
import { User } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -17,7 +16,6 @@ import { environment } from '../../../environments/environment';
templateUrl: './about-page.html'
})
export class AboutPageComponent implements OnDestroy, OnInit {
public baseCurrency = baseCurrency;
public hasPermissionForBlog: boolean;
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;

@ -20,7 +20,6 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { baseCurrency } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -43,7 +42,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
signInWithFingerprintElement: MatSlideToggle;
public accesses: Access[];
public baseCurrency = baseCurrency;
public baseCurrency: string;
public coupon: number;
public couponId: string;
public currencies: string[] = [];
@ -79,8 +78,10 @@ export class AccountPageComponent implements OnDestroy, OnInit {
private userService: UserService,
public webAuthnService: WebAuthnService
) {
const { currencies, globalPermissions, subscriptions } =
const { baseCurrency, currencies, globalPermissions, subscriptions } =
this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
this.coupon = subscriptions?.[0]?.coupon;
this.couponId = subscriptions?.[0]?.couponId;
this.currencies = currencies;

@ -1,7 +1,6 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { baseCurrency } from '@ghostfolio/common/config';
import { User } from '@ghostfolio/common/interfaces';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -13,7 +12,7 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './pricing-page.html'
})
export class PricingPageComponent implements OnDestroy, OnInit {
public baseCurrency = baseCurrency;
public baseCurrency: string;
public coupon: number;
public isLoggedIn: boolean;
public price: number;
@ -29,8 +28,9 @@ export class PricingPageComponent implements OnDestroy, OnInit {
private dataService: DataService,
private userService: UserService
) {
const { subscriptions } = this.dataService.fetchInfo();
const { baseCurrency, subscriptions } = this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
this.coupon = this.price = subscriptions?.[0]?.coupon;
this.price = subscriptions?.[0]?.price;
}

@ -2,8 +2,6 @@ import { DataSource } from '@prisma/client';
import { ToggleOption } from './types';
export const baseCurrency = 'USD';
export const defaultDateRangeOptions: ToggleOption[] = [
{ label: 'Today', value: '1d' },
{ label: 'YTD', value: 'ytd' },

@ -4,6 +4,7 @@ import { Statistics } from './statistics.interface';
import { Subscription } from './subscription.interface';
export interface InfoItem {
baseCurrency: string;
currencies: string[];
demoAuthToken: string;
fearAndGreedDataSource?: string;

@ -1,4 +1,5 @@
import { AssetClass, DataSource } from '@prisma/client';
import { MarketState } from '../types';
export interface Position {

Loading…
Cancel
Save