Browse Source

Merge branch 'main' into task/upgrade-yahoo-finance2-to-version-4.0.0

pull/7371/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
0fd060add2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 15
      CHANGELOG.md
  2. 2
      README.md
  3. 113
      apps/api/src/app/admin/admin.service.ts
  4. 21
      apps/api/src/app/auth/auth.controller.ts
  5. 2
      apps/api/src/app/info/info.module.ts
  6. 28
      apps/api/src/app/info/info.service.ts
  7. 1
      apps/api/src/app/symbol/symbol.controller.ts
  8. 4
      apps/api/src/guards/custom-throttler.guard.ts
  9. 2
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  10. 2
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  11. 2
      apps/api/src/models/rules/asset-class-cluster-risk/equity.ts
  12. 2
      apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts
  13. 2
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  14. 2
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  15. 2
      apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
  16. 2
      apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
  17. 2
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  18. 2
      apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts
  19. 2
      apps/api/src/models/rules/liquidity/buying-power.ts
  20. 2
      apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts
  21. 2
      apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts
  22. 2
      apps/api/src/models/rules/regional-market-cluster-risk/europe.ts
  23. 2
      apps/api/src/models/rules/regional-market-cluster-risk/japan.ts
  24. 2
      apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
  25. 2
      apps/api/src/services/market-data/market-data.service.ts
  26. 9
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  27. 8
      apps/client/src/app/adapter/custom-date-adapter.ts
  28. 19
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  29. 4
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  30. 124
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  31. 13
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  32. 47
      apps/client/src/app/components/home-market/home-market.component.ts
  33. 21
      apps/client/src/app/components/home-market/home-market.html
  34. 4
      apps/client/src/app/components/home-market/home-market.scss
  35. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  36. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  37. 8
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  38. 8
      apps/client/src/app/pages/public/public-page.component.ts
  39. 3
      libs/common/src/lib/config.ts
  40. 2
      libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts
  41. 20
      libs/common/src/lib/helper.spec.ts
  42. 12
      libs/common/src/lib/helper.ts
  43. 2
      libs/common/src/lib/interfaces/info-item.interface.ts
  44. 3
      libs/common/src/lib/pipes/index.ts
  45. 12
      libs/common/src/lib/pipes/symbol.pipe.ts
  46. 4
      libs/ui/src/lib/activities-filter/activities-filter.component.html
  47. 2
      libs/ui/src/lib/activities-filter/activities-filter.component.ts
  48. 4
      libs/ui/src/lib/activities-table/activities-table.component.html
  49. 2
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  50. 2
      libs/ui/src/lib/activities-table/activities-table.component.ts
  51. 3
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  52. 2
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html
  53. 12
      libs/ui/src/lib/currency-selector/currency-selector.component.ts
  54. 2
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html
  55. 2
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts
  56. 2
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html
  57. 14
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
  58. 14
      libs/ui/src/lib/top-holdings/top-holdings.component.html
  59. 2
      libs/ui/src/lib/top-holdings/top-holdings.component.ts
  60. 44
      package-lock.json
  61. 8
      package.json
  62. 2
      tsconfig.base.json

15
CHANGELOG.md

@ -9,9 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Simplified the `getHistorical()` function response in the data provider interface
- Upgraded `yahoo-finance2` from version `3.15.4` to `4.0.0`
## 3.30.0 - 2026-07-19
### Added
- Added support for converting an asset profile to the `MANUAL` data source in the asset profile details dialog of the admin control panel
### Changed
- Extended the `extractNumberFromString()` function to support negative values
- Restricted the symbol data endpoint (`GET /api/v1/symbol/:dataSource/:symbol`) to authenticated users
- Removed the deprecated `auth` endpoint of the login with _Security Token_ (`GET`)
- Simplified the `getHistorical()` function response in the data provider interface
- Upgraded `bull-board` from version `8.0.1` to `8.1.2`
## 3.29.0 - 2026-07-18
### Added

2
README.md

