From e26b015407b774a2dfc92e9db0d270258c56058b Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 15 Feb 2025 15:50:41 +0100
Subject: [PATCH] Bugfix/cannot register cron jobs (#4325)

* Reorganize benchmark modules and move benchmarks endpoint
---
 apps/api/src/app/admin/admin.module.ts        |   2 +-
 apps/api/src/app/admin/admin.service.ts       |   2 +-
 apps/api/src/app/app.module.ts                |   4 +-
 .../benchmarks/benchmarks.controller.ts}      |  10 +-
 .../benchmarks/benchmarks.module.ts}          |  11 +-
 .../benchmarks/benchmarks.service.ts          | 163 +++++++++++++++++
 apps/api/src/app/info/info.module.ts          |   2 +-
 apps/api/src/app/info/info.service.ts         |   2 +-
 .../services/benchmark/benchmark.module.ts    |  24 +++
 .../benchmark/benchmark.service.spec.ts       |  12 +-
 .../benchmark/benchmark.service.ts            | 165 +-----------------
 .../interfaces/benchmark-value.interface.ts   |   0
 .../twitter-bot/twitter-bot.module.ts         |   2 +-
 .../twitter-bot/twitter-bot.service.ts        |   2 +-
 apps/client/src/app/services/data.service.ts  |   8 +-
 15 files changed, 218 insertions(+), 191 deletions(-)
 rename apps/api/src/app/{benchmark/benchmark.controller.ts => endpoints/benchmarks/benchmarks.controller.ts} (93%)
 rename apps/api/src/app/{benchmark/benchmark.module.ts => endpoints/benchmarks/benchmarks.module.ts} (90%)
 create mode 100644 apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
 create mode 100644 apps/api/src/services/benchmark/benchmark.module.ts
 rename apps/api/src/{app => services}/benchmark/benchmark.service.spec.ts (74%)
 rename apps/api/src/{app => services}/benchmark/benchmark.service.ts (65%)
 rename apps/api/src/{app => services}/benchmark/interfaces/benchmark-value.interface.ts (100%)

diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts
index 81c58ff03..d94cdd963 100644
--- a/apps/api/src/app/admin/admin.module.ts
+++ b/apps/api/src/app/admin/admin.module.ts
@@ -1,8 +1,8 @@
-import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
 import { OrderModule } from '@ghostfolio/api/app/order/order.module';
 import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
 import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
 import { ApiModule } from '@ghostfolio/api/services/api/api.module';
+import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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';
diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts
index fb6e90f5d..142109725 100644
--- a/apps/api/src/app/admin/admin.service.ts
+++ b/apps/api/src/app/admin/admin.service.ts
@@ -1,7 +1,7 @@
-import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
 import { OrderService } from '@ghostfolio/api/app/order/order.service';
 import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
 import { environment } from '@ghostfolio/api/environments/environment';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
 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';
diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts
index 3e68bf428..2a515bf43 100644
--- a/apps/api/src/app/app.module.ts
+++ b/apps/api/src/app/app.module.ts
@@ -29,10 +29,10 @@ import { AppController } from './app.controller';
 import { AssetModule } from './asset/asset.module';
 import { AuthDeviceModule } from './auth-device/auth-device.module';
 import { AuthModule } from './auth/auth.module';
-import { BenchmarkModule } from './benchmark/benchmark.module';
 import { CacheModule } from './cache/cache.module';
 import { AiModule } from './endpoints/ai/ai.module';
 import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
+import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
 import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
 import { MarketDataModule } from './endpoints/market-data/market-data.module';
 import { PublicModule } from './endpoints/public/public.module';
@@ -63,7 +63,7 @@ import { UserModule } from './user/user.module';
     AssetModule,
     AuthDeviceModule,
     AuthModule,
