Browse Source

Merge remote-tracking branch 'origin/main' into feature/extend-holdings-endpoint-for-cash

pull/5650/head
KenTandrian 2 weeks ago
parent
commit
bd22dcbfd1
  1. 10
      CHANGELOG.md
  2. 1
      apps/api/src/app/app.module.ts
  3. 4
      apps/api/src/app/asset/asset.controller.ts
  4. 6
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  5. 4
      apps/api/src/app/exchange-rate/exchange-rate.controller.ts
  6. 4
      apps/api/src/app/export/export.controller.ts
  7. 4
      apps/api/src/app/export/export.service.ts
  8. 4
      apps/api/src/app/info/info.controller.ts
  9. 5
      apps/api/src/app/order/order.controller.ts
  10. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  11. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  12. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  13. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  14. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  15. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  16. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  17. 4
      apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts
  18. 4
      apps/api/src/app/symbol/symbol.controller.ts
  19. 10
      apps/api/src/app/symbol/symbol.service.ts
  20. 1
      apps/api/src/models/interfaces/rule-settings.interface.ts
  21. 7
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  22. 3
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  23. 7
      apps/api/src/models/rules/asset-class-cluster-risk/equity.ts
  24. 7
      apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts
  25. 7
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  26. 7
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  27. 7
      apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
  28. 7
      apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
  29. 7
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  30. 7
      apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts
  31. 7
      apps/api/src/models/rules/liquidity/buying-power.ts
  32. 7
      apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts
  33. 7
      apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts
  34. 7
      apps/api/src/models/rules/regional-market-cluster-risk/europe.ts
  35. 7
      apps/api/src/models/rules/regional-market-cluster-risk/japan.ts
  36. 7
      apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
  37. 14
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  38. 2
      apps/api/src/services/data-provider/alpha-vantage/interfaces/interfaces.ts
  39. 12
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  40. 24
      apps/api/src/services/data-provider/data-provider.service.ts
  41. 14
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  42. 14
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  43. 14
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  44. 12
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  45. 10
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
  46. 12
      apps/api/src/services/data-provider/manual/manual.service.ts
  47. 2
      apps/api/src/services/data-provider/rapid-api/interfaces/interfaces.ts
  48. 8
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  49. 14
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  50. 4
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  51. 6
      apps/api/src/services/interfaces/interfaces.ts
  52. 4
      apps/api/src/services/market-data/market-data.service.ts
  53. 4
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  54. 12
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  55. 2
      apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts
  56. 6
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  57. 4
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts
  58. 4
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts
  59. 57
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  60. 2
      apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts
  61. 4
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  62. 4
      apps/client/src/app/components/rule/rule.component.ts
  63. 4
      apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts
  64. 2
      apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts
  65. 4
      apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts
  66. 2
      apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts
  67. 6
      apps/client/src/app/core/notification/interfaces/interfaces.ts
  68. 12
      apps/client/src/app/core/notification/notification.service.ts
  69. 4
      apps/client/src/app/pages/resources/glossary/resources-glossary.component.html
  70. 4
      apps/client/src/app/services/admin.service.ts
  71. 19
      apps/client/src/app/services/data.service.ts
  72. 4
      apps/client/src/app/services/ics/ics.service.ts
  73. 4
      apps/client/src/main.ts
  74. 10
      libs/common/src/lib/interfaces/index.ts
  75. 3
      libs/common/src/lib/interfaces/responses/activity-response.interface.ts
  76. 3
      libs/common/src/lib/interfaces/responses/asset-response.interface.ts
  77. 4
      libs/common/src/lib/interfaces/responses/dividends-response.interface.ts
  78. 6
      libs/common/src/lib/interfaces/responses/export-response.interface.ts
  79. 4
      libs/common/src/lib/interfaces/responses/historical-response.interface.ts
  80. 3
      libs/common/src/lib/interfaces/responses/info-response.interface.ts
  81. 4
      libs/common/src/lib/interfaces/responses/quotes-response.interface.ts
  82. 9
      libs/common/src/lib/interfaces/user.interface.ts
  83. 8
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  84. 163
      libs/ui/src/lib/assistant/assistant.component.ts
  85. 170
      libs/ui/src/lib/assistant/assistant.html
  86. 26
      libs/ui/src/lib/assistant/interfaces/interfaces.ts
  87. 2
      libs/ui/src/lib/portfolio-filter-form/index.ts
  88. 1
      libs/ui/src/lib/portfolio-filter-form/interfaces/index.ts
  89. 8
      libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts
  90. 75
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html
  91. 3
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.scss
  92. 79
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts
  93. 177
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts
  94. 80
      package-lock.json
  95. 2
      package.json
  96. 5
      prisma.config.ts

10
CHANGELOG.md

@ -14,9 +14,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Extracted the portfolio filter form of the assistant to a reusable component
- Formatted the holdings table in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental) - Formatted the holdings table in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental)
- Formatted the holdings table in the _Copy portfolio data to clipboard for AI prompt_ action of the analysis page (experimental) - Formatted the holdings table in the _Copy portfolio data to clipboard for AI prompt_ action on the analysis page (experimental)
- Reverted the explicit configuration of the _Redis_ address family in the job queue module
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
- Upgraded `ioredis` from version `5.6.1` to `5.8.2`
### Fixed
- Fixed the enter key press to submit the form of the login with access token dialog
- Fixed an issue in the database seeding process caused by unresolved environment variables in `DATABASE_URL`
## 2.209.0 - 2025-10-18 ## 2.209.0 - 2025-10-18

1
apps/api/src/app/app.module.ts

