Browse Source

Merge branch 'ghostfolio:main' into claude/ccr-c588a324-Bjc1Z

pull/6340/head
Amir Moradi 2 weeks ago
committed by GitHub
parent
commit
aadc85b42f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      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
  6. 65
      package-lock.json
  7. 2
      package.json

12
CHANGELOG.md

@ -5,6 +5,18 @@ 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
- Upgraded `cheerio` from version `1.0.0` to `1.2.0`
### 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')) {

65
package-lock.json

@ -51,7 +51,7 @@
"chartjs-chart-treemap": "3.1.0", "chartjs-chart-treemap": "3.1.0",
"chartjs-plugin-annotation": "3.1.0", "chartjs-plugin-annotation": "3.1.0",
"chartjs-plugin-datalabels": "2.2.0", "chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.0.0", "cheerio": "1.2.0",
"class-transformer": "0.5.1", "class-transformer": "0.5.1",
"class-validator": "0.14.3", "class-validator": "0.14.3",
"color": "5.0.3", "color": "5.0.3",
@ -16101,25 +16101,25 @@
} }
}, },
"node_modules/cheerio": { "node_modules/cheerio": {
"version": "1.0.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cheerio-select": "^2.1.0", "cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0", "dom-serializer": "^2.0.0",
"domhandler": "^5.0.3", "domhandler": "^5.0.3",
"domutils": "^3.1.0", "domutils": "^3.2.2",
"encoding-sniffer": "^0.2.0", "encoding-sniffer": "^0.2.1",
"htmlparser2": "^9.1.0", "htmlparser2": "^10.1.0",
"parse5": "^7.1.2", "parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2", "parse5-parser-stream": "^7.1.2",
"undici": "^6.19.5", "undici": "^7.19.0",
"whatwg-mimetype": "^4.0.0" "whatwg-mimetype": "^4.0.0"
}, },
"engines": { "engines": {
"node": ">=18.17" "node": ">=20.18.1"
}, },
"funding": { "funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1" "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
@ -16142,25 +16142,6 @@
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
"node_modules/cheerio/node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/cheerio/node_modules/parse5": { "node_modules/cheerio/node_modules/parse5": {
"version": "7.3.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@ -21656,9 +21637,9 @@
} }
}, },
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "10.0.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"funding": [ "funding": [
"https://github.com/fb55/htmlparser2?sponsor=1", "https://github.com/fb55/htmlparser2?sponsor=1",
{ {
@ -21670,14 +21651,14 @@
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
"domhandler": "^5.0.3", "domhandler": "^5.0.3",
"domutils": "^3.2.1", "domutils": "^3.2.2",
"entities": "^6.0.0" "entities": "^7.0.1"
} }
}, },
"node_modules/htmlparser2/node_modules/entities": { "node_modules/htmlparser2/node_modules/entities": {
"version": "6.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@ -33626,12 +33607,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.21.3", "version": "7.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=20.18.1"
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {

2
package.json

@ -95,7 +95,7 @@
"chartjs-chart-treemap": "3.1.0", "chartjs-chart-treemap": "3.1.0",
"chartjs-plugin-annotation": "3.1.0", "chartjs-plugin-annotation": "3.1.0",
"chartjs-plugin-datalabels": "2.2.0", "chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.0.0", "cheerio": "1.2.0",
"class-transformer": "0.5.1", "class-transformer": "0.5.1",
"class-validator": "0.14.3", "class-validator": "0.14.3",
"color": "5.0.3", "color": "5.0.3",

Loading…
Cancel
Save