-    BenchmarkModule,
+    BenchmarksModule,
     BullModule.forRoot({
       redis: {
         db: parseInt(process.env.REDIS_DB ?? '0', 10),
diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
similarity index 93%
rename from apps/api/src/app/benchmark/benchmark.controller.ts
rename to apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
index d19081011..69383a30d 100644
--- a/apps/api/src/app/benchmark/benchmark.controller.ts
+++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
@@ -3,6 +3,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
 import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
 import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
 import { ApiService } from '@ghostfolio/api/services/api/api.service';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
 import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
 import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
 import type {
@@ -32,13 +33,14 @@ import { AuthGuard } from '@nestjs/passport';
 import { DataSource } from '@prisma/client';
 import { StatusCodes, getReasonPhrase } from 'http-status-codes';
 
-import { BenchmarkService } from './benchmark.service';
+import { BenchmarksService } from './benchmarks.service';
 
-@Controller('benchmark')
-export class BenchmarkController {
+@Controller('benchmarks')
+export class BenchmarksController {
   public constructor(
     private readonly apiService: ApiService,
     private readonly benchmarkService: BenchmarkService,
+    private readonly benchmarksService: BenchmarksService,
     @Inject(REQUEST) private readonly request: RequestWithUser
   ) {}
 
@@ -139,7 +141,7 @@ export class BenchmarkController {
 
     const withExcludedAccounts = withExcludedAccountsParam === 'true';
 
-    return this.benchmarkService.getMarketDataForUser({
+    return this.benchmarksService.getMarketDataForUser({
       dataSource,
       dateRange,
       endDate,
diff --git a/apps/api/src/app/benchmark/benchmark.module.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
similarity index 90%
rename from apps/api/src/app/benchmark/benchmark.module.ts
rename to apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
index 8c0428729..a0f443621 100644
--- a/apps/api/src/app/benchmark/benchmark.module.ts
+++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
@@ -11,6 +11,7 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module';
 import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
 import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
 import { ApiModule } from '@ghostfolio/api/services/api/api.module';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
 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';
@@ -24,12 +25,11 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/sym
 
 import { Module } from '@nestjs/common';
 
-import { BenchmarkController } from './benchmark.controller';
-import { BenchmarkService } from './benchmark.service';
+import { BenchmarksController } from './benchmarks.controller';
+import { BenchmarksService } from './benchmarks.service';
 
 @Module({
-  controllers: [BenchmarkController],
-  exports: [BenchmarkService],
+  controllers: [BenchmarksController],
   imports: [
     ApiModule,
     ConfigurationModule,
@@ -52,6 +52,7 @@ import { BenchmarkService } from './benchmark.service';
     AccountBalanceService,
     AccountService,
     BenchmarkService,
+    BenchmarksService,
     CurrentRateService,
     MarketDataService,
     PortfolioCalculatorFactory,
@@ -59,4 +60,4 @@ import { BenchmarkService } from './benchmark.service';
     RulesService
   ]
 })
-export class BenchmarkModule {}
+export class BenchmarksModule {}
diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
new file mode 100644
index 000000000..237f0d153
--- /dev/null
+++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
@@ -0,0 +1,163 @@
+import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
+import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
+import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
+import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
+import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
+import {
+  AssetProfileIdentifier,
+  BenchmarkMarketDataDetails,
+  Filter
+} from '@ghostfolio/common/interfaces';
+import { DateRange, UserWithSettings } from '@ghostfolio/common/types';
+
+import { Injectable, Logger } from '@nestjs/common';
+import { format, isSameDay } from 'date-fns';
+import { isNumber } from 'lodash';
+
+@Injectable()
+export class BenchmarksService {
+  public constructor(
+    private readonly benchmarkService: BenchmarkService,
+    private readonly exchangeRateDataService: ExchangeRateDataService,
+    private readonly marketDataService: MarketDataService,
+    private readonly portfolioService: PortfolioService,
+    private readonly symbolService: SymbolService
+  ) {}
+
+  public async getMarketDataForUser({
+    dataSource,
+    dateRange,
+    endDate = new Date(),
+    filters,
+    impersonationId,
+    startDate,
+    symbol,
+    user,
+    withExcludedAccounts
+  }: {
+    dateRange: DateRange;
+    endDate?: Date;
+    filters?: Filter[];
+    impersonationId: string;
+    startDate: Date;
+    user: UserWithSettings;
+    withExcludedAccounts?: boolean;
+  } & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> {
+    const marketData: { date: string; value: number }[] = [];
+    const userCurrency = user.Settings.settings.baseCurrency;
+    const userId = user.id;
+
+    const { chart } = await this.portfolioService.getPerformance({
+      dateRange,
+      filters,
+      impersonationId,
+      userId,
+      withExcludedAccounts
+    });
+
+    const [currentSymbolItem, marketDataItems] = await Promise.all([
+      this.symbolService.get({
+        dataGatheringItem: {
+          dataSource,
+          symbol
+        }
+      }),
+      this.marketDataService.marketDataItems({
+        orderBy: {
+          date: 'asc'
+        },
+        where: {
+          dataSource,
+          symbol,
+          date: {
+            in: chart.map(({ date }) => {
+              return resetHours(parseDate(date));
+            })
+          }
+        }
+      })
+    ]);
+
+    const exchangeRates =
+      await this.exchangeRateDataService.getExchangeRatesByCurrency({
+        startDate,
+        currencies: [currentSymbolItem.currency],
+        targetCurrency: userCurrency
+      });
+
+    const exchangeRateAtStartDate =
+      exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
+        format(startDate, DATE_FORMAT)
+      ];
+
+    const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
+      return isSameDay(date, startDate);
+    })?.marketPrice;
+
+    if (!marketPriceAtStartDate) {
+      Logger.error(
+        `No historical market data has been found for ${symbol} (${dataSource}) at ${format(
+          startDate,
+          DATE_FORMAT
+        )}`,
+        'BenchmarkService'
+      );
+
+      return { marketData };
+    }
+
+    for (const marketDataItem of marketDataItems) {
+      const exchangeRate =
+        exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
+          format(marketDataItem.date, DATE_FORMAT)
+        ];
+
+      const exchangeRateFactor =
+        isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
+          ? exchangeRate / exchangeRateAtStartDate
+          : 1;
+
+      marketData.push({
+        date: format(marketDataItem.date, DATE_FORMAT),
+        value:
+          marketPriceAtStartDate === 0
+            ? 0
+            : this.benchmarkService.calculateChangeInPercentage(
+                marketPriceAtStartDate,
+                marketDataItem.marketPrice * exchangeRateFactor
+              ) * 100
+      });
+    }
+
+    const includesEndDate = isSameDay(
+      parseDate(marketData.at(-1).date),
+      endDate
+    );
+
+    if (currentSymbolItem?.marketPrice && !includesEndDate) {
+      const exchangeRate =
+        exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
+          format(endDate, DATE_FORMAT)
+        ];
+
+      const exchangeRateFactor =
+        isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
+          ? exchangeRate / exchangeRateAtStartDate
+          : 1;
+
+      marketData.push({
+        date: format(endDate, DATE_FORMAT),
+        value:
+          this.benchmarkService.calculateChangeInPercentage(
+            marketPriceAtStartDate,
+            currentSymbolItem.marketPrice * exchangeRateFactor
+          ) * 100
+      });
+    }
+
+    return {
+      marketData
+    };
+  }
+}
diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts
index 7903ac397..d7a5ed641 100644
--- a/apps/api/src/app/info/info.module.ts
+++ b/apps/api/src/app/info/info.module.ts
@@ -1,8 +1,8 @@
-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 { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
+import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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';
diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts
index 1af41520d..780860232 100644
--- a/apps/api/src/app/info/info.service.ts
+++ b/apps/api/src/app/info/info.service.ts
@@ -1,7 +1,7 @@
-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 { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
 import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
 import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
 import { PropertyService } from '@ghostfolio/api/services/property/property.service';
diff --git a/apps/api/src/services/benchmark/benchmark.module.ts b/apps/api/src/services/benchmark/benchmark.module.ts
new file mode 100644
index 000000000..870ef244f
--- /dev/null
+++ b/apps/api/src/services/benchmark/benchmark.module.ts
@@ -0,0 +1,24 @@
+import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
+import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.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 { Module } from '@nestjs/common';
+
+import { BenchmarkService } from './benchmark.service';
+
+@Module({
+  exports: [BenchmarkService],
+  imports: [
+    DataProviderModule,
+    MarketDataModule,
+    PrismaModule,
+    PropertyModule,
+    RedisCacheModule,
+    SymbolProfileModule
+  ],
+  providers: [BenchmarkService]
+})
+export class BenchmarkModule {}
diff --git a/apps/api/src/app/benchmark/benchmark.service.spec.ts b/apps/api/src/services/benchmark/benchmark.service.spec.ts
similarity index 74%
rename from apps/api/src/app/benchmark/benchmark.service.spec.ts
rename to apps/api/src/services/benchmark/benchmark.service.spec.ts
index 5371fcdc0..833dbcdfc 100644
--- a/apps/api/src/app/benchmark/benchmark.service.spec.ts
+++ b/apps/api/src/services/benchmark/benchmark.service.spec.ts
@@ -4,17 +4,7 @@ describe('BenchmarkService', () => {
   let benchmarkService: BenchmarkService;
 
   beforeAll(async () => {
-    benchmarkService = new BenchmarkService(
-      null,
-      null,
-      null,
-      null,
-      null,
-      null,
-      null,
-      null,
-      null
-    );
+    benchmarkService = new BenchmarkService(null, null, null, null, null, null);
   });
 
   it('calculateChangeInPercentage', async () => {
diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts
similarity index 65%
rename from apps/api/src/app/benchmark/benchmark.service.ts
rename to apps/api/src/services/benchmark/benchmark.service.ts
index 3efbc74ff..57105da71 100644
--- a/apps/api/src/app/benchmark/benchmark.service.ts
+++ b/apps/api/src/services/benchmark/benchmark.service.ts
@@ -1,8 +1,5 @@
-import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
 import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
-import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.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 { 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';
@@ -11,31 +8,20 @@ import {
   CACHE_TTL_INFINITE,
   PROPERTY_BENCHMARKS
 } from '@ghostfolio/common/config';
-import {
-  DATE_FORMAT,
-  calculateBenchmarkTrend,
-  parseDate,
-  resetHours
-} from '@ghostfolio/common/helper';
+import { calculateBenchmarkTrend } from '@ghostfolio/common/helper';
 import {
   AssetProfileIdentifier,
   Benchmark,
-  BenchmarkMarketDataDetails,
   BenchmarkProperty,
-  BenchmarkResponse,
-  Filter
+  BenchmarkResponse
 } from '@ghostfolio/common/interfaces';
-import {
-  BenchmarkTrend,
-  DateRange,
-  UserWithSettings
-} from '@ghostfolio/common/types';
+import { BenchmarkTrend } from '@ghostfolio/common/types';
 
 import { Injectable, Logger } from '@nestjs/common';
 import { SymbolProfile } from '@prisma/client';
 import { Big } from 'big.js';
-import { addHours, format, isAfter, isSameDay, subDays } from 'date-fns';
-import { isNumber, uniqBy } from 'lodash';
+import { addHours, isAfter, subDays } from 'date-fns';
+import { uniqBy } from 'lodash';
 import ms from 'ms';
 
 import { BenchmarkValue } from './interfaces/benchmark-value.interface';
@@ -46,14 +32,11 @@ export class BenchmarkService {
 
   public constructor(
     private readonly dataProviderService: DataProviderService,
-    private readonly exchangeRateDataService: ExchangeRateDataService,
     private readonly marketDataService: MarketDataService,
     private readonly prismaService: PrismaService,
-    private readonly portfolioService: PortfolioService,
     private readonly propertyService: PropertyService,
     private readonly redisCacheService: RedisCacheService,
-    private readonly symbolProfileService: SymbolProfileService,
-    private readonly symbolService: SymbolService
+    private readonly symbolProfileService: SymbolProfileService
   ) {}
 
   public calculateChangeInPercentage(baseValue: number, currentValue: number) {
@@ -153,142 +136,6 @@ export class BenchmarkService {
       .sort((a, b) => a.name.localeCompare(b.name));
   }
 
-  public async getMarketDataForUser({
-    dataSource,
-    dateRange,
-    endDate = new Date(),
-    filters,
-    impersonationId,
-    startDate,
-    symbol,
-    user,
-    withExcludedAccounts
-  }: {
-    dateRange: DateRange;
-    endDate?: Date;
-    filters?: Filter[];
-    impersonationId: string;
-    startDate: Date;
-    user: UserWithSettings;
-    withExcludedAccounts?: boolean;
-  } & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> {
-    const marketData: { date: string; value: number }[] = [];
-    const userCurrency = user.Settings.settings.baseCurrency;
-    const userId = user.id;
-
-    const { chart } = await this.portfolioService.getPerformance({
-      dateRange,
-      filters,
-      impersonationId,
-      userId,
-      withExcludedAccounts
-    });
-
-    const [currentSymbolItem, marketDataItems] = await Promise.all([
-      this.symbolService.get({
-        dataGatheringItem: {
-          dataSource,
-          symbol
-        }
-      }),
-      this.marketDataService.marketDataItems({
-        orderBy: {
-          date: 'asc'
-        },
-        where: {
-          dataSource,
-          symbol,
-          date: {
-            in: chart.map(({ date }) => {
-              return resetHours(parseDate(date));
-            })
-          }
-        }
-      })
-    ]);
-
-    const exchangeRates =
-      await this.exchangeRateDataService.getExchangeRatesByCurrency({
-        startDate,
-        currencies: [currentSymbolItem.currency],
-        targetCurrency: userCurrency
-      });
-
-    const exchangeRateAtStartDate =
-      exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
-        format(startDate, DATE_FORMAT)
-      ];
-
-    const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
-      return isSameDay(date, startDate);
-    })?.marketPrice;
-
-    if (!marketPriceAtStartDate) {
-      Logger.error(
-        `No historical market data has been found for ${symbol} (${dataSource}) at ${format(
-          startDate,
-          DATE_FORMAT
-        )}`,
-        'BenchmarkService'
-      );
-
-      return { marketData };
-    }
-
-    for (const marketDataItem of marketDataItems) {
-      const exchangeRate =
-        exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
-          format(marketDataItem.date, DATE_FORMAT)
-        ];
-
-      const exchangeRateFactor =
-        isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
-          ? exchangeRate / exchangeRateAtStartDate
-          : 1;
-
-      marketData.push({
-        date: format(marketDataItem.date, DATE_FORMAT),
-        value:
-          marketPriceAtStartDate === 0
-            ? 0
-            : this.calculateChangeInPercentage(
-                marketPriceAtStartDate,
-                marketDataItem.marketPrice * exchangeRateFactor
-              ) * 100
-      });
-    }
-
-    const includesEndDate = isSameDay(
-      parseDate(marketData.at(-1).date),
-      endDate
-    );
-
-    if (currentSymbolItem?.marketPrice && !includesEndDate) {
-      const exchangeRate =
-        exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
-          format(endDate, DATE_FORMAT)
-        ];
-
-      const exchangeRateFactor =
-        isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
-          ? exchangeRate / exchangeRateAtStartDate
-          : 1;
-
-      marketData.push({
-        date: format(endDate, DATE_FORMAT),
-        value:
-          this.calculateChangeInPercentage(
-            marketPriceAtStartDate,
-            currentSymbolItem.marketPrice * exchangeRateFactor
-          ) * 100
-      });
-    }
-
-    return {
-      marketData
-    };
-  }
-
   public async addBenchmark({
     dataSource,
     symbol
diff --git a/apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts b/apps/api/src/services/benchmark/interfaces/benchmark-value.interface.ts
similarity index 100%
rename from apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts
rename to apps/api/src/services/benchmark/interfaces/benchmark-value.interface.ts
diff --git a/apps/api/src/services/twitter-bot/twitter-bot.module.ts b/apps/api/src/services/twitter-bot/twitter-bot.module.ts
index 4a2b1589a..80d53169c 100644
--- a/apps/api/src/services/twitter-bot/twitter-bot.module.ts
+++ b/apps/api/src/services/twitter-bot/twitter-bot.module.ts
@@ -1,5 +1,5 @@
-import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
 import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
+import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
 import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
 import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
 
diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts
index a32882aed..a17585c5b 100644
--- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts
+++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts
@@ -1,5 +1,5 @@
-import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
 import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
 import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
 import {
   ghostfolioFearAndGreedIndexDataSource,
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index 6782644ee..0bc4ebccc 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -303,7 +303,7 @@ export class DataService {
   }
 
   public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
-    return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
+    return this.http.delete<any>(`/api/v1/benchmarks/${dataSource}/${symbol}`);
   }
 
   public deleteOwnUser(aData: DeleteOwnUserDto) {
@@ -358,7 +358,7 @@ export class DataService {
     }
 
     return this.http.get<BenchmarkMarketDataDetails>(
-      `/api/v1/benchmark/${dataSource}/${symbol}/${format(
+      `/api/v1/benchmarks/${dataSource}/${symbol}/${format(
         startDate,
         DATE_FORMAT
       )}`,
@@ -367,7 +367,7 @@ export class DataService {
   }
 
   public fetchBenchmarks() {
-    return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
+    return this.http.get<BenchmarkResponse>('/api/v1/benchmarks');
   }
 
   public fetchExport({
@@ -704,7 +704,7 @@ export class DataService {
   }
 
   public postBenchmark(benchmark: AssetProfileIdentifier) {
-    return this.http.post('/api/v1/benchmark', benchmark);
+    return this.http.post('/api/v1/benchmarks', benchmark);
   }
 
   public postMarketData({