@ -71,7 +71,6 @@ import { UserModule } from './user/user.module';
BullModule.forRoot({ BullModule.forRoot({
redis: { redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10), db: parseInt(process.env.REDIS_DB ?? '0', 10),
family: 0,
host: process.env.REDIS_HOST, host: process.env.REDIS_HOST,
password: process.env.REDIS_PASSWORD, password: process.env.REDIS_PASSWORD,
port: parseInt(process.env.REDIS_PORT ?? '6379', 10) port: parseInt(process.env.REDIS_PORT ?? '6379', 10)

4
apps/api/src/app/asset/asset.controller.ts

@ -1,7 +1,7 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service'; import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; 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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces'; import type { AssetResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common'; import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
@ -17,7 +17,7 @@ export class AssetController {
public async getAsset( public async getAsset(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> { ): Promise<AssetResponse> {
const { assetProfile, marketData } = const { assetProfile, marketData } =
await this.adminService.getMarketDataBySymbol({ dataSource, symbol }); await this.adminService.getMarketDataBySymbol({ dataSource, symbol });

6
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -8,7 +8,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
@ -114,7 +114,7 @@ export class GhostfolioService {
try { try {
const promises: Promise<{ const promises: Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}>[] = []; }>[] = [];
for (const dataProviderService of this.getDataProviderServices()) { for (const dataProviderService of this.getDataProviderServices()) {
@ -156,7 +156,7 @@ export class GhostfolioService {
try { try {
const promises: Promise<{ const promises: Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>[] = []; }>[] = [];
for (const dataProviderService of this.getDataProviderServices()) { for (const dataProviderService of this.getDataProviderServices()) {

4
apps/api/src/app/exchange-rate/exchange-rate.controller.ts

@ -1,5 +1,5 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
Controller, Controller,
@ -25,7 +25,7 @@ export class ExchangeRateController {
public async getExchangeRate( public async getExchangeRate(
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> { ): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString); const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({ const exchangeRate = await this.exchangeRateService.getExchangeRate({

4
apps/api/src/app/export/export.controller.ts

@ -1,7 +1,7 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; 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 { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -35,7 +35,7 @@ export class ExportController {
@Query('dataSource') filterByDataSource?: string, @Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string, @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<Export> { ): Promise<ExportResponse> {
const activityIds = filterByActivityIds?.split(',') ?? []; const activityIds = filterByActivityIds?.split(',') ?? [];
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,

4
apps/api/src/app/export/export.service.ts

@ -3,7 +3,7 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { Filter, Export } from '@ghostfolio/common/interfaces'; import { ExportResponse, Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Platform, Prisma } from '@prisma/client'; import { Platform, Prisma } from '@prisma/client';
@ -28,7 +28,7 @@ export class ExportService {
filters?: Filter[]; filters?: Filter[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}): Promise<Export> { }): Promise<ExportResponse> {
const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => { const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
return type; return type;
}); });

4
apps/api/src/app/info/info.controller.ts

@ -1,5 +1,5 @@
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { Controller, Get, UseInterceptors } from '@nestjs/common';
@ -11,7 +11,7 @@ export class InfoController {
@Get() @Get()
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getInfo(): Promise<InfoItem> { public async getInfo(): Promise<InfoResponse> {
return this.infoService.get(); return this.infoService.get();
} }
} }

5
apps/api/src/app/order/order.controller.ts

@ -11,6 +11,7 @@ import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_HIGH,
HEADER_KEY_IMPERSONATION HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { ActivityResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
@ -36,7 +37,7 @@ import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateOrderDto } from './create-order.dto'; import { CreateOrderDto } from './create-order.dto';
import { Activities, Activity } from './interfaces/activities.interface'; import { Activities } from './interfaces/activities.interface';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
import { UpdateOrderDto } from './update-order.dto'; import { UpdateOrderDto } from './update-order.dto';
@ -157,7 +158,7 @@ export class OrderController {
public async getOrderById( public async getOrderById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('id') id: string @Param('id') id: string
): Promise<Activity> { ): Promise<ActivityResponse> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.settings.settings.baseCurrency; const userCurrency = this.request.user.settings.settings.baseCurrency;

4
apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts

@ -1,4 +1,4 @@
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
@ -39,6 +39,6 @@ export const userDummyData = {
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}; };
export function loadExportFile(filePath: string): Export { export function loadExportFile(filePath: string): ExportResponse {
return JSON.parse(readFileSync(filePath, 'utf8')); return JSON.parse(readFileSync(filePath, 'utf8'));
} }

4
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -9,7 +9,7 @@ import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
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';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { import {
@ -193,7 +193,7 @@ export abstract class PortfolioCalculator {
} }
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: DataGatheringItem[] = [];
let firstIndex = transactionPoints.length; let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
let totalInterestWithCurrencyEffect = new Big(0); let totalInterestWithCurrencyEffect = new Big(0);

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: Export; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts

@ -1,8 +1,8 @@
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DateQuery } from './date-query.interface'; import { DateQuery } from './date-query.interface';
export interface GetValuesParams { export interface GetValuesParams {
dataGatheringItems: IDataGatheringItem[]; dataGatheringItems: DataGatheringItem[];
dateQuery: DateQuery; dateQuery: DateQuery;
} }

4
apps/api/src/app/symbol/symbol.controller.ts

@ -1,7 +1,7 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; 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 { 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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { LookupResponse } from '@ghostfolio/common/interfaces'; import { LookupResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -97,7 +97,7 @@ export class SymbolController {
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> { ): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString); const date = parseISO(dateString);
if (!isDate(date)) { if (!isDate(date)) {

10
apps/api/src/app/symbol/symbol.service.ts

@ -1,7 +1,7 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { import {
IDataGatheringItem, DataGatheringItem,
IDataProviderHistoricalResponse DataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -27,7 +27,7 @@ export class SymbolService {
dataGatheringItem, dataGatheringItem,
includeHistoricalData includeHistoricalData
}: { }: {
dataGatheringItem: IDataGatheringItem; dataGatheringItem: DataGatheringItem;
includeHistoricalData?: number; includeHistoricalData?: number;
}): Promise<SymbolItem> { }): Promise<SymbolItem> {
const quotes = await this.dataProviderService.getQuotes({ const quotes = await this.dataProviderService.getQuotes({
@ -75,10 +75,10 @@ export class SymbolService {
dataSource, dataSource,
date = new Date(), date = new Date(),
symbol symbol
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> { }: DataGatheringItem): Promise<DataProviderHistoricalResponse> {
let historicalData: { let historicalData: {
[symbol: string]: { [symbol: string]: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}; };
} = { } = {
[symbol]: {} [symbol]: {}

1
apps/api/src/models/interfaces/rule-settings.interface.ts

@ -1,3 +1,4 @@
export interface RuleSettings { export interface RuleSettings {
isActive: boolean; isActive: boolean;
locale: string;
} }

7
apps/api/src/models/rules/account-cluster-risk/current-investment.ts

@ -121,9 +121,14 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5
}; };

3
apps/api/src/models/rules/account-cluster-risk/single-account.ts

@ -72,8 +72,9 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
}); });
} }
public getSettings({ xRayRules }: UserSettings): RuleSettings { public getSettings({ locale, xRayRules }: UserSettings): RuleSettings {
return { return {
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true isActive: xRayRules?.[this.getKey()]?.isActive ?? true
}; };
} }

7
apps/api/src/models/rules/asset-class-cluster-risk/equity.ts

@ -109,9 +109,14 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.82, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.82,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.78 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.78

7
apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts

@ -109,9 +109,14 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.22, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.22,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.18 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.18

7
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -97,9 +97,14 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true isActive: xRayRules?.[this.getKey()]?.isActive ?? true
}; };
} }

7
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -98,9 +98,14 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5
}; };

7
apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts

@ -104,9 +104,14 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.72, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.72,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.68 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.68

7
apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts

@ -104,9 +104,14 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.32, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.32,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.28 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.28

7
apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts

@ -59,9 +59,14 @@ export class EmergencyFundSetup extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true isActive: xRayRules?.[this.getKey()]?.isActive ?? true
}; };
} }

7
apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts

@ -82,9 +82,14 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01 thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01
}; };

7
apps/api/src/models/rules/liquidity/buying-power.ts

@ -86,9 +86,14 @@ export class BuyingPower extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0
}; };

7
apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02

7
apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts

@ -96,9 +96,14 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.12, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.12,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.08 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.08

7
apps/api/src/models/rules/regional-market-cluster-risk/europe.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.15, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.15,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.11 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.11

7
apps/api/src/models/rules/regional-market-cluster-risk/japan.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.04 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.04

7
apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
}); });
} }
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.69, thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.69,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.65 thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.65

14
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -23,7 +23,7 @@ import { DataSource, SymbolProfile } from '@prisma/client';
import * as Alphavantage from 'alphavantage'; import * as Alphavantage from 'alphavantage';
import { format, isAfter, isBefore, parse } from 'date-fns'; import { format, isAfter, isBefore, parse } from 'date-fns';
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; import { AlphaVantageHistoricalResponse } from './interfaces/interfaces';
@Injectable() @Injectable()
export class AlphaVantageService implements DataProviderInterface { export class AlphaVantageService implements DataProviderInterface {
@ -68,11 +68,11 @@ export class AlphaVantageService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const historicalData: { const historicalData: {
[symbol: string]: IAlphaVantageHistoricalResponse[]; [symbol: string]: AlphaVantageHistoricalResponse[];
} = await this.alphaVantage.crypto.daily( } = await this.alphaVantage.crypto.daily(
symbol symbol
.substring(0, symbol.length - DEFAULT_CURRENCY.length) .substring(0, symbol.length - DEFAULT_CURRENCY.length)
@ -81,7 +81,7 @@ export class AlphaVantageService implements DataProviderInterface {
); );
const response: { const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
response[symbol] = {}; response[symbol] = {};
@ -115,7 +115,7 @@ export class AlphaVantageService implements DataProviderInterface {
} }
public async getQuotes({}: GetQuotesParams): Promise<{ public async getQuotes({}: GetQuotesParams): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
}> { }> {
return {}; return {};
} }

2
apps/api/src/services/data-provider/alpha-vantage/interfaces/interfaces.ts

@ -1 +1 @@
export interface IAlphaVantageHistoricalResponse {} export interface AlphaVantageHistoricalResponse {}

12
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -109,7 +109,7 @@ export class CoinGeckoService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const { error, prices, status } = await fetch( const { error, prices, status } = await fetch(
@ -133,7 +133,7 @@ export class CoinGeckoService implements DataProviderInterface {
} }
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = { } = {
[symbol]: {} [symbol]: {}
}; };
@ -166,8 +166,8 @@ export class CoinGeckoService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

24
apps/api/src/services/data-provider/data-provider.service.ts

@ -2,8 +2,8 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -215,10 +215,10 @@ export class DataProviderService implements OnModuleInit {
from: Date, from: Date,
to: Date to: Date
): Promise<{ ): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
let response: { let response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
if (isEmpty(aItems) || !isValid(from) || !isValid(to)) { if (isEmpty(aItems) || !isValid(from) || !isValid(to)) {
@ -284,7 +284,7 @@ export class DataProviderService implements OnModuleInit {
from: Date; from: Date;
to: Date; to: Date;
}): Promise<{ }): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) { for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
if ( if (
@ -317,11 +317,11 @@ export class DataProviderService implements OnModuleInit {
); );
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
const promises: Promise<{ const promises: Promise<{
data: { [date: string]: IDataProviderHistoricalResponse }; data: { [date: string]: DataProviderHistoricalResponse };
symbol: string; symbol: string;
}>[] = []; }>[] = [];
for (const { dataSource, symbol } of assetProfileIdentifiers) { for (const { dataSource, symbol } of assetProfileIdentifiers) {
@ -329,7 +329,7 @@ export class DataProviderService implements OnModuleInit {
if (dataProvider.canHandle(symbol)) { if (dataProvider.canHandle(symbol)) {
if (symbol === `${DEFAULT_CURRENCY}USX`) { if (symbol === `${DEFAULT_CURRENCY}USX`) {
const data: { const data: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
for (const date of eachDayOfInterval({ end: to, start: from })) { for (const date of eachDayOfInterval({ end: to, start: from })) {
@ -399,10 +399,10 @@ export class DataProviderService implements OnModuleInit {
useCache?: boolean; useCache?: boolean;
user?: UserWithSettings; user?: UserWithSettings;
}): Promise<{ }): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
}> { }> {
const response: { const response: {
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
} = {}; } = {};
const startTimeTotal = performance.now(); const startTimeTotal = performance.now();
@ -716,7 +716,7 @@ export class DataProviderService implements OnModuleInit {
}: { }: {
allData: { allData: {
data: { data: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}; };
symbol: string; symbol: string;
}[]; }[];
@ -728,7 +728,7 @@ export class DataProviderService implements OnModuleInit {
})?.data; })?.data;
const data: { const data: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
for (const date in rootData) { for (const date in rootData) {

14
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
@ -89,7 +89,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol, symbol,
to to
}: GetDividendsParams): Promise<{ }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}> { }> {
symbol = this.convertToEodSymbol(symbol); symbol = this.convertToEodSymbol(symbol);
@ -99,7 +99,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
try { try {
const response: { const response: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
const historicalResult = await fetch( const historicalResult = await fetch(
@ -141,7 +141,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
symbol = this.convertToEodSymbol(symbol); symbol = this.convertToEodSymbol(symbol);
@ -198,8 +198,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

14
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -9,8 +9,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { import {
@ -245,7 +245,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
try { try {
const response: { const response: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
const dividends = await fetch( const dividends = await fetch(
@ -289,11 +289,11 @@ export class FinancialModelingPrepService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
const MAX_YEARS_PER_REQUEST = 5; const MAX_YEARS_PER_REQUEST = 5;
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = { } = {
[symbol]: {} [symbol]: {}
}; };
@ -353,8 +353,8 @@ export class FinancialModelingPrepService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

14
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -9,8 +9,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
@ -111,10 +111,10 @@ export class GhostfolioService implements DataProviderInterface {
symbol, symbol,
to to
}: GetDividendsParams): Promise<{ }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}> { }> {
let dividends: { let dividends: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
try { try {
@ -164,7 +164,7 @@ export class GhostfolioService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const response = await fetch( const response = await fetch(
@ -228,9 +228,9 @@ export class GhostfolioService implements DataProviderInterface {
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ }: GetQuotesParams): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: DataProviderResponse;
}> { }> {
let quotes: { [symbol: string]: IDataProviderResponse } = {}; let quotes: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return quotes; return quotes;

12
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -60,7 +60,7 @@ export class GoogleSheetsService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const sheet = await this.getSheet({ const sheet = await this.getSheet({
@ -71,7 +71,7 @@ export class GoogleSheetsService implements DataProviderInterface {
const rows = await sheet.getRows(); const rows = await sheet.getRows();
const historicalData: { const historicalData: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
rows rows
@ -104,8 +104,8 @@ export class GoogleSheetsService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

10
apps/api/src/services/data-provider/interfaces/data-provider.interface.ts

@ -1,6 +1,6 @@
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
DataProviderInfo, DataProviderInfo,
@ -26,7 +26,7 @@ export interface DataProviderInterface {
symbol, symbol,
to to
}: GetDividendsParams): Promise<{ }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}>; }>;
getHistorical({ getHistorical({
@ -36,7 +36,7 @@ export interface DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>; // TODO: Return only one symbol }>; // TODO: Return only one symbol
getMaxNumberOfSymbolsPerRequest?(): number; getMaxNumberOfSymbolsPerRequest?(): number;
@ -46,7 +46,7 @@ export interface DataProviderInterface {
getQuotes({ getQuotes({
requestTimeout, requestTimeout,
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }>; }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }>;
getTestSymbol(): string; getTestSymbol(): string;

12
apps/api/src/services/data-provider/manual/manual.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -77,7 +77,7 @@ export class ManualService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
@ -88,7 +88,7 @@ export class ManualService implements DataProviderInterface {
if (defaultMarketPrice) { if (defaultMarketPrice) {
const historical: { const historical: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = { } = {
[symbol]: {} [symbol]: {}
}; };
@ -132,8 +132,8 @@ export class ManualService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

2
apps/api/src/services/data-provider/rapid-api/interfaces/interfaces.ts

@ -1 +1 @@
export interface IRapidApiResponse {} export interface RapidApiResponse {}

8
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
ghostfolioFearAndGreedIndexSymbol, ghostfolioFearAndGreedIndexSymbol,
@ -59,7 +59,7 @@ export class RapidApiService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
try { try {
if ( if (
@ -96,7 +96,7 @@ export class RapidApiService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
if (symbols.length <= 0) { if (symbols.length <= 0) {
return {}; return {};
} }

14
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -10,8 +10,8 @@ import {
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, DataProviderHistoricalResponse,
IDataProviderResponse DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -96,7 +96,7 @@ export class YahooFinanceService implements DataProviderInterface {
) )
); );
const response: { const response: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
for (const historicalItem of historicalResult) { for (const historicalItem of historicalResult) {
@ -124,7 +124,7 @@ export class YahooFinanceService implements DataProviderInterface {
symbol, symbol,
to to
}: GetHistoricalParams): Promise<{ }: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> { }> {
if (isSameDay(from, to)) { if (isSameDay(from, to)) {
to = addDays(to, 1); to = addDays(to, 1);
@ -145,7 +145,7 @@ export class YahooFinanceService implements DataProviderInterface {
); );
const response: { const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {}; } = {};
response[symbol] = {}; response[symbol] = {};
@ -183,8 +183,8 @@ export class YahooFinanceService implements DataProviderInterface {
public async getQuotes({ public async getQuotes({
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;

4
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -1,6 +1,6 @@
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -31,7 +31,7 @@ import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interfa
@Injectable() @Injectable()
export class ExchangeRateDataService { export class ExchangeRateDataService {
private currencies: string[] = []; private currencies: string[] = [];
private currencyPairs: IDataGatheringItem[] = []; private currencyPairs: DataGatheringItem[] = [];
private exchangeRates: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {};
public constructor( public constructor(

6
apps/api/src/services/interfaces/interfaces.ts

@ -6,11 +6,11 @@ import { MarketState } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
export interface IDataProviderHistoricalResponse { export interface DataProviderHistoricalResponse {
marketPrice: number; marketPrice: number;
} }
export interface IDataProviderResponse { export interface DataProviderResponse {
currency: string; currency: string;
dataProviderInfo?: DataProviderInfo; dataProviderInfo?: DataProviderInfo;
dataSource: DataSource; dataSource: DataSource;
@ -18,6 +18,6 @@ export interface IDataProviderResponse {
marketState: MarketState; marketState: MarketState;
} }
export interface IDataGatheringItem extends AssetProfileIdentifier { export interface DataGatheringItem extends AssetProfileIdentifier {
date?: Date; date?: Date;
} }

4
apps/api/src/services/market-data/market-data.service.ts

@ -1,6 +1,6 @@
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface'; import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
@ -30,7 +30,7 @@ export class MarketDataService {
dataSource, dataSource,
date = new Date(), date = new Date(),
symbol symbol
}: IDataGatheringItem): Promise<MarketData> { }: DataGatheringItem): Promise<MarketData> {
return await this.prismaService.marketData.findFirst({ return await this.prismaService.marketData.findFirst({
where: { where: {
dataSource, dataSource,

4
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -1,6 +1,6 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
@ -99,7 +99,7 @@ export class DataGatheringProcessor {
), ),
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME
}) })
public async gatherHistoricalMarketData(job: Job<IDataGatheringItem>) { public async gatherHistoricalMarketData(job: Job<DataGatheringItem>) {
const { dataSource, date, symbol } = job.data; const { dataSource, date, symbol } = job.data;
try { try {

12
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -1,7 +1,7 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
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';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -94,7 +94,7 @@ export class DataGatheringService {
}); });
} }
public async gatherSymbol({ dataSource, date, symbol }: IDataGatheringItem) { public async gatherSymbol({ dataSource, date, symbol }: DataGatheringItem) {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
const dataGatheringItems = (await this.getSymbolsMax()) const dataGatheringItems = (await this.getSymbolsMax())
@ -276,7 +276,7 @@ export class DataGatheringService {
dataGatheringItems, dataGatheringItems,
priority priority
}: { }: {
dataGatheringItems: IDataGatheringItem[]; dataGatheringItems: DataGatheringItem[];
priority: number; priority: number;
}) { }) {
await this.addJobsToQueue( await this.addJobsToQueue(
@ -348,7 +348,7 @@ export class DataGatheringService {
}); });
} }
private async getCurrencies7D(): Promise<IDataGatheringItem[]> { private async getCurrencies7D(): Promise<DataGatheringItem[]> {
const assetProfileIdentifiersWithCompleteMarketData = const assetProfileIdentifiersWithCompleteMarketData =
await this.getAssetProfileIdentifiersWithCompleteMarketData(); await this.getAssetProfileIdentifiersWithCompleteMarketData();
@ -376,7 +376,7 @@ export class DataGatheringService {
withUserSubscription = false withUserSubscription = false
}: { }: {
withUserSubscription?: boolean; withUserSubscription?: boolean;
}): Promise<IDataGatheringItem[]> { }): Promise<DataGatheringItem[]> {
const symbolProfiles = const symbolProfiles =
await this.symbolProfileService.getActiveSymbolProfilesByUserSubscription( await this.symbolProfileService.getActiveSymbolProfilesByUserSubscription(
{ {
@ -407,7 +407,7 @@ export class DataGatheringService {
}); });
} }
private async getSymbolsMax(): Promise<IDataGatheringItem[]> { private async getSymbolsMax(): Promise<DataGatheringItem[]> {
const benchmarkAssetProfileIdMap: { [key: string]: boolean } = {}; const benchmarkAssetProfileIdMap: { [key: string]: boolean } = {};
( (
(await this.propertyService.getByKey<BenchmarkProperty[]>( (await this.propertyService.getByKey<BenchmarkProperty[]>(

2
apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts

@ -1,7 +1,7 @@
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export interface IPortfolioSnapshotQueueJob { export interface PortfolioSnapshotQueueJob {
calculationType: PerformanceCalculationType; calculationType: PerformanceCalculationType;
filters: Filter[]; filters: Filter[];
userCurrency: string; userCurrency: string;

6
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts

@ -16,7 +16,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { Job } from 'bull'; import { Job } from 'bull';
import { addMilliseconds } from 'date-fns'; import { addMilliseconds } from 'date-fns';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable() @Injectable()
@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) @Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE)
@ -37,9 +37,7 @@ export class PortfolioSnapshotProcessor {
), ),
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME
}) })
public async calculatePortfolioSnapshot( public async calculatePortfolioSnapshot(job: Job<PortfolioSnapshotQueueJob>) {
job: Job<IPortfolioSnapshotQueueJob>
) {
try { try {
const startTime = performance.now(); const startTime = performance.now();

4
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts

@ -1,13 +1,13 @@
import { Job, JobOptions } from 'bull'; import { Job, JobOptions } from 'bull';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
export const PortfolioSnapshotServiceMock = { export const PortfolioSnapshotServiceMock = {
addJobToQueue({ addJobToQueue({
opts opts
}: { }: {
data: IPortfolioSnapshotQueueJob; data: PortfolioSnapshotQueueJob;
name: string; name: string;
opts?: JobOptions; opts?: JobOptions;
}): Promise<Job<any>> { }): Promise<Job<any>> {

4
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts

@ -4,7 +4,7 @@ import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { JobOptions, Queue } from 'bull'; import { JobOptions, Queue } from 'bull';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable() @Injectable()
export class PortfolioSnapshotService { export class PortfolioSnapshotService {
@ -18,7 +18,7 @@ export class PortfolioSnapshotService {
name, name,
opts opts
}: { }: {
data: IPortfolioSnapshotQueueJob; data: PortfolioSnapshotQueueJob;
name: string; name: string;
opts?: JobOptions; opts?: JobOptions;
}) { }) {

57
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -2,13 +2,14 @@
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">
<form class="w-100" (ngSubmit)="onLoginWithAccessToken()"> <form class="w-100">
<mat-form-field appearance="outline" class="without-hint w-100"> <mat-form-field appearance="outline" class="without-hint w-100">
<mat-label i18n>Security Token</mat-label> <mat-label i18n>Security Token</mat-label>
<input <input
matInput matInput
[formControl]="accessTokenFormControl" [formControl]="accessTokenFormControl"
[type]="isAccessTokenHidden ? 'password' : 'text'" [type]="isAccessTokenHidden ? 'password' : 'text'"
(keydown.enter)="onLoginWithAccessToken(); $event.preventDefault()"
/> />
<button <button
mat-button mat-button
@ -21,35 +22,37 @@
/> />
</button> </button>
</mat-form-field> </mat-form-field>
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
</form> </form>
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
</div> </div>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<div class="flex-grow-1"> <div class="flex-grow-1">
<mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)" <mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)"

2
apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts

@ -3,7 +3,7 @@ import {
XRayRulesSettings XRayRulesSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
export interface IRuleSettingsDialogParams { export interface RuleSettingsDialogParams {
categoryName: string; categoryName: string;
locale: string; locale: string;
rule: PortfolioReportRule; rule: PortfolioReportRule;

4
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts

@ -12,7 +12,7 @@ import {
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import { MatSliderModule } from '@angular/material/slider'; import { MatSliderModule } from '@angular/material/slider';
import { IRuleSettingsDialogParams } from './interfaces/interfaces'; import { RuleSettingsDialogParams } from './interfaces/interfaces';
@Component({ @Component({
imports: [ imports: [
@ -31,7 +31,7 @@ export class GfRuleSettingsDialogComponent {
public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment'];
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, @Inject(MAT_DIALOG_DATA) public data: RuleSettingsDialogParams,
public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent> public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent>
) {} ) {}
} }

4
apps/client/src/app/components/rule/rule.component.ts

@ -31,7 +31,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { IRuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces'; import { RuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces';
import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component'; import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component';
@Component({ @Component({
@ -83,7 +83,7 @@ export class GfRuleComponent implements OnInit {
rule, rule,
categoryName: this.categoryName, categoryName: this.categoryName,
settings: this.settings settings: this.settings
} as IRuleSettingsDialogParams, } as RuleSettingsDialogParams,
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });

4
apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts

@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { IAlertDialogParams } from './interfaces/interfaces'; import { AlertDialogParams } from './interfaces/interfaces';
@Component({ @Component({
imports: [MatButtonModule, MatDialogModule], imports: [MatButtonModule, MatDialogModule],
@ -17,7 +17,7 @@ export class GfAlertDialogComponent {
public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {} public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {}
public initialize(aParams: IAlertDialogParams) { public initialize(aParams: AlertDialogParams) {
this.discardLabel = aParams.discardLabel; this.discardLabel = aParams.discardLabel;
this.message = aParams.message; this.message = aParams.message;
this.title = aParams.title; this.title = aParams.title;

2
apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts

@ -1,4 +1,4 @@
export interface IAlertDialogParams { export interface AlertDialogParams {
confirmLabel?: string; confirmLabel?: string;
discardLabel?: string; discardLabel?: string;
message?: string; message?: string;

4
apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts

@ -3,7 +3,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ConfirmationDialogType } from './confirmation-dialog.type'; import { ConfirmationDialogType } from './confirmation-dialog.type';
import { IConfirmDialogParams } from './interfaces/interfaces'; import { ConfirmDialogParams } from './interfaces/interfaces';
@Component({ @Component({
imports: [MatButtonModule, MatDialogModule], imports: [MatButtonModule, MatDialogModule],
@ -29,7 +29,7 @@ export class GfConfirmationDialogComponent {
} }
} }
public initialize(aParams: IConfirmDialogParams) { public initialize(aParams: ConfirmDialogParams) {
this.confirmLabel = aParams.confirmLabel; this.confirmLabel = aParams.confirmLabel;
this.confirmType = aParams.confirmType; this.confirmType = aParams.confirmType;
this.discardLabel = aParams.discardLabel; this.discardLabel = aParams.discardLabel;

2
apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts

@ -1,6 +1,6 @@
import { ConfirmationDialogType } from '../confirmation-dialog.type'; import { ConfirmationDialogType } from '../confirmation-dialog.type';
export interface IConfirmDialogParams { export interface ConfirmDialogParams {
confirmLabel?: string; confirmLabel?: string;
confirmType: ConfirmationDialogType; confirmType: ConfirmationDialogType;
discardLabel?: string; discardLabel?: string;

6
apps/client/src/app/core/notification/interfaces/interfaces.ts

@ -1,13 +1,13 @@
import { ConfirmationDialogType } from '../confirmation-dialog/confirmation-dialog.type'; import { ConfirmationDialogType } from '../confirmation-dialog/confirmation-dialog.type';
export interface IAlertParams { export interface AlertParams {
discardFn?: () => void; discardFn?: () => void;
discardLabel?: string; discardLabel?: string;
message?: string; message?: string;
title: string; title: string;
} }
export interface IConfirmParams { export interface ConfirmParams {
confirmFn: () => void; confirmFn: () => void;
confirmLabel?: string; confirmLabel?: string;
confirmType?: ConfirmationDialogType; confirmType?: ConfirmationDialogType;
@ -18,7 +18,7 @@ export interface IConfirmParams {
title: string; title: string;
} }
export interface IPromptParams { export interface PromptParams {
confirmFn: (value: string) => void; confirmFn: (value: string) => void;
confirmLabel?: string; confirmLabel?: string;
defaultValue?: string; defaultValue?: string;

12
apps/client/src/app/core/notification/notification.service.ts

@ -8,9 +8,9 @@ import { GfAlertDialogComponent } from './alert-dialog/alert-dialog.component';
import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component'; import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
import { ConfirmationDialogType } from './confirmation-dialog/confirmation-dialog.type'; import { ConfirmationDialogType } from './confirmation-dialog/confirmation-dialog.type';
import { import {
IAlertParams, AlertParams,
IConfirmParams, ConfirmParams,
IPromptParams PromptParams
} from './interfaces/interfaces'; } from './interfaces/interfaces';
import { GfPromptDialogComponent } from './prompt-dialog/prompt-dialog.component'; import { GfPromptDialogComponent } from './prompt-dialog/prompt-dialog.component';
@ -21,7 +21,7 @@ export class NotificationService {
public constructor(private matDialog: MatDialog) {} public constructor(private matDialog: MatDialog) {}
public alert(aParams: IAlertParams) { public alert(aParams: AlertParams) {
if (!aParams.discardLabel) { if (!aParams.discardLabel) {
aParams.discardLabel = translate('CLOSE'); aParams.discardLabel = translate('CLOSE');
} }
@ -45,7 +45,7 @@ export class NotificationService {
}); });
} }
public confirm(aParams: IConfirmParams) { public confirm(aParams: ConfirmParams) {
if (!aParams.confirmLabel) { if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('YES'); aParams.confirmLabel = translate('YES');
} }
@ -78,7 +78,7 @@ export class NotificationService {
}); });
} }
public prompt(aParams: IPromptParams) { public prompt(aParams: PromptParams) {
if (!aParams.confirmLabel) { if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('OK'); aParams.confirmLabel = translate('OK');
} }

4
apps/client/src/app/pages/resources/glossary/resources-glossary.component.html

@ -105,8 +105,8 @@
<h3 class="h5 mt-0">Personal Finance Tools</h3> <h3 class="h5 mt-0">Personal Finance Tools</h3>
<div class="mb-1"> <div class="mb-1">
Personal finance tools are software applications that help Personal finance tools are software applications that help
individuals manage their money, track expenses, set budgets, manage your money, track expenses, set budgets, monitor
monitor investments, and make informed financial decisions. investments, and make informed financial decisions.
</div> </div>
<div> <div>
<a [routerLink]="routerLinkResourcesPersonalFinanceTools" <a [routerLink]="routerLinkResourcesPersonalFinanceTools"

4
apps/client/src/app/services/admin.service.ts

@ -1,7 +1,7 @@
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_SKIP_INTERCEPTOR,
HEADER_KEY_TOKEN HEADER_KEY_TOKEN
@ -208,7 +208,7 @@ export class AdminService {
}) { }) {
const url = `/api/v1/symbol/${dataSource}/${symbol}/${dateString}`; const url = `/api/v1/symbol/${dataSource}/${symbol}/${dateString}`;
return this.http.get<IDataProviderHistoricalResponse>(url); return this.http.get<DataProviderHistoricalResponse>(url);
} }
public patchAssetProfile( public patchAssetProfile(

19
apps/client/src/app/services/data.service.ts

@ -9,17 +9,14 @@ import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto'
import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { CreateWatchlistItemDto } from '@ghostfolio/api/app/endpoints/watchlist/create-watchlist-item.dto'; import { CreateWatchlistItemDto } from '@ghostfolio/api/app/endpoints/watchlist/create-watchlist-item.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
Activities,
Activity
} from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto'; import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
import { UpdateOwnAccessTokenDto } from '@ghostfolio/api/app/user/update-own-access-token.dto'; import { UpdateOwnAccessTokenDto } from '@ghostfolio/api/app/user/update-own-access-token.dto';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
@ -27,13 +24,15 @@ import {
AccessTokenResponse, AccessTokenResponse,
AccountBalancesResponse, AccountBalancesResponse,
AccountsResponse, AccountsResponse,
ActivityResponse,
AiPromptResponse, AiPromptResponse,
ApiKeyResponse, ApiKeyResponse,
AssetProfileIdentifier, AssetProfileIdentifier,
AssetResponse,
BenchmarkMarketDataDetailsResponse, BenchmarkMarketDataDetailsResponse,
BenchmarkResponse, BenchmarkResponse,
DataProviderHealthResponse, DataProviderHealthResponse,
Export, ExportResponse,
Filter, Filter,
ImportResponse, ImportResponse,
InfoItem, InfoItem,
@ -247,7 +246,7 @@ export class DataService {
} }
public fetchActivity(aActivityId: string) { public fetchActivity(aActivityId: string) {
return this.http.get<Activity>(`/api/v1/order/${aActivityId}`).pipe( return this.http.get<ActivityResponse>(`/api/v1/order/${aActivityId}`).pipe(
map((activity) => { map((activity) => {
activity.createdAt = parseISO(activity.createdAt as unknown as string); activity.createdAt = parseISO(activity.createdAt as unknown as string);
activity.date = parseISO(activity.date as unknown as string); activity.date = parseISO(activity.date as unknown as string);
@ -291,7 +290,7 @@ export class DataService {
date: Date; date: Date;
symbol: string; symbol: string;
}) { }) {
return this.http.get<IDataProviderHistoricalResponse>( return this.http.get<DataProviderHistoricalResponse>(
`/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}` `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}`
); );
} }
@ -345,7 +344,7 @@ export class DataService {
public fetchAsset({ public fetchAsset({
dataSource, dataSource,
symbol symbol
}: AssetProfileIdentifier): Observable<MarketDataDetailsResponse> { }: AssetProfileIdentifier): Observable<AssetResponse> {
return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe( return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe(
map((data) => { map((data) => {
for (const item of data.marketData) { for (const item of data.marketData) {
@ -406,7 +405,7 @@ export class DataService {
params = params.append('activityIds', activityIds.join(',')); params = params.append('activityIds', activityIds.join(','));
} }
return this.http.get<Export>('/api/v1/export', { return this.http.get<ExportResponse>('/api/v1/export', {
params params
}); });
} }

4
apps/client/src/app/services/ics/ics.service.ts

@ -1,5 +1,5 @@
import { capitalize } from '@ghostfolio/common/helper'; import { capitalize } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Type } from '@prisma/client'; import { Type } from '@prisma/client';
@ -13,7 +13,7 @@ export class IcsService {
private readonly ICS_LINE_BREAK = '\r\n'; private readonly ICS_LINE_BREAK = '\r\n';
public transformActivitiesToIcsContent( public transformActivitiesToIcsContent(
aActivities: Export['activities'] aActivities: ExportResponse['activities']
): string { ): string {
const header = [ const header = [
'BEGIN:VCALENDAR', 'BEGIN:VCALENDAR',

4
apps/client/src/main.ts

@ -1,5 +1,5 @@
import { locale } from '@ghostfolio/common/config'; import { locale } from '@ghostfolio/common/config';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoResponse } from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
@ -11,7 +11,7 @@ import { environment } from './environments/environment';
(async () => { (async () => {
const response = await fetch('/api/v1/info'); const response = await fetch('/api/v1/info');
const info: InfoItem = await response.json(); const info: InfoResponse = await response.json();
const utmSource = window.localStorage.getItem('utm_source') as const utmSource = window.localStorage.getItem('utm_source') as
| 'ios' | 'ios'
| 'trusted-web-activity'; | 'trusted-web-activity';

10
libs/common/src/lib/interfaces/index.ts

@ -15,7 +15,6 @@ import type { Benchmark } from './benchmark.interface';
import type { Coupon } from './coupon.interface'; import type { Coupon } from './coupon.interface';
import type { DataProviderInfo } from './data-provider-info.interface'; import type { DataProviderInfo } from './data-provider-info.interface';
import type { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; import type { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
import type { Export } from './export.interface';
import type { FilterGroup } from './filter-group.interface'; import type { FilterGroup } from './filter-group.interface';
import type { Filter } from './filter.interface'; import type { Filter } from './filter.interface';
import type { FireWealth } from './fire-wealth.interface'; import type { FireWealth } from './fire-wealth.interface';
@ -38,8 +37,10 @@ import type { Product } from './product';
import type { AccessTokenResponse } from './responses/access-token-response.interface'; import type { AccessTokenResponse } from './responses/access-token-response.interface';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface'; import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
import type { AccountsResponse } from './responses/accounts-response.interface'; import type { AccountsResponse } from './responses/accounts-response.interface';
import type { ActivityResponse } from './responses/activity-response.interface';
import type { AiPromptResponse } from './responses/ai-prompt-response.interface'; import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface'; import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { AssetResponse } from './responses/asset-response.interface';
import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface'; import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { DataEnhancerHealthResponse } from './responses/data-enhancer-health-response.interface'; import type { DataEnhancerHealthResponse } from './responses/data-enhancer-health-response.interface';
@ -48,8 +49,10 @@ import type { DataProviderGhostfolioStatusResponse } from './responses/data-prov
import type { DataProviderHealthResponse } from './responses/data-provider-health-response.interface'; import type { DataProviderHealthResponse } from './responses/data-provider-health-response.interface';
import type { DividendsResponse } from './responses/dividends-response.interface'; import type { DividendsResponse } from './responses/dividends-response.interface';
import type { ResponseError } from './responses/errors.interface'; import type { ResponseError } from './responses/errors.interface';
import type { ExportResponse } from './responses/export-response.interface';
import type { HistoricalResponse } from './responses/historical-response.interface'; import type { HistoricalResponse } from './responses/historical-response.interface';
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
import type { InfoResponse } from './responses/info-response.interface';
import type { LookupResponse } from './responses/lookup-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface'; import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface'; import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
@ -80,6 +83,7 @@ export {
AccountBalance, AccountBalance,
AccountBalancesResponse, AccountBalancesResponse,
AccountsResponse, AccountsResponse,
ActivityResponse,
AdminData, AdminData,
AdminJobs, AdminJobs,
AdminMarketData, AdminMarketData,
@ -90,6 +94,7 @@ export {
ApiKeyResponse, ApiKeyResponse,
AssetClassSelectorOption, AssetClassSelectorOption,
AssetProfileIdentifier, AssetProfileIdentifier,
AssetResponse,
Benchmark, Benchmark,
BenchmarkMarketDataDetailsResponse, BenchmarkMarketDataDetailsResponse,
BenchmarkProperty, BenchmarkProperty,
@ -102,7 +107,7 @@ export {
DataProviderInfo, DataProviderInfo,
DividendsResponse, DividendsResponse,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Export, ExportResponse,
Filter, Filter,
FilterGroup, FilterGroup,
FireWealth, FireWealth,
@ -112,6 +117,7 @@ export {
HoldingWithParents, HoldingWithParents,
ImportResponse, ImportResponse,
InfoItem, InfoItem,
InfoResponse,
InvestmentItem, InvestmentItem,
LineChartItem, LineChartItem,
LookupItem, LookupItem,

3
libs/common/src/lib/interfaces/responses/activity-response.interface.ts

@ -0,0 +1,3 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
export interface ActivityResponse extends Activity {}

3
libs/common/src/lib/interfaces/responses/asset-response.interface.ts

@ -0,0 +1,3 @@
import type { AdminMarketDataDetails } from '../admin-market-data-details.interface';
export interface AssetResponse extends AdminMarketDataDetails {}

4
libs/common/src/lib/interfaces/responses/dividends-response.interface.ts

@ -1,7 +1,7 @@
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface DividendsResponse { export interface DividendsResponse {
dividends: { dividends: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}; };
} }

6
libs/common/src/lib/interfaces/export.interface.ts → libs/common/src/lib/interfaces/responses/export-response.interface.ts

@ -7,10 +7,10 @@ import {
Tag Tag
} from '@prisma/client'; } from '@prisma/client';
import { AccountBalance } from './account-balance.interface'; import { AccountBalance } from '../account-balance.interface';
import { MarketData } from './market-data.interface'; import { MarketData } from '../market-data.interface';
export interface Export { export interface ExportResponse {
accounts: (Omit<Account, 'createdAt' | 'updatedAt' | 'userId'> & { accounts: (Omit<Account, 'createdAt' | 'updatedAt' | 'userId'> & {
balances: AccountBalance[]; balances: AccountBalance[];
})[]; })[];

4
libs/common/src/lib/interfaces/responses/historical-response.interface.ts

@ -1,7 +1,7 @@
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface HistoricalResponse { export interface HistoricalResponse {
historicalData: { historicalData: {
[date: string]: IDataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
}; };
} }

3
libs/common/src/lib/interfaces/responses/info-response.interface.ts

@ -0,0 +1,3 @@
import { InfoItem } from '../index';
export interface InfoResponse extends InfoItem {}

4
libs/common/src/lib/interfaces/responses/quotes-response.interface.ts

@ -1,5 +1,5 @@
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface QuotesResponse { export interface QuotesResponse {
quotes: { [symbol: string]: IDataProviderResponse }; quotes: { [symbol: string]: DataProviderResponse };
} }

9
libs/common/src/lib/interfaces/user.interface.ts

@ -1,6 +1,9 @@
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; import {
AccountWithPlatform,
SubscriptionType
} from '@ghostfolio/common/types';
import { Access, Account, Tag } from '@prisma/client'; import { Access, Tag } from '@prisma/client';
import { SubscriptionOffer } from './subscription-offer.interface'; import { SubscriptionOffer } from './subscription-offer.interface';
import { SystemMessage } from './system-message.interface'; import { SystemMessage } from './system-message.interface';
@ -9,7 +12,7 @@ import { UserSettings } from './user-settings.interface';
// TODO: Compare with UserWithSettings // TODO: Compare with UserWithSettings
export interface User { export interface User {
access: Pick<Access, 'alias' | 'id' | 'permissions'>[]; access: Pick<Access, 'alias' | 'id' | 'permissions'>[];
accounts: Account[]; accounts: AccountWithPlatform[];
activitiesCount: number; activitiesCount: number;
dateOfFirstActivity: Date; dateOfFirstActivity: Date;
id: string; id: string;

8
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts

@ -18,8 +18,8 @@ import { Params, RouterModule } from '@angular/router';
import { SearchMode } from '../enums/search-mode'; import { SearchMode } from '../enums/search-mode';
import { import {
IAssetSearchResultItem, AssetSearchResultItem,
ISearchResultItem SearchResultItem
} from '../interfaces/interfaces'; } from '../interfaces/interfaces';
@Component({ @Component({
@ -37,7 +37,7 @@ export class GfAssistantListItemComponent
return this.hasFocus; return this.hasFocus;
} }
@Input() item: ISearchResultItem; @Input() item: SearchResultItem;
@Output() clicked = new EventEmitter<void>(); @Output() clicked = new EventEmitter<void>();
@ -86,7 +86,7 @@ export class GfAssistantListItemComponent
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
public isAsset(item: ISearchResultItem): item is IAssetSearchResultItem { public isAsset(item: SearchResultItem): item is AssetSearchResultItem {
return ( return (
(item.mode === SearchMode.ASSET_PROFILE || (item.mode === SearchMode.ASSET_PROFILE ||
item.mode === SearchMode.HOLDING) && item.mode === SearchMode.HOLDING) &&

163
libs/ui/src/lib/assistant/assistant.component.ts

@ -1,11 +1,10 @@
import { GfSymbolPipe } from '@ghostfolio/client/pipes/symbol/symbol.pipe';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface'; import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { DateRange } from '@ghostfolio/common/types'; import { AccountWithPlatform, DateRange } from '@ghostfolio/common/types';
import { FocusKeyManager } from '@angular/cdk/a11y'; import { FocusKeyManager } from '@angular/cdk/a11y';
import { import {
@ -25,19 +24,14 @@ import {
ViewChild, ViewChild,
ViewChildren ViewChildren
} from '@angular/core'; } from '@angular/core';
import { import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
FormBuilder,
FormControl,
FormsModule,
ReactiveFormsModule
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { Account, AssetClass, DataSource } from '@prisma/client'; import { AssetClass, DataSource } from '@prisma/client';
import { differenceInYears } from 'date-fns'; import { differenceInYears } from 'date-fns';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
@ -60,14 +54,17 @@ import {
tap tap
} from 'rxjs/operators'; } from 'rxjs/operators';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
import { translate } from '../i18n'; import { translate } from '../i18n';
import {
GfPortfolioFilterFormComponent,
PortfolioFilterFormValue
} from '../portfolio-filter-form';
import { GfAssistantListItemComponent } from './assistant-list-item/assistant-list-item.component'; import { GfAssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
import { SearchMode } from './enums/search-mode'; import { SearchMode } from './enums/search-mode';
import { import {
IDateRangeOption, DateRangeOption,
ISearchResultItem, SearchResultItem,
ISearchResults SearchResults
} from './interfaces/interfaces'; } from './interfaces/interfaces';
@Component({ @Component({
@ -75,8 +72,7 @@ import {
imports: [ imports: [
FormsModule, FormsModule,
GfAssistantListItemComponent, GfAssistantListItemComponent,
GfEntityLogoComponent, GfPortfolioFilterFormComponent,
GfSymbolPipe,
IonIcon, IonIcon,
MatButtonModule, MatButtonModule,
MatFormFieldModule, MatFormFieldModule,
@ -141,16 +137,10 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5; public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5;
public accounts: Account[] = []; public accounts: AccountWithPlatform[] = [];
public assetClasses: Filter[] = []; public assetClasses: Filter[] = [];
public dateRangeFormControl = new FormControl<string>(undefined); public dateRangeFormControl = new FormControl<string>(undefined);
public dateRangeOptions: IDateRangeOption[] = []; public dateRangeOptions: DateRangeOption[] = [];
public filterForm = this.formBuilder.group({
account: new FormControl<string>(undefined),
assetClass: new FormControl<string>(undefined),
holding: new FormControl<PortfolioPosition>(undefined),
tag: new FormControl<string>(undefined)
});
public holdings: PortfolioPosition[] = []; public holdings: PortfolioPosition[] = [];
public isLoading = { public isLoading = {
accounts: false, accounts: false,
@ -160,8 +150,16 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}; };
public isOpen = false; public isOpen = false;
public placeholder = $localize`Find account, holding or page...`; public placeholder = $localize`Find account, holding or page...`;
public portfolioFilterFormControl = new FormControl<PortfolioFilterFormValue>(
{
account: null,
assetClass: null,
holding: null,
tag: null
}
);
public searchFormControl = new FormControl(''); public searchFormControl = new FormControl('');
public searchResults: ISearchResults = { public searchResults: SearchResults = {
accounts: [], accounts: [],
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
@ -186,8 +184,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService
private formBuilder: FormBuilder
) { ) {
addIcons({ closeCircleOutline, closeOutline, searchOutline }); addIcons({ closeCircleOutline, closeOutline, searchOutline });
} }
@ -229,7 +226,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
} as ISearchResults; } as SearchResults;
if (!searchTerm) { if (!searchTerm) {
return of(results).pipe( return of(results).pipe(
@ -244,8 +241,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
); );
} }
// Accounts const accounts$: Observable<Partial<SearchResults>> =
const accounts$: Observable<Partial<ISearchResults>> =
this.searchAccounts(searchTerm).pipe( this.searchAccounts(searchTerm).pipe(
map((accounts) => ({ map((accounts) => ({
accounts: accounts.slice( accounts: accounts.slice(
@ -255,7 +251,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
})), })),
catchError((error) => { catchError((error) => {
console.error('Error fetching accounts for assistant:', error); console.error('Error fetching accounts for assistant:', error);
return of({ accounts: [] as ISearchResultItem[] }); return of({ accounts: [] as SearchResultItem[] });
}), }),
tap(() => { tap(() => {
this.isLoading.accounts = false; this.isLoading.accounts = false;
@ -263,8 +259,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}) })
); );
// Asset profiles const assetProfiles$: Observable<Partial<SearchResults>> = this
const assetProfiles$: Observable<Partial<ISearchResults>> = this
.hasPermissionToAccessAdminControl .hasPermissionToAccessAdminControl
? this.searchAssetProfiles(searchTerm).pipe( ? this.searchAssetProfiles(searchTerm).pipe(
map((assetProfiles) => ({ map((assetProfiles) => ({
@ -278,22 +273,21 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
'Error fetching asset profiles for assistant:', 'Error fetching asset profiles for assistant:',
error error
); );
return of({ assetProfiles: [] as ISearchResultItem[] }); return of({ assetProfiles: [] as SearchResultItem[] });
}), }),
tap(() => { tap(() => {
this.isLoading.assetProfiles = false; this.isLoading.assetProfiles = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}) })
) )
: of({ assetProfiles: [] as ISearchResultItem[] }).pipe( : of({ assetProfiles: [] as SearchResultItem[] }).pipe(
tap(() => { tap(() => {
this.isLoading.assetProfiles = false; this.isLoading.assetProfiles = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}) })
); );
// Holdings const holdings$: Observable<Partial<SearchResults>> =
const holdings$: Observable<Partial<ISearchResults>> =
this.searchHoldings(searchTerm).pipe( this.searchHoldings(searchTerm).pipe(
map((holdings) => ({ map((holdings) => ({
holdings: holdings.slice( holdings: holdings.slice(
@ -303,7 +297,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
})), })),
catchError((error) => { catchError((error) => {
console.error('Error fetching holdings for assistant:', error); console.error('Error fetching holdings for assistant:', error);
return of({ holdings: [] as ISearchResultItem[] }); return of({ holdings: [] as SearchResultItem[] });
}), }),
tap(() => { tap(() => {
this.isLoading.holdings = false; this.isLoading.holdings = false;
@ -311,8 +305,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}) })
); );
// Quick links const quickLinks$: Observable<Partial<SearchResults>> = of(
const quickLinks$: Observable<Partial<ISearchResults>> = of(
this.searchQuickLinks(searchTerm) this.searchQuickLinks(searchTerm)
).pipe( ).pipe(
map((quickLinks) => ({ map((quickLinks) => ({
@ -327,10 +320,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}) })
); );
// Merge all results
return merge(accounts$, assetProfiles$, holdings$, quickLinks$).pipe( return merge(accounts$, assetProfiles$, holdings$, quickLinks$).pipe(
scan( scan(
(acc: ISearchResults, curr: Partial<ISearchResults>) => ({ (acc: SearchResults, curr: Partial<SearchResults>) => ({
...acc, ...acc,
...curr ...curr
}), }),
@ -339,7 +331,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
} as ISearchResults } as SearchResults
) )
); );
}), }),
@ -362,22 +354,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
quickLinks: [] quickLinks: []
}; };
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
},
complete: () => {
this.isLoading = {
accounts: false,
assetProfiles: false,
holdings: false,
quickLinks: false
};
this.changeDetectorRef.markForCheck();
} }
}); });
} }
public ngOnChanges() { public ngOnChanges() {
this.accounts = this.user?.accounts ?? [];
this.dateRangeOptions = [ this.dateRangeOptions = [
{ {
label: $localize`Today`, label: $localize`Today`,
@ -445,7 +426,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null); this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null);
this.filterForm.disable({ emitEvent: false }); if (this.hasPermissionToChangeFilters) {
this.portfolioFilterFormControl.enable({ emitEvent: false });
} else {
this.portfolioFilterFormControl.disable({ emitEvent: false });
}
this.tags = this.tags =
this.user?.tags this.user?.tags
@ -459,29 +444,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
type: 'TAG' type: 'TAG'
}; };
}) ?? []; }) ?? [];
if (this.tags.length === 0) {
this.filterForm.get('tag').disable({ emitEvent: false });
}
}
public hasFilter(aFormValue: { [key: string]: string }) {
return Object.values(aFormValue).some((value) => {
return !!value;
});
}
public holdingComparisonFunction(
option: PortfolioPosition,
value: PortfolioPosition
): boolean {
if (value === null) {
return false;
}
return (
getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value)
);
} }
public initialize() { public initialize() {
@ -527,36 +489,35 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
.sort((a, b) => { .sort((a, b) => {
return a.name?.localeCompare(b.name); return a.name?.localeCompare(b.name);
}); });
this.setFilterFormValues();
if (this.hasPermissionToChangeFilters) { this.setPortfolioFilterFormValues();
this.filterForm.enable({ emitEvent: false });
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
public onApplyFilters() { public onApplyFilters() {
const filterValue = this.portfolioFilterFormControl.value;
this.filtersChanged.emit([ this.filtersChanged.emit([
{ {
id: this.filterForm.get('account').value, id: filterValue?.account,
type: 'ACCOUNT' type: 'ACCOUNT'
}, },
{ {
id: this.filterForm.get('assetClass').value, id: filterValue?.assetClass,
type: 'ASSET_CLASS' type: 'ASSET_CLASS'
}, },
{ {
id: this.filterForm.get('holding').value?.dataSource, id: filterValue?.holding?.dataSource,
type: 'DATA_SOURCE' type: 'DATA_SOURCE'
}, },
{ {
id: this.filterForm.get('holding').value?.symbol, id: filterValue?.holding?.symbol,
type: 'SYMBOL' type: 'SYMBOL'
}, },
{ {
id: this.filterForm.get('tag').value, id: filterValue?.tag,
type: 'TAG' type: 'TAG'
} }
]); ]);
@ -569,12 +530,15 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
public onCloseAssistant() { public onCloseAssistant() {
this.portfolioFilterFormControl.reset();
this.setIsOpen(false); this.setIsOpen(false);
this.closed.emit(); this.closed.emit();
} }
public onResetFilters() { public onResetFilters() {
this.portfolioFilterFormControl.reset();
this.filtersChanged.emit( this.filtersChanged.emit(
this.filterTypes.map((type) => { this.filterTypes.map((type) => {
return { return {
@ -658,7 +622,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}, this.PRESELECTION_DELAY); }, this.PRESELECTION_DELAY);
} }
private searchAccounts(aSearchTerm: string): Observable<ISearchResultItem[]> { private searchAccounts(aSearchTerm: string): Observable<SearchResultItem[]> {
return this.dataService return this.dataService
.fetchAccounts({ .fetchAccounts({
filters: [ filters: [
@ -688,7 +652,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
private searchAssetProfiles( private searchAssetProfiles(
aSearchTerm: string aSearchTerm: string
): Observable<ISearchResultItem[]> { ): Observable<SearchResultItem[]> {
return this.adminService return this.adminService
.fetchAdminMarketData({ .fetchAdminMarketData({
filters: [ filters: [
@ -721,7 +685,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
); );
} }
private searchHoldings(aSearchTerm: string): Observable<ISearchResultItem[]> { private searchHoldings(aSearchTerm: string): Observable<SearchResultItem[]> {
return this.dataService return this.dataService
.fetchPortfolioHoldings({ .fetchPortfolioHoldings({
filters: [ filters: [
@ -753,7 +717,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
); );
} }
private searchQuickLinks(aSearchTerm: string): ISearchResultItem[] { private searchQuickLinks(aSearchTerm: string): SearchResultItem[] {
const searchTerm = aSearchTerm.toLowerCase(); const searchTerm = aSearchTerm.toLowerCase();
const allRoutes = Object.values(internalRoutes) const allRoutes = Object.values(internalRoutes)
@ -786,7 +750,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}); });
} }
private setFilterFormValues() { private setPortfolioFilterFormValues() {
const dataSource = this.user?.settings?.[ const dataSource = this.user?.settings?.[
'filters.dataSource' 'filters.dataSource'
] as DataSource; ] as DataSource;
@ -800,16 +764,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
); );
}); });
this.filterForm.setValue( this.portfolioFilterFormControl.setValue({
{ account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
account: this.user?.settings?.['filters.accounts']?.[0] ?? null, assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null, holding: selectedHolding ?? null,
holding: selectedHolding ?? null, tag: this.user?.settings?.['filters.tags']?.[0] ?? null
tag: this.user?.settings?.['filters.tags']?.[0] ?? null });
},
{
emitEvent: false
}
);
} }
} }

170
libs/ui/src/lib/assistant/assistant.html

@ -164,119 +164,61 @@
</div> </div>
} }
</div> </div>
<form [formGroup]="filterForm"> @if (!searchFormControl.value) {
@if (!searchFormControl.value) { <div class="date-range-selector-container p-3">
<div class="date-range-selector-container p-3"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-label i18n>Date Range</mat-label>
<mat-label i18n>Date Range</mat-label> <mat-select
<mat-select [formControl]="dateRangeFormControl"
[formControl]="dateRangeFormControl" (selectionChange)="onChangeDateRange($event.value)"
(selectionChange)="onChangeDateRange($event.value)" >
> @for (
@for (range of dateRangeOptions; track range) { dateRangeOption of dateRangeOptions;
<mat-option [value]="range.value">{{ range.label }}</mat-option> track dateRangeOption.value
} ) {
</mat-select> <mat-option [value]="dateRangeOption.value">{{
</mat-form-field> dateRangeOption.label
</div> }}</mat-option>
<div class="p-3"> }
<div class="mb-3"> </mat-select>
<mat-form-field appearance="outline" class="w-100 without-hint"> </mat-form-field>
<mat-label i18n>Account</mat-label> </div>
<mat-select formControlName="account"> <div class="p-3">
<mat-option [value]="null" /> <gf-portfolio-filter-form
@for (account of accounts; track account.id) { #portfolioFilterForm
<mat-option [value]="account.id"> [accounts]="user?.accounts"
<div class="d-flex"> [assetClasses]="assetClasses"
@if (account.platform?.url) { [formControl]="portfolioFilterFormControl"
<gf-entity-logo [holdings]="holdings"
class="mr-1" [tags]="tags"
[tooltip]="account.platform?.name" />
[url]="account.platform?.url" <div class="d-flex w-100">
/> <button
} i18n
<span>{{ account.name }}</span> mat-button
</div> type="button"
</mat-option> [disabled]="
} !portfolioFilterForm.hasFilters() || portfolioFilterForm.disabled
</mat-select> "
</mat-form-field> (click)="onResetFilters()"
</div> >
<div class="mb-3"> Reset Filters
<mat-form-field appearance="outline" class="w-100 without-hint"> </button>
<mat-label i18n>Holding</mat-label> <span class="gf-spacer"></span>
<mat-select <button
formControlName="holding" color="primary"
[compareWith]="holdingComparisonFunction" i18n
> mat-flat-button
<mat-select-trigger>{{ type="button"
filterForm.get('holding')?.value?.name [disabled]="
}}</mat-select-trigger> !portfolioFilterForm.filterForm.dirty ||
<mat-option [value]="null" /> portfolioFilterForm.disabled
@for (holding of holdings; track holding.name) { "
<mat-option [value]="holding"> (click)="onApplyFilters()"
<div class="line-height-1 text-truncate"> >
<span Apply Filters
><b>{{ holding.name }}</b></span </button>
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} ·
{{ holding.currency }}</small
>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tag</mat-label>
<mat-select formControlName="tag">
<mat-option [value]="null" />
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass">
<mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id">{{
assetClass.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="d-flex w-100">
<button
i18n
mat-button
[disabled]="
!hasFilter(filterForm.value) || !hasPermissionToChangeFilters
"
(click)="onResetFilters()"
>
Reset Filters
</button>
<span class="gf-spacer"></span>
<button
color="primary"
i18n
mat-flat-button
[disabled]="!filterForm.dirty || !hasPermissionToChangeFilters"
(click)="onApplyFilters()"
>
Apply Filters
</button>
</div>
</div> </div>
} </div>
</form> }
</div> </div>

26
libs/ui/src/lib/assistant/interfaces/interfaces.ts

@ -3,38 +3,38 @@ import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
import { SearchMode } from '../enums/search-mode'; import { SearchMode } from '../enums/search-mode';
export interface IAccountSearchResultItem export interface AccountSearchResultItem
extends Pick<AccountWithValue, 'id' | 'name'> { extends Pick<AccountWithValue, 'id' | 'name'> {
mode: SearchMode.ACCOUNT; mode: SearchMode.ACCOUNT;
routerLink: string[]; routerLink: string[];
} }
export interface IAssetSearchResultItem extends AssetProfileIdentifier { export interface AssetSearchResultItem extends AssetProfileIdentifier {
assetSubClassString: string; assetSubClassString: string;
currency: string; currency: string;
mode: SearchMode.ASSET_PROFILE | SearchMode.HOLDING; mode: SearchMode.ASSET_PROFILE | SearchMode.HOLDING;
name: string; name: string;
} }
export interface IDateRangeOption { export interface DateRangeOption {
label: string; label: string;
value: DateRange; value: DateRange;
} }
export interface IQuickLinkSearchResultItem { export interface QuickLinkSearchResultItem {
mode: SearchMode.QUICK_LINK; mode: SearchMode.QUICK_LINK;
name: string; name: string;
routerLink: string[]; routerLink: string[];
} }
export type ISearchResultItem = export type SearchResultItem =
| IAccountSearchResultItem | AccountSearchResultItem
| IAssetSearchResultItem | AssetSearchResultItem
| IQuickLinkSearchResultItem; | QuickLinkSearchResultItem;
export interface ISearchResults { export interface SearchResults {
accounts: ISearchResultItem[]; accounts: SearchResultItem[];
assetProfiles: ISearchResultItem[]; assetProfiles: SearchResultItem[];
holdings: ISearchResultItem[]; holdings: SearchResultItem[];
quickLinks: ISearchResultItem[]; quickLinks: SearchResultItem[];
} }

2
libs/ui/src/lib/portfolio-filter-form/index.ts

@ -0,0 +1,2 @@
export * from './interfaces';
export * from './portfolio-filter-form.component';

1
libs/ui/src/lib/portfolio-filter-form/interfaces/index.ts

@ -0,0 +1 @@
export * from './portfolio-filter-form-value.interface';

8
libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts

@ -0,0 +1,8 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export interface PortfolioFilterFormValue {
account: string;
assetClass: string;
holding: PortfolioPosition;
tag: string;
}

75
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html

@ -0,0 +1,75 @@
<form [formGroup]="filterForm">
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Account</mat-label>
<mat-select formControlName="account">
<mat-option [value]="null" />
@for (account of accounts; track account.id) {
<mat-option [value]="account.id">
<div class="d-flex">
@if (account.platform?.url) {
<gf-entity-logo
class="mr-1"
[tooltip]="account.platform?.name"
[url]="account.platform?.url"
/>
}
<span>{{ account.name }}</span>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Holding</mat-label>
<mat-select
formControlName="holding"
[compareWith]="holdingComparisonFunction"
>
<mat-select-trigger>{{
filterForm.get('holding')?.value?.name
}}</mat-select-trigger>
<mat-option [value]="null" />
@for (holding of holdings; track holding.name) {
<mat-option [value]="holding">
<div class="line-height-1 text-truncate">
<span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} · {{ holding.currency }}</small
>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tag</mat-label>
<mat-select formControlName="tag">
<mat-option [value]="null" />
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass">
<mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id">{{
assetClass.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</form>

3
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.scss

@ -0,0 +1,3 @@
:host {
display: block;
}

79
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts

@ -0,0 +1,79 @@
import '@angular/localize/init';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { GfPortfolioFilterFormComponent } from './portfolio-filter-form.component';
const meta: Meta<GfPortfolioFilterFormComponent> = {
title: 'Portfolio Filter Form',
component: GfPortfolioFilterFormComponent,
decorators: [
moduleMetadata({
imports: [GfPortfolioFilterFormComponent]
})
]
};
export default meta;
type Story = StoryObj<GfPortfolioFilterFormComponent>;
export const Default: Story = {
args: {
accounts: [
{
id: '733110b6-7c55-44eb-8cc5-c4c3e9d48a79',
name: 'Trading Account',
platform: {
name: 'Interactive Brokers',
url: 'https://interactivebrokers.com'
}
},
{
id: '24ba27d6-e04b-4fb4-b856-b24c2ef0422a',
name: 'Investment Account',
platform: {
name: 'Fidelity',
url: 'https://fidelity.com'
}
}
] as any,
assetClasses: [
{ id: 'COMMODITY', label: 'Commodity', type: 'ASSET_CLASS' },
{ id: 'EQUITY', label: 'Equity', type: 'ASSET_CLASS' },
{ id: 'FIXED_INCOME', label: 'Fixed Income', type: 'ASSET_CLASS' }
] as any,
holdings: [
{
currency: 'USD',
dataSource: 'YAHOO',
name: 'Apple Inc.',
symbol: 'AAPL'
},
{
currency: 'USD',
dataSource: 'YAHOO',
name: 'Microsoft Corporation',
symbol: 'MSFT'
}
] as any,
tags: [
{
id: 'EMERGENCY_FUND',
label: 'Emergency Fund',
type: 'TAG'
},
{
id: 'RETIREMENT_FUND',
label: 'Retirement Fund',
type: 'TAG'
}
] as any,
disabled: false
}
};
export const Disabled: Story = {
args: {
...Default.args,
disabled: true
}
};

177
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts

@ -0,0 +1,177 @@
import { GfSymbolPipe } from '@ghostfolio/client/pipes/symbol/symbol.pipe';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
forwardRef
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule
} from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { Subject, takeUntil } from 'rxjs';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
import { PortfolioFilterFormValue } from './interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FormsModule,
GfEntityLogoComponent,
GfSymbolPipe,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule
],
providers: [
{
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GfPortfolioFilterFormComponent)
}
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-portfolio-filter-form',
styleUrls: ['./portfolio-filter-form.component.scss'],
templateUrl: './portfolio-filter-form.component.html'
})
export class GfPortfolioFilterFormComponent
implements ControlValueAccessor, OnInit, OnChanges, OnDestroy
{
@Input() accounts: AccountWithPlatform[] = [];
@Input() assetClasses: Filter[] = [];
@Input() holdings: PortfolioPosition[] = [];
@Input() tags: Filter[] = [];
@Input() disabled = false;
public filterForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private formBuilder: FormBuilder
) {
this.filterForm = this.formBuilder.group({
account: new FormControl<string>(null),
assetClass: new FormControl<string>(null),
holding: new FormControl<PortfolioPosition>(null),
tag: new FormControl<string>(null)
});
}
public ngOnInit() {
this.filterForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((value) => {
this.onChange(value as PortfolioFilterFormValue);
this.onTouched();
});
}
public hasFilters() {
const formValue = this.filterForm.value;
return Object.values(formValue).some((value) => {
return !!value;
});
}
public holdingComparisonFunction(
option: PortfolioPosition,
value: PortfolioPosition
) {
if (value === null) {
return false;
}
return (
getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value)
);
}
public ngOnChanges() {
if (this.disabled) {
this.filterForm.disable({ emitEvent: false });
} else {
this.filterForm.enable({ emitEvent: false });
}
const tagControl = this.filterForm.get('tag');
if (this.tags.length === 0) {
tagControl?.disable({ emitEvent: false });
} else if (!this.disabled) {
tagControl?.enable({ emitEvent: false });
}
this.changeDetectorRef.markForCheck();
}
public registerOnChange(fn: (value: PortfolioFilterFormValue) => void) {
this.onChange = fn;
}
public registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
public setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.filterForm.disable({ emitEvent: false });
} else {
this.filterForm.enable({ emitEvent: false });
}
this.changeDetectorRef.markForCheck();
}
public writeValue(value: PortfolioFilterFormValue | null) {
if (value) {
this.filterForm.setValue(
{
account: value.account ?? null,
assetClass: value.assetClass ?? null,
holding: value.holding ?? null,
tag: value.tag ?? null
},
{ emitEvent: false }
);
} else {
this.filterForm.reset({}, { emitEvent: false });
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private onChange = (_value: PortfolioFilterFormValue): void => {
// ControlValueAccessor onChange callback
};
private onTouched = (): void => {
// ControlValueAccessor onTouched callback
};
}

80
package-lock.json

@ -65,6 +65,8 @@
"countries-list": "3.1.1", "countries-list": "3.1.1",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.0", "envalid": "8.1.0",
"fuse.js": "7.1.0", "fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0", "google-spreadsheet": "3.2.0",
@ -6113,9 +6115,9 @@
} }
}, },
"node_modules/@ioredis/commands": { "node_modules/@ioredis/commands": {
"version": "1.3.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
"integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@isaacs/balanced-match": { "node_modules/@isaacs/balanced-match": {
@ -9081,6 +9083,33 @@
"rxjs": "^7.1.0" "rxjs": "^7.1.0"
} }
}, },
"node_modules/@nestjs/config/node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/config/node_modules/dotenv-expand": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/core": { "node_modules/@nestjs/core": {
"version": "11.1.3", "version": "11.1.3",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz",
@ -21037,9 +21066,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -21049,9 +21078,9 @@
} }
}, },
"node_modules/dotenv-expand": { "node_modules/dotenv-expand": {
"version": "12.0.1", "version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"dotenv": "^16.4.5" "dotenv": "^16.4.5"
@ -21063,6 +21092,18 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -25067,12 +25108,12 @@
} }
}, },
"node_modules/ioredis": { "node_modules/ioredis": {
"version": "5.6.1", "version": "5.8.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
"integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ioredis/commands": "^1.1.1", "@ioredis/commands": "1.4.0",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"denque": "^2.1.0", "denque": "^2.1.0",
@ -33459,6 +33500,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/nx/node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/nx/node_modules/dotenv-expand": { "node_modules/nx/node_modules/dotenv-expand": {
"version": "11.0.7", "version": "11.0.7",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",

2
package.json

@ -111,6 +111,8 @@
"countries-list": "3.1.1", "countries-list": "3.1.1",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.0", "envalid": "8.1.0",
"fuse.js": "7.1.0", "fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0", "google-spreadsheet": "3.2.0",

5
prisma.config.ts

@ -1,7 +1,10 @@
import 'dotenv/config'; import { config } from 'dotenv';
import { expand } from 'dotenv-expand';
import { join } from 'node:path'; import { join } from 'node:path';
import { defineConfig } from 'prisma/config'; import { defineConfig } from 'prisma/config';
expand(config({ quiet: true }));
export default defineConfig({ export default defineConfig({
migrations: { migrations: {
path: join('prisma', 'migrations'), path: join('prisma', 'migrations'),

Loading…
Cancel
Save