Browse Source

Automatically gather exchange rates if needed

#5897
pull/5917/head
Sven Günther 2 months ago
committed by Thomas Kaul
parent
commit
7c205fe10a
  1. 13
      apps/api/src/app/import/import.service.ts
  2. 9
      apps/api/src/app/order/order.service.ts
  3. 12
      apps/api/src/events/asset-profile-changed.event.ts
  4. 92
      apps/api/src/events/asset-profile-changed.listener.ts
  5. 17
      apps/api/src/events/events.module.ts
  6. 1
      apps/api/src/services/configuration/configuration.service.ts
  7. 1
      apps/api/src/services/interfaces/environment.interface.ts

13
apps/api/src/app/import/import.service.ts

@ -4,6 +4,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
@ -28,6 +29,7 @@ import {
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns';
@ -44,6 +46,7 @@ export class ImportService {
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly eventEmitter: EventEmitter2,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly orderService: OrderService, private readonly orderService: OrderService,
private readonly platformService: PlatformService, private readonly platformService: PlatformService,
@ -605,6 +608,16 @@ export class ImportService {
}), }),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
}); });
uniqueActivities.forEach(({ SymbolProfile }) => {
this.eventEmitter.emit(
AssetProfileChangedEvent.getName(),
new AssetProfileChangedEvent(
SymbolProfile.currency,
SymbolProfile.symbol
)
);
});
} }
return activities; return activities;

9
apps/api/src/app/order/order.service.ts

@ -1,4 +1,5 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -232,6 +233,14 @@ export class OrderService {
}) })
); );
this.eventEmitter.emit(
AssetProfileChangedEvent.getName(),
new AssetProfileChangedEvent(
order.SymbolProfile.currency,
order.SymbolProfile.symbol
)
);
return order; return order;
} }

12
apps/api/src/events/asset-profile-changed.event.ts

@ -0,0 +1,12 @@
export class AssetProfileChangedEvent {
private static readonly eventName = 'asset-profile.changed';
public constructor(
public readonly currency: string,
public readonly symbol: string
) {}
public static getName(): string {
return AssetProfileChangedEvent.eventName;
}
}

92
apps/api/src/events/asset-profile-changed.listener.ts

@ -0,0 +1,92 @@
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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AssetProfileChangedEvent } from './asset-profile-changed.event';
@Injectable()
export class AssetProfileChangedListener {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
@OnEvent(AssetProfileChangedEvent.getName())
public async handleAssetProfileChanged(event: AssetProfileChangedEvent) {
const isEnabled = this.configurationService.get(
'ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES'
);
if (isEnabled === false) {
return;
}
if (event.currency === DEFAULT_CURRENCY) {
return;
}
Logger.log(
`Asset profile changed: ${event.symbol} (${event.currency}). Checking if exchange rate gathering is needed.`,
'AssetProfileChangedListener'
);
const existingCurrencies = this.exchangeRateDataService.getCurrencies();
const currencyAlreadyExists = existingCurrencies.includes(event.currency);
if (currencyAlreadyExists) {
return;
}
Logger.log(
`New currency detected: ${event.currency}. Initializing exchange rate data service.`,
'AssetProfileChangedListener'
);
await this.exchangeRateDataService.initialize();
if (
!this.exchangeRateDataService.hasCurrencyPair(
DEFAULT_CURRENCY,
event.currency
)
) {
Logger.warn(
`Currency pair ${DEFAULT_CURRENCY}${event.currency} was not added after initialization.`,
'AssetProfileChangedListener'
);
return;
}
const firstOrderWithCurrency = await this.prismaService.order.findFirst({
orderBy: [{ date: 'asc' }],
select: { date: true },
where: {
SymbolProfile: {
currency: event.currency
}
}
});
const startDate = firstOrderWithCurrency.date ?? new Date();
Logger.log(
`Triggering exchange rate data gathering for ${DEFAULT_CURRENCY}${event.currency} from ${startDate.toISOString()}.`,
'AssetProfileChangedListener'
);
await this.dataGatheringService.gatherSymbol({
dataSource: this.dataProviderService.getDataSourceForExchangeRates(),
symbol: `${DEFAULT_CURRENCY}${event.currency}`,
date: startDate
});
}
}

17
apps/api/src/events/events.module.ts

@ -1,11 +1,24 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.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 { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AssetProfileChangedListener } from './asset-profile-changed.listener';
import { PortfolioChangedListener } from './portfolio-changed.listener'; import { PortfolioChangedListener } from './portfolio-changed.listener';
@Module({ @Module({
imports: [RedisCacheModule], imports: [
providers: [PortfolioChangedListener] ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule,
RedisCacheModule
],
providers: [AssetProfileChangedListener, PortfolioChangedListener]
}) })
export class EventsModule {} export class EventsModule {}

1
apps/api/src/services/configuration/configuration.service.ts

@ -43,6 +43,7 @@ export class ConfigurationService {
ENABLE_FEATURE_AUTH_GOOGLE: bool({ default: false }), ENABLE_FEATURE_AUTH_GOOGLE: bool({ default: false }),
ENABLE_FEATURE_AUTH_TOKEN: bool({ default: true }), ENABLE_FEATURE_AUTH_TOKEN: bool({ default: true }),
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: bool({ default: true }),
ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }),
ENABLE_FEATURE_STATISTICS: bool({ default: false }), ENABLE_FEATURE_STATISTICS: bool({ default: false }),
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }), ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),

1
apps/api/src/services/interfaces/environment.interface.ts

@ -19,6 +19,7 @@ export interface Environment extends CleanedEnvAccessors {
ENABLE_FEATURE_AUTH_GOOGLE: boolean; ENABLE_FEATURE_AUTH_GOOGLE: boolean;
ENABLE_FEATURE_AUTH_TOKEN: boolean; ENABLE_FEATURE_AUTH_TOKEN: boolean;
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: boolean;
ENABLE_FEATURE_READ_ONLY_MODE: boolean; ENABLE_FEATURE_READ_ONLY_MODE: boolean;
ENABLE_FEATURE_STATISTICS: boolean; ENABLE_FEATURE_STATISTICS: boolean;
ENABLE_FEATURE_SUBSCRIPTION: boolean; ENABLE_FEATURE_SUBSCRIPTION: boolean;

Loading…
Cancel
Save