Browse Source

Merge remote-tracking branch 'origin/main' into feature/enable-strict-null-checks-in-ui

pull/6264/head
Kenrick Tandrian 2 weeks ago
parent
commit
64871fcb1f
  1. 11
      CHANGELOG.md
  2. 7
      apps/api/src/app/admin/admin.controller.ts
  3. 10
      apps/api/src/app/import/import.service.ts
  4. 10
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  5. 36
      apps/api/src/services/data-provider/manual/manual.service.ts

11
CHANGELOG.md

@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Ignored nested ETFs when fetching top holdings for ETF and mutual fund assets from _Yahoo Finance_
- Improved the scraper configuration with more detailed error messages
### Fixed
- Added the missing `valueInBaseCurrency` to the response of the import activities endpoint
## 2.238.0 - 2026-02-12 ## 2.238.0 - 2026-02-12
### Changed ### Changed

7
apps/api/src/app/admin/admin.controller.ts

@ -247,14 +247,17 @@ export class AdminController {
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<{ price: number }> { ): Promise<{ price: number }> {
try { try {
const price = await this.manualService.test(data.scraperConfiguration); const price = await this.manualService.test({
symbol,
scraperConfiguration: data.scraperConfiguration
});
if (price) { if (price) {
return { price }; return { price };
} }
throw new Error( throw new Error(
`Could not parse the current market price for ${symbol} (${dataSource})` `Could not parse the market price for ${symbol} (${dataSource})`
); );
} catch (error) { } catch (error) {
Logger.error(error, 'AdminController'); Logger.error(error, 'AdminController');

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

@ -5,6 +5,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -48,6 +49,7 @@ export class ImportService {
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly orderService: OrderService, private readonly orderService: OrderService,
private readonly platformService: PlatformService, private readonly platformService: PlatformService,
@ -590,10 +592,18 @@ export class ImportService {
const value = new Big(quantity).mul(unitPrice).toNumber(); const value = new Big(quantity).mul(unitPrice).toNumber();
const valueInBaseCurrency = this.exchangeRateDataService.toCurrencyAtDate(
value,
currency ?? assetProfile.currency,
userCurrency,
date
);
activities.push({ activities.push({
...order, ...order,
error, error,
value, value,
valueInBaseCurrency: await valueInBaseCurrency,
// @ts-ignore // @ts-ignore
SymbolProfile: assetProfile SymbolProfile: assetProfile
}); });

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

@ -207,14 +207,16 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
if (['ETF', 'MUTUALFUND'].includes(assetSubClass)) { if (['ETF', 'MUTUALFUND'].includes(assetSubClass)) {
response.holdings = response.holdings =
assetProfile.topHoldings?.holdings?.map( assetProfile.topHoldings?.holdings
({ holdingName, holdingPercent }) => { ?.filter(({ holdingName }) => {
return !holdingName?.includes('ETF');
})
?.map(({ holdingName, holdingPercent }) => {
return { return {
name: this.formatName({ longName: holdingName }), name: this.formatName({ longName: holdingName }),
weight: holdingPercent weight: holdingPercent
}; };
} }) ?? [];
) ?? [];
response.sectors = ( response.sectors = (
assetProfile.topHoldings?.sectorWeightings ?? [] assetProfile.topHoldings?.sectorWeightings ?? []

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

@ -105,7 +105,10 @@ export class ManualService implements DataProviderInterface {
return {}; return {};
} }
const value = await this.scrape(symbolProfile.scraperConfiguration); const value = await this.scrape({
symbol,
scraperConfiguration: symbolProfile.scraperConfiguration
});
return { return {
[symbol]: { [symbol]: {
@ -170,7 +173,10 @@ export class ManualService implements DataProviderInterface {
symbolProfilesWithScraperConfigurationAndInstantMode.map( symbolProfilesWithScraperConfigurationAndInstantMode.map(
async ({ scraperConfiguration, symbol }) => { async ({ scraperConfiguration, symbol }) => {
try { try {
const marketPrice = await this.scrape(scraperConfiguration); const marketPrice = await this.scrape({
scraperConfiguration,
symbol
});
return { marketPrice, symbol }; return { marketPrice, symbol };
} catch (error) { } catch (error) {
Logger.error( Logger.error(
@ -267,13 +273,23 @@ export class ManualService implements DataProviderInterface {
}; };
} }
public async test(scraperConfiguration: ScraperConfiguration) { public async test({
return this.scrape(scraperConfiguration); scraperConfiguration,
symbol
}: {
scraperConfiguration: ScraperConfiguration;
symbol: string;
}) {
return this.scrape({ scraperConfiguration, symbol });
} }
private async scrape( private async scrape({
scraperConfiguration: ScraperConfiguration scraperConfiguration,
): Promise<number> { symbol
}: {
scraperConfiguration: ScraperConfiguration;
symbol: string;
}): Promise<number> {
let locale = scraperConfiguration.locale; let locale = scraperConfiguration.locale;
const response = await fetch(scraperConfiguration.url, { const response = await fetch(scraperConfiguration.url, {
@ -283,6 +299,12 @@ export class ManualService implements DataProviderInterface {
) )
}); });
if (!response.ok) {
throw new Error(
`Failed to scrape the market price for ${symbol} (${this.getName()}): ${response.status} ${response.statusText} at ${scraperConfiguration.url}`
);
}
let value: string; let value: string;
if (response.headers.get('content-type')?.includes('application/json')) { if (response.headers.get('content-type')?.includes('application/json')) {

Loading…
Cancel
Save