feat(fundamentals): multi-ticker DB caching, parallel html scraper, and frontend mappings

This commit is contained in:
2026-08-16 14:05:57 +02:00
parent b0f8d4b78b
commit 2ba54e8057
17 changed files with 1791 additions and 563 deletions
@@ -32,6 +32,7 @@ public interface IYahooFinanceScraper
Task<YahooQuoteSummaryModulesDto?> GetQuoteSummaryModulesAsync(
string symbolOrIsin,
bool forceHtmlScrape = false,
bool includeProfile = true,
CancellationToken cancellationToken = default);
}
@@ -98,7 +99,11 @@ public class YahooFinanceScraper : IYahooFinanceScraper
}
}
}
catch { }
catch (Exception ex)
{
await _finlyticLogger.LogWarningAsync(CoreSettingKeys.YahooClientChannel, ex,
$"[{_serviceName}] Crypto search failed for {cryptoSubtitle}");
}
await _finlyticLogger.LogInfoAsync(CoreSettingKeys.FundamentalsChannel,
$"[{_serviceName}] Resolved Crypto ISIN {cleanIsin} to {cryptoEur} using Subtitle {cryptoSubtitle}");
@@ -131,7 +136,7 @@ public class YahooFinanceScraper : IYahooFinanceScraper
}
}
// 2. Falls Ticker gefunden, aber mit Unternehmensname noch mehr Exchangeticker auffindbar sind
// 2. Falls Ticker gefunden, mit Unternehmensname noch mehr internationale Exchangeticker suchen (z.B. APC.DE)
if (validQuotes.Count > 0)
{
var companyName = validQuotes[0].LongName ?? validQuotes[0].ShortName;
@@ -155,16 +160,24 @@ public class YahooFinanceScraper : IYahooFinanceScraper
$"[{_serviceName}] Fehler beim Auflösen des Tickers für ISIN '{cleanIsin}'");
}
return symbols
var result = symbols
.OrderBy(s => s.priority)
.Select(s => new TickerInfoDto { Ticker = s.symbol, Exchange = s.exchange })
.Select(s => new TickerInfoDto
{
Ticker = s.symbol,
Exchange = !string.IsNullOrWhiteSpace(s.exchange) ? s.exchange : "Unknown"
})
.DistinctBy(s => s.Ticker, StringComparer.OrdinalIgnoreCase)
.ToList();
return result;
}
/// <inheritdoc />
public async Task<YahooQuoteSummaryModulesDto?> GetQuoteSummaryModulesAsync(
string symbolOrIsin,
bool forceHtmlScrape = false,
bool includeProfile = true,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(symbolOrIsin)) return null;
@@ -220,9 +233,9 @@ public class YahooFinanceScraper : IYahooFinanceScraper
try
{
await _finlyticLogger.LogInfoAsync(CoreSettingKeys.YahooClientChannel,
$"[{_serviceName}] Starte HTML-Scraper Fallback für '{symbol}'...");
$"[{_serviceName}] Starte HTML-Scraper Fallback für '{symbol}' (IncludeProfile: {includeProfile})...");
htmlModules = await _htmlScraperClient.ScrapeQuoteSummaryModulesAsync(symbol, cancellationToken);
htmlModules = await _htmlScraperClient.ScrapeQuoteSummaryModulesAsync(symbol, includeProfile, cancellationToken);
}
catch (Exception ex)
{
@@ -246,16 +259,20 @@ public class YahooFinanceScraper : IYahooFinanceScraper
modules.DefaultKeyStatistics != null;
}
private static YahooQuoteSummaryModulesDto MergeModules(
YahooQuoteSummaryModulesDto primary,
YahooQuoteSummaryModulesDto secondary)
public static YahooQuoteSummaryModulesDto? MergeModules(
YahooQuoteSummaryModulesDto? primary,
YahooQuoteSummaryModulesDto? secondary)
{
if (primary == null && secondary == null) return null;
if (primary == null) return secondary;
if (secondary == null) return primary;
return new YahooQuoteSummaryModulesDto(
QuoteType: primary.QuoteType ?? secondary.QuoteType,
AssetProfile: primary.AssetProfile ?? secondary.AssetProfile,
FinancialData: primary.FinancialData ?? secondary.FinancialData,
DefaultKeyStatistics: primary.DefaultKeyStatistics ?? secondary.DefaultKeyStatistics,
SummaryDetail: primary.SummaryDetail ?? secondary.SummaryDetail,
FinancialData: MergeFinancialData(primary.FinancialData, secondary.FinancialData),
DefaultKeyStatistics: MergeDefaultKeyStatistics(primary.DefaultKeyStatistics, secondary.DefaultKeyStatistics),
SummaryDetail: MergeSummaryDetail(primary.SummaryDetail, secondary.SummaryDetail),
IncomeStatementHistory: primary.IncomeStatementHistory ?? secondary.IncomeStatementHistory,
IncomeStatementHistoryQuarterly: primary.IncomeStatementHistoryQuarterly ?? secondary.IncomeStatementHistoryQuarterly,
BalanceSheetHistory: primary.BalanceSheetHistory ?? secondary.BalanceSheetHistory,
@@ -266,6 +283,124 @@ public class YahooFinanceScraper : IYahooFinanceScraper
);
}
private static YahooFinancialDataDto? MergeFinancialData(YahooFinancialDataDto? a, YahooFinancialDataDto? b)
{
if (a == null) return b;
if (b == null) return a;
return new YahooFinancialDataDto(
CurrentPrice: a.CurrentPrice ?? b.CurrentPrice,
TargetHighPrice: a.TargetHighPrice ?? b.TargetHighPrice,
TargetLowPrice: a.TargetLowPrice ?? b.TargetLowPrice,
TargetMeanPrice: a.TargetMeanPrice ?? b.TargetMeanPrice,
TargetMedianPrice: a.TargetMedianPrice ?? b.TargetMedianPrice,
RecommendationMean: a.RecommendationMean ?? b.RecommendationMean,
RecommendationKey: !string.IsNullOrWhiteSpace(a.RecommendationKey) && a.RecommendationKey != "none" ? a.RecommendationKey : b.RecommendationKey,
NumberOfAnalystOpinions: a.NumberOfAnalystOpinions ?? b.NumberOfAnalystOpinions,
TotalCash: a.TotalCash ?? b.TotalCash,
TotalCashPerShare: a.TotalCashPerShare ?? b.TotalCashPerShare,
Ebitda: a.Ebitda ?? b.Ebitda,
TotalDebt: a.TotalDebt ?? b.TotalDebt,
QuickRatio: a.QuickRatio ?? b.QuickRatio,
CurrentRatio: a.CurrentRatio ?? b.CurrentRatio,
TotalRevenue: a.TotalRevenue ?? b.TotalRevenue,
DebtToEquity: a.DebtToEquity ?? b.DebtToEquity,
RevenuePerShare: a.RevenuePerShare ?? b.RevenuePerShare,
ReturnOnAssets: a.ReturnOnAssets ?? b.ReturnOnAssets,
ReturnOnEquity: a.ReturnOnEquity ?? b.ReturnOnEquity,
GrossProfits: a.GrossProfits ?? b.GrossProfits,
FreeCashflow: a.FreeCashflow ?? b.FreeCashflow,
OperatingCashflow: a.OperatingCashflow ?? b.OperatingCashflow,
RevenueGrowth: a.RevenueGrowth ?? b.RevenueGrowth,
GrossMargins: a.GrossMargins ?? b.GrossMargins,
EbitdaMargins: a.EbitdaMargins ?? b.EbitdaMargins,
OperatingMargins: a.OperatingMargins ?? b.OperatingMargins,
ProfitMargins: a.ProfitMargins ?? b.ProfitMargins,
FinancialCurrency: a.FinancialCurrency ?? b.FinancialCurrency
);
}
private static YahooDefaultKeyStatisticsDto? MergeDefaultKeyStatistics(YahooDefaultKeyStatisticsDto? a, YahooDefaultKeyStatisticsDto? b)
{
if (a == null) return b;
if (b == null) return a;
return new YahooDefaultKeyStatisticsDto(
PriceToBook: a.PriceToBook ?? b.PriceToBook,
EnterpriseValue: a.EnterpriseValue ?? b.EnterpriseValue,
ForwardPE: a.ForwardPE ?? b.ForwardPE,
ProfitMargins: a.ProfitMargins ?? b.ProfitMargins,
FloatShares: a.FloatShares ?? b.FloatShares,
SharesOutstanding: a.SharesOutstanding ?? b.SharesOutstanding,
SharesShort: a.SharesShort ?? b.SharesShort,
SharesShortPriorMonth: a.SharesShortPriorMonth ?? b.SharesShortPriorMonth,
SharesShortPreviousMonthDate: a.SharesShortPreviousMonthDate ?? b.SharesShortPreviousMonthDate,
DateShortInterest: a.DateShortInterest ?? b.DateShortInterest,
SharesPercentSharesOut: a.SharesPercentSharesOut ?? b.SharesPercentSharesOut,
HeldPercentInsiders: a.HeldPercentInsiders ?? b.HeldPercentInsiders,
HeldPercentInstitutions: a.HeldPercentInstitutions ?? b.HeldPercentInstitutions,
ShortRatio: a.ShortRatio ?? b.ShortRatio,
ShortPercentOfFloat: a.ShortPercentOfFloat ?? b.ShortPercentOfFloat,
Beta: a.Beta ?? b.Beta,
Category: a.Category ?? b.Category,
BookValue: a.BookValue ?? b.BookValue,
PriceToSalesTrailing12Months: a.PriceToSalesTrailing12Months ?? b.PriceToSalesTrailing12Months,
LastFiscalYearEnd: a.LastFiscalYearEnd ?? b.LastFiscalYearEnd,
NextFiscalYearEnd: a.NextFiscalYearEnd ?? b.NextFiscalYearEnd,
MostRecentQuarter: a.MostRecentQuarter ?? b.MostRecentQuarter,
EarningsQuarterlyGrowth: a.EarningsQuarterlyGrowth ?? b.EarningsQuarterlyGrowth,
NetIncomeToCommon: a.NetIncomeToCommon ?? b.NetIncomeToCommon,
TrailingEps: a.TrailingEps ?? b.TrailingEps,
ForwardEps: a.ForwardEps ?? b.ForwardEps,
PegRatio: a.PegRatio ?? b.PegRatio,
EnterpriseToRevenue: a.EnterpriseToRevenue ?? b.EnterpriseToRevenue,
EnterpriseToEbitda: a.EnterpriseToEbitda ?? b.EnterpriseToEbitda,
FiftyTwoWeekChange: a.FiftyTwoWeekChange ?? b.FiftyTwoWeekChange,
SandP52WeekChange: a.SandP52WeekChange ?? b.SandP52WeekChange
);
}
private static YahooSummaryDetailDto? MergeSummaryDetail(YahooSummaryDetailDto? a, YahooSummaryDetailDto? b)
{
if (a == null) return b;
if (b == null) return a;
return new YahooSummaryDetailDto(
MaxAge: a.MaxAge ?? b.MaxAge,
PriceHint: a.PriceHint ?? b.PriceHint,
PreviousClose: a.PreviousClose ?? b.PreviousClose,
Open: a.Open ?? b.Open,
DayLow: a.DayLow ?? b.DayLow,
DayHigh: a.DayHigh ?? b.DayHigh,
RegularMarketPreviousClose: a.RegularMarketPreviousClose ?? b.RegularMarketPreviousClose,
RegularMarketOpen: a.RegularMarketOpen ?? b.RegularMarketOpen,
RegularMarketDayLow: a.RegularMarketDayLow ?? b.RegularMarketDayLow,
RegularMarketDayHigh: a.RegularMarketDayHigh ?? b.RegularMarketDayHigh,
DividendRate: a.DividendRate ?? b.DividendRate,
DividendYield: a.DividendYield ?? b.DividendYield,
ExDividendDate: a.ExDividendDate ?? b.ExDividendDate,
PayoutRatio: a.PayoutRatio ?? b.PayoutRatio,
FiveYearAvgDividendYield: a.FiveYearAvgDividendYield ?? b.FiveYearAvgDividendYield,
Beta: a.Beta ?? b.Beta,
TrailingPE: a.TrailingPE ?? b.TrailingPE,
ForwardPE: a.ForwardPE ?? b.ForwardPE,
Volume: a.Volume ?? b.Volume,
RegularMarketVolume: a.RegularMarketVolume ?? b.RegularMarketVolume,
AverageVolume: a.AverageVolume ?? b.AverageVolume,
AverageVolume10days: a.AverageVolume10days ?? b.AverageVolume10days,
AverageDailyVolume10Day: a.AverageDailyVolume10Day ?? b.AverageDailyVolume10Day,
Bid: a.Bid ?? b.Bid,
Ask: a.Ask ?? b.Ask,
BidSize: a.BidSize ?? b.BidSize,
AskSize: a.AskSize ?? b.AskSize,
MarketCap: a.MarketCap ?? b.MarketCap,
FiftyTwoWeekLow: a.FiftyTwoWeekLow ?? b.FiftyTwoWeekLow,
FiftyTwoWeekHigh: a.FiftyTwoWeekHigh ?? b.FiftyTwoWeekHigh,
PriceToSalesTrailing12Months: a.PriceToSalesTrailing12Months ?? b.PriceToSalesTrailing12Months,
Currency: a.Currency ?? b.Currency
);
}
private static bool IsIsin(string value)
{
return value.Length == 12 &&