feat(fundamentals): multi-ticker DB caching, parallel html scraper, and frontend mappings
This commit is contained in:
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user