@ -190,8 +190,6 @@ Set the header for each request as follows:
You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ "accessToken": "<INSERT_SECURITY_TOKEN_OF_ACCOUNT>" }`)
Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
### Health Check (experimental)
#### Request

113
apps/api/src/app/admin/admin.service.ts

@ -12,6 +12,7 @@ import {
PROPERTY_IS_USER_SIGNUP_ENABLED
} from '@ghostfolio/common/config';
import {
applyAssetProfileOverrides,
getAssetProfileIdentifier,
getCurrencyFromSymbol
} from '@ghostfolio/common/helper';
@ -39,6 +40,7 @@ import {
} from '@prisma/client';
import { differenceInDays } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { randomUUID } from 'node:crypto';
@Injectable()
export class AdminService {
@ -240,16 +242,25 @@ export class AdminService {
url
}: Prisma.SymbolProfileUpdateInput
) {
const isConversionToManualDataSource =
newDataSource === DataSource.MANUAL && dataSource !== DataSource.MANUAL;
if (isConversionToManualDataSource && !newSymbol) {
newSymbol = randomUUID();
}
if (
newSymbol &&
newDataSource &&
(newSymbol !== symbol || newDataSource !== dataSource)
newSymbol &&
(newDataSource !== dataSource || newSymbol !== symbol)
) {
const newAssetProfileIdentifier: AssetProfileIdentifier = {
dataSource: newDataSource as DataSource,
symbol: newSymbol as string
};
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([
{
dataSource: DataSource[newDataSource.toString()],
symbol: newSymbol as string
}
newAssetProfileIdentifier
]);
if (assetProfile) {
@ -259,45 +270,79 @@ export class AdminService {
);
}
try {
await Promise.all([
this.symbolProfileService.updateAssetProfileIdentifier(
{
dataSource,
symbol
},
{
dataSource: DataSource[newDataSource.toString()],
symbol: newSymbol as string
}
const operations: Prisma.PrismaPromise<unknown>[] = [
this.symbolProfileService.updateAssetProfileIdentifier(
{
dataSource,
symbol
},
newAssetProfileIdentifier
),
this.marketDataService.updateAssetProfileIdentifier(
{
dataSource,
symbol
},
newAssetProfileIdentifier
)
];
if (isConversionToManualDataSource) {
const currentAssetProfile =
await this.prismaService.symbolProfile.findUnique({
include: { assetProfileOverrides: true },
where: { dataSource_symbol: { dataSource, symbol } }
});
if (!currentAssetProfile) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
const currentAssetProfileWithOverrides = applyAssetProfileOverrides(
currentAssetProfile,
currentAssetProfile.assetProfileOverrides
);
operations.push(
// The overrides are applied on every read, so delete them and
// persist the merged values in the asset profile instead
this.symbolProfileService.deleteAssetProfileOverrides(
newAssetProfileIdentifier
),
this.marketDataService.updateAssetProfileIdentifier(
this.symbolProfileService.updateSymbolProfile(
newAssetProfileIdentifier,
{
dataSource,
symbol
},
{
dataSource: DataSource[newDataSource.toString()],
symbol: newSymbol as string
assetClass: currentAssetProfileWithOverrides.assetClass,
assetSubClass: currentAssetProfileWithOverrides.assetSubClass,
countries:
currentAssetProfileWithOverrides.countries ?? undefined,
holdings: currentAssetProfileWithOverrides.holdings ?? undefined,
name: currentAssetProfileWithOverrides.name,
sectors: currentAssetProfileWithOverrides.sectors ?? undefined,
url: currentAssetProfileWithOverrides.url
}
)
]);
const [updatedAssetProfile] =
await this.symbolProfileService.getSymbolProfiles([
{
dataSource: DataSource[newDataSource.toString()],
symbol: newSymbol as string
}
]);
);
}
return updatedAssetProfile;
try {
await this.prismaService.$transaction(operations);
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
const [updatedAssetProfile] =
await this.symbolProfileService.getSymbolProfiles([
newAssetProfileIdentifier
]);
return updatedAssetProfile;
} else {
const assetProfileOverrides = {
assetClass: assetClass as AssetClass,

21
apps/api/src/app/auth/auth.controller.ts

@ -14,7 +14,6 @@ import {
Controller,
Get,
HttpException,
Param,
Post,
Req,
Res,
@ -36,26 +35,6 @@ export class AuthController {
private readonly webAuthService: WebAuthService
) {}
/**
* @deprecated
*/
@Get('anonymous/:accessToken')
@UseGuards(CustomThrottlerGuard)
public async accessTokenLoginGet(
@Param('accessToken') accessToken: string
): Promise<OAuthResponse> {
try {
const authToken =
await this.authService.validateAnonymousLogin(accessToken);
return { authToken };
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
@Post('anonymous')
@UseGuards(CustomThrottlerGuard)
public async accessTokenLogin(

2
apps/api/src/app/info/info.module.ts

@ -7,6 +7,7 @@ import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.mo
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -29,6 +30,7 @@ import { InfoService } from './info.service';
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '30 days' }
}),
MarketDataModule,
PlatformModule,
PropertyModule,
RedisCacheModule,

28
apps/api/src/app/info/info.service.ts

@ -1,14 +1,15 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
DEFAULT_CURRENCY,
ghostfolioFearAndGreedIndexSymbolStocks,
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
PROPERTY_DEMO_USER_ID,
PROPERTY_DOCKER_HUB_PULLS,
@ -23,6 +24,7 @@ import { permissions } from '@ghostfolio/common/permissions';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { MarketData } from '@prisma/client';
import { subDays } from 'date-fns';
import { isNil } from 'lodash';
@ -36,6 +38,7 @@ export class InfoService {
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly jwtService: JwtService,
private readonly marketDataService: MarketDataService,
private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService,
private readonly subscriptionService: SubscriptionService,
@ -45,6 +48,7 @@ export class InfoService {
public async get(): Promise<InfoItem> {
const info: Partial<InfoItem> = {};
let isReadOnlyMode: boolean;
let latestFearAndGreedStocksMarketDataPromise: Promise<MarketData>;
const globalPermissions: string[] = [];
@ -61,16 +65,12 @@ export class InfoService {
}
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
const fearAndGreedIndexDataSource =
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks();
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
info.fearAndGreedDataSource = encodeDataSource(
fearAndGreedIndexDataSource
);
} else {
info.fearAndGreedDataSource = fearAndGreedIndexDataSource;
}
latestFearAndGreedStocksMarketDataPromise =
this.marketDataService.getLatest({
dataSource:
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(),
symbol: ghostfolioFearAndGreedIndexSymbolStocks
});
globalPermissions.push(permissions.enableFearAndGreedIndex);
}
@ -102,12 +102,14 @@ export class InfoService {
benchmarks,
demoAuthToken,
isUserSignupEnabled,
latestFearAndGreedStocksMarketData,
statistics,
subscriptionOffer
] = await Promise.all([
this.benchmarkService.getBenchmarkAssetProfiles(),
this.getDemoAuthToken(),
this.propertyService.isUserSignupEnabled(),
latestFearAndGreedStocksMarketDataPromise,
this.getStatistics(),
this.subscriptionService.getSubscriptionOffer({ key: 'default' })
]);
@ -125,7 +127,9 @@ export class InfoService {
statistics,
subscriptionOffer,
baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies()
currencies: this.exchangeRateDataService.getCurrencies(),
fearAndGreedStocksMarketPrice:
latestFearAndGreedStocksMarketData?.marketPrice
};
}

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

@ -65,6 +65,7 @@ export class SymbolController {
* Must be after /lookup
*/
@Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getSymbolData(

4
apps/api/src/guards/custom-throttler.guard.ts

@ -5,7 +5,9 @@ import { ThrottlerException, ThrottlerGuard } from '@nestjs/throttler';
export class CustomThrottlerGuard extends ThrottlerGuard {
private readonly logger = new Logger(CustomThrottlerGuard.name);
public async canActivate(context: ExecutionContext): Promise<boolean> {
public override async canActivate(
context: ExecutionContext
): Promise<boolean> {
try {
return await super.canActivate(context);
} catch (error) {

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

@ -13,7 +13,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
private accounts: PortfolioDetails['accounts'];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
accounts: PortfolioDetails['accounts']

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

@ -11,7 +11,7 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
private accounts: PortfolioDetails['accounts'];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
accounts: PortfolioDetails['accounts']

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

@ -11,7 +11,7 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
private holdings: PortfolioPosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
holdings: PortfolioPosition[]

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

@ -11,7 +11,7 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
private holdings: PortfolioPosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
holdings: PortfolioPosition[]

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

@ -11,7 +11,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
private holdings: PortfolioPosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
holdings: PortfolioPosition[],
languageCode: string

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

@ -11,7 +11,7 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
private holdings: PortfolioPosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
holdings: PortfolioPosition[],
languageCode: string

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

@ -8,7 +8,7 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
private developedMarketsValueInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
currentValueInBaseCurrency: number,
developedMarketsValueInBaseCurrency: number,

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

@ -8,7 +8,7 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
private emergingMarketsValueInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
currentValueInBaseCurrency: number,
emergingMarketsValueInBaseCurrency: number,

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

@ -7,7 +7,7 @@ export class EmergencyFundSetup extends Rule<Settings> {
private emergencyFund: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
emergencyFund: number

2
apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts

@ -8,7 +8,7 @@ export class FeeRatioTotalInvestmentVolume extends Rule<Settings> {
private totalInvestmentVolumeInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
totalInvestmentVolumeInBaseCurrency: number,

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

@ -7,7 +7,7 @@ export class BuyingPower extends Rule<Settings> {
private buyingPower: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
buyingPower: number,
languageCode: string

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

@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
private currentValueInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
currentValueInBaseCurrency: number,

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

@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
private emergingMarketsValueInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
currentValueInBaseCurrency: number,

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

@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
private europeValueInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
currentValueInBaseCurrency: number,

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

@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
private japanValueInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
currentValueInBaseCurrency: number,

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

@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
private northAmericaValueInBaseCurrency: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
currentValueInBaseCurrency: number,

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

@ -204,7 +204,7 @@ export class MarketDataService {
);
}
public async updateAssetProfileIdentifier(
public updateAssetProfileIdentifier(
oldAssetProfileIdentifier: AssetProfileIdentifier,
newAssetProfileIdentifier: AssetProfileIdentifier
) {

9
apps/api/src/services/symbol-profile/symbol-profile.service.ts

@ -35,6 +35,15 @@ export class SymbolProfileService {
});
}
public deleteAssetProfileOverrides({
dataSource,
symbol
}: AssetProfileIdentifier) {
return this.prismaService.assetProfileOverrides.deleteMany({
where: { symbolProfile: { dataSource, symbol } }
});
}
public async deleteById(id: string) {
return this.prismaService.symbolProfile.delete({
where: { id }

8
apps/client/src/app/adapter/custom-date-adapter.ts

@ -6,7 +6,7 @@ import { addYears, format, getYear, parse } from 'date-fns';
export class CustomDateAdapter extends NativeDateAdapter {
public constructor(
@Inject(MAT_DATE_LOCALE) public locale: string,
@Inject(MAT_DATE_LOCALE) public override locale: string,
@Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string
) {
super(matDateLocale);
@ -15,21 +15,21 @@ export class CustomDateAdapter extends NativeDateAdapter {
/**
* Formats a date as a string
*/
public format(aDate: Date): string {
public override format(aDate: Date): string {
return format(aDate, getDateFormatString(this.locale));
}
/**
* Sets the first day of the week to Monday
*/
public getFirstDayOfWeek(): number {
public override getFirstDayOfWeek(): number {
return 1;
}
/**
* Parses a date from a provided value
*/
public parse(aValue: string): Date {
public override parse(aValue: string): Date {
let date = parse(aValue, getDateFormatString(this.locale), new Date());
if (getYear(date) < 1900) {

19
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -13,7 +13,6 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
import { GfFabComponent } from '@ghostfolio/ui/fab';
import { translate } from '@ghostfolio/ui/i18n';
@ -83,7 +82,6 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in
GfActivitiesFilterComponent,
GfFabComponent,
GfPremiumIndicatorComponent,
GfSymbolPipe,
GfValueComponent,
IonIcon,
MatButtonModule,
@ -410,7 +408,8 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
const dialogRef = this.dialog.open<
GfAssetProfileDialogComponent,
AssetProfileDialogParams
AssetProfileDialogParams,
AssetProfileIdentifier
>(GfAssetProfileDialogComponent, {
autoFocus: false,
data: {
@ -428,15 +427,13 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(
(newAssetProfileIdentifier: AssetProfileIdentifier | undefined) => {
if (newAssetProfileIdentifier) {
this.onOpenAssetProfileDialog(newAssetProfileIdentifier);
} else {
this.router.navigate(['.'], { relativeTo: this.route });
}
.subscribe((newAssetProfileIdentifier) => {
if (newAssetProfileIdentifier) {
this.onOpenAssetProfileDialog(newAssetProfileIdentifier);
} else {
this.router.navigate(['.'], { relativeTo: this.route });
}
);
});
});
}

4
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -95,9 +95,7 @@
<div class="text-truncate">{{ element.name }}</div>
@if (!isUUID(element.symbol)) {
<div>
<small class="text-muted">{{
element.symbol | gfSymbol
}}</small>
<small class="text-muted">{{ element.symbol }}</small>
</div>
}
</td>

124
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -5,6 +5,7 @@ import {
PROPERTY_IS_DATA_GATHERING_ENABLED
} from '@ghostfolio/common/config';
import { UpdateAssetProfileDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import {
canDeleteAssetProfile,
DATE_FORMAT,
@ -75,6 +76,7 @@ import {
AssetClass,
AssetSubClass,
DataGatheringFrequency,
DataSource,
MarketData,
Prisma,
SymbolProfile
@ -215,6 +217,8 @@ export class GfAssetProfileDialogComponent implements OnInit {
}
];
protected readonly DataSource = DataSource;
protected readonly dateRangeOptions = [
{
label: $localize`Current week` + ' (' + $localize`WTD` + ')',
@ -277,7 +281,10 @@ export class GfAssetProfileDialogComponent implements OnInit {
@Inject(MAT_DIALOG_DATA) protected data: AssetProfileDialogParams,
private dataService: DataService,
private destroyRef: DestroyRef,
private dialogRef: MatDialogRef<GfAssetProfileDialogComponent>,
private dialogRef: MatDialogRef<
GfAssetProfileDialogComponent,
AssetProfileIdentifier
>,
private formBuilder: FormBuilder,
private notificationService: NotificationService,
private snackBar: MatSnackBar,
@ -467,6 +474,19 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.dialogRef.close();
}
protected onConvertToManualDataSource() {
this.patchAssetProfileIdentifier({
getErrorMessage: () => {
return (
'😞 ' +
$localize`An error occurred while converting the data source to ${DataSource.MANUAL}.`
);
},
title: $localize`Do you really want to convert the data source to ${DataSource.MANUAL}?`,
updateAssetProfileDto: { dataSource: DataSource.MANUAL }
});
}
protected onDeleteProfileData({
dataSource,
symbol
@ -666,13 +686,16 @@ export class GfAssetProfileDialogComponent implements OnInit {
}
protected async onSubmitAssetProfileIdentifierForm() {
const newAssetProfileIdentifier =
this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value;
if (!newAssetProfileIdentifier?.dataSource) {
return;
}
const assetProfileIdentifier: UpdateAssetProfileDto = {
dataSource:
this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value
?.dataSource ?? undefined,
symbol:
this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value
?.symbol ?? undefined
dataSource: newAssetProfileIdentifier.dataSource,
symbol: newAssetProfileIdentifier.symbol
};
try {
@ -687,46 +710,19 @@ export class GfAssetProfileDialogComponent implements OnInit {
return;
}
this.adminService
.patchAssetProfile(
{
dataSource: this.data.dataSource,
symbol: this.data.symbol
},
assetProfileIdentifier
)
.pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === StatusCodes.CONFLICT) {
this.snackBar.open(
$localize`${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}) is already in use.`,
undefined,
{
duration: ms('3 seconds')
}
);
} else {
this.snackBar.open(
$localize`An error occurred while updating to ${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}).`,
undefined,
{
duration: ms('3 seconds')
}
);
}
this.patchAssetProfileIdentifier({
getErrorMessage: (error) => {
if (error.status === StatusCodes.CONFLICT) {
// TODO: Ask if the user wants to merge the two asset profiles
return EMPTY;
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
const newAssetProfileIdentifier = {
dataSource: assetProfileIdentifier.dataSource,
symbol: assetProfileIdentifier.symbol
};
return $localize`${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}) is already in use.`;
}
this.dialogRef.close(newAssetProfileIdentifier);
});
return $localize`An error occurred while updating to ${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}).`;
},
title: $localize`Do you really want to convert this asset profile to ${newAssetProfileIdentifier.symbol} (${newAssetProfileIdentifier.dataSource})?`,
updateAssetProfileDto: assetProfileIdentifier
});
}
protected onTestMarketData() {
@ -824,4 +820,42 @@ export class GfAssetProfileDialogComponent implements OnInit {
return null;
}
private patchAssetProfileIdentifier({
getErrorMessage,
title,
updateAssetProfileDto
}: {
getErrorMessage: (error: HttpErrorResponse) => string;
title: string;
updateAssetProfileDto: UpdateAssetProfileDto;
}) {
this.notificationService.confirm({
title,
confirmFn: () => {
this.adminService
.patchAssetProfile(
{
dataSource: this.data.dataSource,
symbol: this.data.symbol
},
updateAssetProfileDto
)
.pipe(
catchError((error: HttpErrorResponse) => {
this.snackBar.open(getErrorMessage(error), undefined, {
duration: ms('3 seconds')
});
return EMPTY;
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(({ dataSource, symbol }) => {
this.dialogRef.close({ dataSource, symbol });
});
},
confirmType: ConfirmationDialogType.Primary
});
}
}

13
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -162,6 +162,19 @@
<ng-container i18n>Cancel</ng-container>
</button>
</form>
@if (data.dataSource !== DataSource.MANUAL) {
<p class="my-1 px-1 text-muted" i18n>or</p>
<button
color="accent"
mat-flat-button
type="button"
(click)="onConvertToManualDataSource()"
>
<ng-container i18n
>Convert to {{ DataSource.MANUAL }}</ng-container
>
</button>
}
</div>
} @else {
<div class="col-6 mb-3">

47
apps/client/src/app/components/home-market/home-market.component.ts

@ -1,16 +1,8 @@
import { GfFearAndGreedIndexComponent } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.component';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ghostfolioFearAndGreedIndexSymbolStocks } from '@ghostfolio/common/config';
import { resetHours } from '@ghostfolio/common/helper';
import {
Benchmark,
HistoricalDataItem,
InfoItem,
User
} from '@ghostfolio/common/interfaces';
import { Benchmark, InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { DataService } from '@ghostfolio/ui/services';
import {
@ -29,11 +21,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
GfBenchmarkComponent,
GfFearAndGreedIndexComponent,
GfLineChartComponent
],
imports: [GfBenchmarkComponent, GfFearAndGreedIndexComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-market',
styleUrls: ['./home-market.scss'],
@ -41,15 +29,13 @@ import { DeviceDetectorService } from 'ngx-device-detector';
})
export class GfHomeMarketComponent implements OnInit {
protected readonly benchmarks = signal<Benchmark[]>([]);
protected readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
protected readonly fearAndGreedIndex = signal<number | undefined>(undefined);
protected readonly fearLabel = $localize`Fear`;
protected readonly greedLabel = $localize`Greed`;
protected fearAndGreedIndex: number | undefined;
protected hasPermissionToAccessFearAndGreedIndex: boolean;
protected readonly historicalDataItems = signal<HistoricalDataItem[]>([]);
protected readonly numberOfDays = 365;
protected user: User;
private readonly info: InfoItem;
@ -80,27 +66,8 @@ export class GfHomeMarketComponent implements OnInit {
permissions.enableFearAndGreedIndex
);
if (
this.hasPermissionToAccessFearAndGreedIndex &&
this.info.fearAndGreedDataSource
) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbolStocks
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex.set(marketPrice);
this.historicalDataItems.set([
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
]);
});
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.fearAndGreedIndex = this.info.fearAndGreedStocksMarketPrice;
}
this.dataService

21
apps/client/src/app/components/home-market/home-market.html

@ -1,28 +1,11 @@
<div class="container">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
@if (hasPermissionToAccessFearAndGreedIndex) {
<div class="mb-5 row">
<div class="mb-4 row">
<div class="col-xs-12 col-md-10 offset-md-1">
<div class="mb-2 text-center text-muted">
<small i18n>Last {{ numberOfDays }} Days</small>
</div>
<gf-line-chart
class="mb-3"
label="Fear & Greed Index"
[colorScheme]="user?.settings?.colorScheme"
[historicalDataItems]="historicalDataItems()"
[isAnimated]="true"
[locale]="user?.settings?.locale || undefined"
[showXAxis]="true"
[showYAxis]="true"
[yMax]="100"
[yMaxLabel]="greedLabel"
[yMin]="0"
[yMinLabel]="fearLabel"
/>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex()"
[fearAndGreedIndex]="fearAndGreedIndex"
/>
</div>
</div>

4
apps/client/src/app/components/home-market/home-market.scss

@ -1,7 +1,3 @@
:host {
display: block;
gf-line-chart {
aspect-ratio: 16 / 9;
}
}

2
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -7,7 +7,6 @@ import {
CreateTagDto
} from '@ghostfolio/common/dtos';
import { Activity, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer';
import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header';
@ -66,7 +65,6 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfFileDropDirective,
GfSymbolPipe,
IonIcon,
MatButtonModule,
MatDialogModule,

2
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -51,7 +51,7 @@
>
<br />
<small class="text-muted"
>{{ holding.assetProfile.symbol | gfSymbol }} ·
>{{ holding.assetProfile.symbol }} ·
{{ holding.assetProfile.currency }}</small
>
</mat-option>

8
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -6,7 +6,7 @@ import {
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper';
import { getCountryName } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
HoldingWithParents,
@ -495,10 +495,10 @@ export class GfAllocationsPageComponent implements OnInit {
this.totalValueInEtf += this.holdings[symbol].value;
}
this.symbols[prettifySymbol(symbol)] = {
this.symbols[symbol] = {
symbol,
dataSource: position.assetProfile.dataSource,
name: position.assetProfile.name ?? '',
symbol: prettifySymbol(symbol),
value:
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
@ -565,11 +565,11 @@ export class GfAllocationsPageComponent implements OnInit {
return currentParentHolding &&
isNumber(currentParentHolding.valueInBaseCurrency)
? {
symbol,
allocationInPercentage:
currentParentHolding.valueInBaseCurrency / value,
name: holding.assetProfile.name ?? '',
position: holding,
symbol: prettifySymbol(symbol),
valueInBaseCurrency:
currentParentHolding.valueInBaseCurrency
}

8
apps/client/src/app/pages/public/public-page.component.ts

@ -1,5 +1,5 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper';
import { getCountryName } from '@ghostfolio/common/helper';
import {
InfoItem,
PortfolioPosition,
@ -250,9 +250,9 @@ export class GfPublicPageComponent implements OnInit {
}
}
this.symbols[prettifySymbol(symbol)] = {
name: position.assetProfile.name ?? prettifySymbol(symbol),
symbol: prettifySymbol(symbol),
this.symbols[symbol] = {
symbol,
name: position.assetProfile.name ?? symbol,
value: isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: (position.valueInPercentage ?? 0)

3
libs/common/src/lib/config.ts

@ -6,9 +6,6 @@ import { ColorScheme, DateRange } from './types';
export const ghostfolioPrefix = 'GF';
/* @deprecated */
export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`;
export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies =
DataSource.MANUAL;
export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`;

2
libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts

@ -9,7 +9,7 @@ export class CreateAssetProfileWithMarketDataDto extends CreateAssetProfileDto {
@IsIn([DataSource.MANUAL], {
message: `dataSource must be '${DataSource.MANUAL}'`
})
dataSource: DataSource;
override dataSource: DataSource;
@IsArray()
@IsOptional()

20
libs/common/src/lib/helper.spec.ts

@ -11,6 +11,10 @@ describe('Helper', () => {
expect(extractNumberFromString({ value: '999.99' })).toEqual(999.99);
});
it('Get negative decimal number', () => {
expect(extractNumberFromString({ value: '-999.99' })).toEqual(-999.99);
});
it('Get decimal number (with spaces)', () => {
expect(extractNumberFromString({ value: ' 999.99 ' })).toEqual(999.99);
});
@ -19,6 +23,12 @@ describe('Helper', () => {
expect(extractNumberFromString({ value: '999.99 CHF' })).toEqual(999.99);
});
it('Get negative decimal number (with currency)', () => {
expect(extractNumberFromString({ value: '-999.99 CHF' })).toEqual(
-999.99
);
});
it('Get decimal number (comma notation)', () => {
expect(
extractNumberFromString({ locale: 'de-DE', value: '999,99' })
@ -37,12 +47,22 @@ describe('Helper', () => {
).toEqual(99999.99);
});
it('Get negative decimal number with group (comma notation)', () => {
expect(
extractNumberFromString({ locale: 'de-DE', value: '-99.999,99' })
).toEqual(-99999.99);
});
it('Get decimal number (comma notation) for locale where currency is not grouped by default', () => {
expect(
extractNumberFromString({ locale: 'es-ES', value: '999,99' })
).toEqual(999.99);
});
it('Get decimal number (with hyphenated text)', () => {
expect(extractNumberFromString({ value: 'BRK-B 425.30' })).toEqual(425.3);
});
it('Not a number', () => {
expect(extractNumberFromString({ value: 'X' })).toEqual(NaN);
});

12
libs/common/src/lib/helper.ts

@ -40,7 +40,6 @@ import {
DERIVED_CURRENCIES,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks,
ghostfolioScraperApiSymbolPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from './config';
import {
@ -210,12 +209,17 @@ export function extractNumberFromString({
value: string;
}): number | undefined {
try {
// Only a leading minus sign indicates a negative value. Detect it before
// stripping so that hyphens within the text cannot flip the sign.
const isNegative = value.trim().startsWith('-');
// Remove non-numeric characters (excluding international formatting characters)
const numericValue = value.replace(/[^\d.,'’\s]/g, '');
const parser = new NumberParser(locale);
const parsedValue = parser.parse(numericValue);
return parser.parse(numericValue);
return isNegative ? -parsedValue : parsedValue;
} catch {
return undefined;
}
@ -527,10 +531,6 @@ export function parseSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
};
}
export function prettifySymbol(aSymbol: string): string {
return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, '');
}
export function resetHours(aDate: Date) {
const year = getYear(aDate);
const month = getMonth(aDate);

2
libs/common/src/lib/interfaces/info-item.interface.ts

@ -9,7 +9,7 @@ export interface InfoItem {
countriesOfSubscribers?: string[];
currencies: string[];
demoAuthToken: string;
fearAndGreedDataSource?: string;
fearAndGreedStocksMarketPrice?: number;
globalPermissions: string[];
isDataGatheringEnabled?: string;
isReadOnlyMode?: boolean;

3
libs/common/src/lib/pipes/index.ts

@ -1,3 +0,0 @@
import { GfSymbolPipe } from './symbol.pipe';
export { GfSymbolPipe };

12
libs/common/src/lib/pipes/symbol.pipe.ts

@ -1,12 +0,0 @@
import { prettifySymbol } from '@ghostfolio/common/helper';
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'gfSymbol'
})
export class GfSymbolPipe implements PipeTransform {
public transform(aSymbol: string) {
return prettifySymbol(aSymbol);
}
}

4
libs/ui/src/lib/activities-filter/activities-filter.component.html

@ -8,7 +8,7 @@
[removable]="true"
(removed)="onRemoveFilter(filter)"
>
{{ filter.label ?? '' | gfSymbol }}
{{ filter.label }}
<button matChipRemove>
<ion-icon name="close-outline" />
</button>
@ -33,7 +33,7 @@
<mat-optgroup [label]="filterGroup.name">
@for (filter of filterGroup.filters; track filter) {
<mat-option [value]="filter.id">
{{ filter.label ?? '' | gfSymbol }}
{{ filter.label }}
</mat-option>
}
</mat-optgroup>

2
libs/ui/src/lib/activities-filter/activities-filter.component.ts

@ -1,5 +1,4 @@
import { Filter, FilterGroup } from '@ghostfolio/common/interfaces';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CommonModule } from '@angular/common';
@ -39,7 +38,6 @@ import { translate } from '../i18n';
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfSymbolPipe,
IonIcon,
MatAutocompleteModule,
MatButtonModule,

4
libs/ui/src/lib/activities-table/activities-table.component.html

@ -172,9 +172,7 @@
!isUUID(element.assetProfile?.symbol)
) {
<div>
<small class="text-muted">{{
element.assetProfile?.symbol | gfSymbol
}}</small>
<small class="text-muted">{{ element.assetProfile?.symbol }}</small>
</div>
}
</td>

2
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -1,5 +1,4 @@
import { Activity } from '@ghostfolio/common/interfaces';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
@ -375,7 +374,6 @@ export default {
GfActivityTypeComponent,
GfEntityLogoComponent,
GfNoTransactionsInfoComponent,
GfSymbolPipe,
GfValueComponent,
IonIcon,
MatButtonModule,

2
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -8,7 +8,6 @@ import {
Activity,
AssetProfileIdentifier
} from '@ghostfolio/common/interfaces';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { translate } from '@ghostfolio/ui/i18n';
import { NotificationService } from '@ghostfolio/ui/notifications';
@ -83,7 +82,6 @@ import { GfValueComponent } from '../value/value.component';
GfActivityTypeComponent,
GfEntityLogoComponent,
GfNoTransactionsInfoComponent,
GfSymbolPipe,
GfValueComponent,
IonIcon,
MatButtonModule,

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

@ -1,4 +1,3 @@
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { FocusableOption } from '@angular/cdk/a11y';
@ -24,7 +23,7 @@ import {
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [GfSymbolPipe, RouterModule],
imports: [RouterModule],
selector: 'gf-assistant-list-item',
styleUrls: ['./assistant-list-item.scss'],
templateUrl: './assistant-list-item.html'

2
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html

@ -8,7 +8,7 @@
@if (item && isAsset(item)) {
<br />
<small class="text-muted"
>{{ item?.symbol ?? '' | gfSymbol }}
>{{ item?.symbol }}
@if (item.currency) {
· {{ item.currency }}
}

12
libs/ui/src/lib/currency-selector/currency-selector.component.ts

@ -75,22 +75,22 @@ export class GfCurrencySelectorComponent
private readonly input = viewChild.required(MatInput);
public constructor(
public readonly _elementRef: ElementRef,
public readonly _focusMonitor: FocusMonitor,
public override readonly _elementRef: ElementRef,
public override readonly _focusMonitor: FocusMonitor,
public readonly changeDetectorRef: ChangeDetectorRef,
private readonly formGroupDirective: FormGroupDirective,
public readonly ngControl: NgControl
public override readonly ngControl: NgControl
) {
super(_elementRef, _focusMonitor, ngControl);
this.controlType = 'currency-selector';
}
public get empty() {
public override get empty() {
return this.input().empty;
}
public set value(value: string | null) {
public override set value(value: string | null) {
this.control.setValue(value);
super.value = value;
}
@ -138,7 +138,7 @@ export class GfCurrencySelectorComponent
});
}
public ngDoCheck() {
public override ngDoCheck() {
if (this.ngControl) {
this.validateRequired();
this.errorState = !!(this.ngControl.invalid && this.ngControl.touched);

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

@ -40,7 +40,7 @@
>
<br />
<small class="text-muted"
>{{ holding.assetProfile.symbol | gfSymbol }} ·
>{{ holding.assetProfile.symbol }} ·
{{ holding.assetProfile.currency }}</small
>
</div>

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

@ -1,6 +1,5 @@
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import {
@ -37,7 +36,6 @@ import { PortfolioFilterFormValue } from './interfaces';
imports: [
FormsModule,
GfEntityLogoComponent,
GfSymbolPipe,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule

2
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html

@ -25,7 +25,7 @@
}
</span>
<small class="text-muted"
>{{ lookupItem.symbol | gfSymbol }}
>{{ lookupItem.symbol }}
@if (lookupItem.currency) {
· {{ lookupItem.currency }}
}

14
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -1,5 +1,4 @@
import { LookupItem } from '@ghostfolio/common/interfaces';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { DataService } from '@ghostfolio/ui/services';
import { FocusMonitor } from '@angular/cdk/a11y';
@ -58,7 +57,6 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
imports: [
FormsModule,
GfPremiumIndicatorComponent,
GfSymbolPipe,
MatAutocompleteModule,
MatFormFieldModule,
MatInputModule,
@ -95,22 +93,22 @@ export class GfSymbolAutocompleteComponent
private readonly input = viewChild.required(MatInput);
public constructor(
public readonly _elementRef: ElementRef,
public readonly _focusMonitor: FocusMonitor,
public override readonly _elementRef: ElementRef<HTMLElement>,
public override readonly _focusMonitor: FocusMonitor,
public readonly changeDetectorRef: ChangeDetectorRef,
public readonly dataService: DataService,
public readonly ngControl: NgControl
public override readonly ngControl: NgControl
) {
super(_elementRef, _focusMonitor, ngControl);
this.controlType = 'symbol-autocomplete';
}
public get empty() {
public override get empty() {
return this.input().empty;
}
public set value(value: LookupItem) {
public override set value(value: LookupItem) {
this.control.setValue(value);
super.value = value;
}
@ -188,7 +186,7 @@ export class GfSymbolAutocompleteComponent
});
}
public ngDoCheck() {
public override ngDoCheck() {
if (this.ngControl) {
this.validateRequired();
this.errorState = !!(this.ngControl.invalid && this.ngControl.touched);

14
libs/ui/src/lib/top-holdings/top-holdings.component.html

@ -72,15 +72,15 @@
</colgroup>
<ng-container matColumnDef="name">
<td *matCellDef="let parentHolding" class="px-2" mat-cell>
<div
class="align-items-center d-flex line-height-1 text-nowrap"
>
<div>{{ parentHolding?.name }}</div>
</div>
<td
*matCellDef="let parentHolding"
class="line-height-normal px-2"
mat-cell
>
<div class="text-truncate">{{ parentHolding?.name }}</div>
<div>
<small class="text-muted">{{
parentHolding?.symbol | gfSymbol
parentHolding?.symbol
}}</small>
</div>
</td>

2
libs/ui/src/lib/top-holdings/top-holdings.component.ts

@ -3,7 +3,6 @@ import {
AssetProfileIdentifier,
HoldingWithParents
} from '@ghostfolio/common/interfaces';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import {
animate,
@ -43,7 +42,6 @@ import { GfValueComponent } from '../value/value.component';
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
GfSymbolPipe,
GfValueComponent,
MatButtonModule,
MatPaginatorModule,

44
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "3.29.0",
"version": "3.30.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "3.29.0",
"version": "3.30.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -21,9 +21,9 @@
"@angular/platform-browser-dynamic": "21.2.7",
"@angular/router": "21.2.7",
"@angular/service-worker": "21.2.7",
"@bull-board/api": "8.0.1",
"@bull-board/express": "8.0.1",
"@bull-board/nestjs": "8.0.1",
"@bull-board/api": "8.1.2",
"@bull-board/express": "8.1.2",
"@bull-board/nestjs": "8.1.2",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7",
@ -3548,25 +3548,25 @@
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@bull-board/api": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@bull-board/api/-/api-8.0.1.tgz",
"integrity": "sha512-7FELJHRQPtjH9+r/DUArr4pDVU8r1yeDS9azQUzFtIbsUT5xGjyg5R1RB0I/RfwFfK5bqho/4cpzxJ/UnQjSKA==",
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@bull-board/api/-/api-8.1.2.tgz",
"integrity": "sha512-6NGYCIRhHJmmoCwAFA1z4lPpR/D/BdOjyERd2MMeG4Samu05FSjzKKdN55S3ga30EcXeENfYzmNn6IDmVz5OTg==",
"license": "MIT",
"dependencies": {
"redis-info": "^3.1.0"
},
"peerDependencies": {
"@bull-board/ui": "8.0.1"
"@bull-board/ui": "8.1.2"
}
},
"node_modules/@bull-board/express": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@bull-board/express/-/express-8.0.1.tgz",
"integrity": "sha512-VOEhLNlaaVk3mBBoREXO/Dopzr9rKK5TpOGyaCbcnpzLKlrlwhj6BAeWWreB7/3Wu+pNnbKmrAhK8OOnYdzxLg==",
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@bull-board/express/-/express-8.1.2.tgz",
"integrity": "sha512-IldpQLXlezJRzk1BVNJjH+Oi3NBRcBopq9bI2ndLs0R+ySK1ux+TtGDUW+eX2fl0wEHwSpuUgPBAJID695zohA==",
"license": "MIT",
"dependencies": {
"@bull-board/api": "8.0.1",
"@bull-board/ui": "8.0.1",
"@bull-board/api": "8.1.2",
"@bull-board/ui": "8.1.2",
"ejs": "^6.0.1",
"express": "^5.2.1"
}
@ -3584,12 +3584,12 @@
}
},
"node_modules/@bull-board/nestjs": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-8.0.1.tgz",
"integrity": "sha512-yBB+S7ibdrcO6y01VQTzd51Qxx19bPnte5TSdA59BWSeH6oVagB+EkCqHXTbfdGrvLrwZEF3tnSCE6339nB7MQ==",
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-8.1.2.tgz",
"integrity": "sha512-7sgESSitxULFSxuWIYx6zUyCgQIVFtw+s5ezGFvdh3/s2dwP2NoOQ76xJn+16cXh0EAFM+rN1f05BAtc0aLlUQ==",
"license": "MIT",
"peerDependencies": {
"@bull-board/api": "^8.0.1",
"@bull-board/api": "^8.1.2",
"@nestjs/bull-shared": "^10.0.0 || ^11.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
@ -3598,12 +3598,12 @@
}
},
"node_modules/@bull-board/ui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-8.0.1.tgz",
"integrity": "sha512-gKEGSD8dlUoWFGJJ4I4Q5bDtfB7yJwGfpgA2DVn1G1TzlUSN5n4fzzcGpLf5fS+QhR7tB/niydUiFRQ2zrjVrQ==",
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-8.1.2.tgz",
"integrity": "sha512-gC5u9XUSiRile4/VC+WHXtaPvfZV4Mrrz8MSvPe32Hjl2MXStfSuEOb+9Tw/WMpNSZ442tGpFZLz0RDh1Yo3jg==",
"license": "MIT",
"dependencies": {
"@bull-board/api": "8.0.1"
"@bull-board/api": "8.1.2"
}
},
"node_modules/@cacheable/utils": {

8
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "3.29.0",
"version": "3.30.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -65,9 +65,9 @@
"@angular/platform-browser-dynamic": "21.2.7",
"@angular/router": "21.2.7",
"@angular/service-worker": "21.2.7",
"@bull-board/api": "8.0.1",
"@bull-board/express": "8.0.1",
"@bull-board/nestjs": "8.0.1",
"@bull-board/api": "8.1.2",
"@bull-board/express": "8.1.2",
"@bull-board/nestjs": "8.1.2",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7",

2
tsconfig.base.json

@ -30,7 +30,7 @@
"noImplicitReturns": false,
"noImplicitAny": false,
"noImplicitThis": true,
"noImplicitOverride": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"noUnusedLocals": true,
"noUnusedParameters": true,

Loading…
Cancel
Save