feat(fundamentals): preserve ticker selection and add deterministic key executives sorting
This commit is contained in:
@@ -129,92 +129,85 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
"[DEBUG-TR-ERROR] Could not fetch Trade Republic details for {Isin}", cleanIsin);
|
||||
}
|
||||
|
||||
// --- STEP 2: Ticker auflösen (Null-safe) ---
|
||||
TickerInfoDto primaryTicker;
|
||||
// --- STEP 2: Ticker auflösen (Der Primary Ticker ist IMMER der 1. von Yahoo Finance) ---
|
||||
var resolvedTickers = await _scraper.ResolveAllTickersFromIsinAsync(cleanIsin, cancellationToken);
|
||||
var yahooPrimaryTicker = resolvedTickers.FirstOrDefault()
|
||||
?? (assetData?.PrimaryTicker != null && !string.IsNullOrWhiteSpace(assetData.PrimaryTicker.Ticker)
|
||||
? new TickerInfoDto { Ticker = assetData.PrimaryTicker.Ticker, Exchange = assetData.PrimaryTicker.Exchange ?? "Unknown" }
|
||||
: new TickerInfoDto { Ticker = cleanIsin, Exchange = "Unknown" });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(yahooPrimaryTicker.Exchange))
|
||||
{
|
||||
yahooPrimaryTicker = new TickerInfoDto
|
||||
{
|
||||
Ticker = yahooPrimaryTicker.Ticker,
|
||||
Exchange = GetExchangeDisplayName(yahooPrimaryTicker.Ticker)
|
||||
};
|
||||
}
|
||||
|
||||
// Der activeQueryTicker wird für die aktuelle Kurs- und Modulabfrage verwendet (z. B. wenn der User im Web UI einen bestimmten Börsenplatz wählt)
|
||||
TickerInfoDto activeQueryTicker;
|
||||
if (!string.IsNullOrWhiteSpace(requestedTicker))
|
||||
{
|
||||
var match = assetData?.AvailableTickers?
|
||||
.FirstOrDefault(a => string.Equals(a.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase));
|
||||
var matchDto = resolvedTickers.FirstOrDefault(a => string.Equals(a.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase));
|
||||
var matchEntity = assetData?.AvailableTickers?.FirstOrDefault(a => string.Equals(a.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match != null)
|
||||
if (matchDto != null)
|
||||
{
|
||||
primaryTicker = new TickerInfoDto
|
||||
activeQueryTicker = new TickerInfoDto
|
||||
{
|
||||
Ticker = match.Ticker,
|
||||
Exchange = !string.IsNullOrWhiteSpace(match.Exchange)
|
||||
? match.Exchange
|
||||
: GetExchangeDisplayName(match.Ticker)
|
||||
Ticker = matchDto.Ticker,
|
||||
Exchange = !string.IsNullOrWhiteSpace(matchDto.Exchange) ? matchDto.Exchange : GetExchangeDisplayName(matchDto.Ticker)
|
||||
};
|
||||
}
|
||||
else if (assetData?.PrimaryTicker != null &&
|
||||
string.Equals(assetData.PrimaryTicker.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase))
|
||||
else if (matchEntity != null)
|
||||
{
|
||||
primaryTicker = new TickerInfoDto
|
||||
activeQueryTicker = new TickerInfoDto
|
||||
{
|
||||
Ticker = assetData.PrimaryTicker.Ticker,
|
||||
Exchange = !string.IsNullOrWhiteSpace(assetData.PrimaryTicker.Exchange)
|
||||
? assetData.PrimaryTicker.Exchange
|
||||
: GetExchangeDisplayName(assetData.PrimaryTicker.Ticker)
|
||||
Ticker = matchEntity.Ticker,
|
||||
Exchange = !string.IsNullOrWhiteSpace(matchEntity.Exchange) ? matchEntity.Exchange : GetExchangeDisplayName(matchEntity.Ticker)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
primaryTicker = new TickerInfoDto
|
||||
activeQueryTicker = new TickerInfoDto
|
||||
{
|
||||
Ticker = requestedTicker,
|
||||
Exchange = GetExchangeDisplayName(requestedTicker)
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (assetData?.PrimaryTicker != null && !string.IsNullOrWhiteSpace(assetData.PrimaryTicker.Ticker))
|
||||
{
|
||||
primaryTicker = new TickerInfoDto
|
||||
{
|
||||
Ticker = assetData.PrimaryTicker.Ticker,
|
||||
Exchange = !string.IsNullOrWhiteSpace(assetData.PrimaryTicker.Exchange)
|
||||
? assetData.PrimaryTicker.Exchange
|
||||
: GetExchangeDisplayName(assetData.PrimaryTicker.Ticker)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var resolved = await _scraper.ResolveTickerFromIsinAsync(cleanIsin, cancellationToken);
|
||||
primaryTicker = resolved != null && !string.IsNullOrWhiteSpace(resolved.Ticker)
|
||||
? resolved
|
||||
: new TickerInfoDto
|
||||
{
|
||||
Ticker = cleanIsin,
|
||||
Exchange = "Unknown"
|
||||
};
|
||||
activeQueryTicker = yahooPrimaryTicker;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(primaryTicker.Exchange))
|
||||
if (string.IsNullOrWhiteSpace(activeQueryTicker.Exchange))
|
||||
{
|
||||
primaryTicker = new TickerInfoDto
|
||||
activeQueryTicker = new TickerInfoDto
|
||||
{
|
||||
Ticker = primaryTicker.Ticker,
|
||||
Exchange = GetExchangeDisplayName(primaryTicker.Ticker)
|
||||
Ticker = activeQueryTicker.Ticker,
|
||||
Exchange = GetExchangeDisplayName(activeQueryTicker.Ticker)
|
||||
};
|
||||
}
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.FundamentalsChannel,
|
||||
"[DEBUG-TICKER-RESOLVED] Ticker aufgelöst zu: '{Ticker}' (Exchange: '{Exchange}') für ISIN {Isin}",
|
||||
primaryTicker.Ticker, primaryTicker.Exchange ?? "Unknown", cleanIsin);
|
||||
"[DEBUG-TICKER-RESOLVED] PrimaryTicker: '{Primary}' | ActiveQueryTicker: '{Active}' für ISIN {Isin}",
|
||||
yahooPrimaryTicker.Ticker, activeQueryTicker.Ticker, cleanIsin);
|
||||
|
||||
// --- STEP 3 & 4: Yahoo Finance API & HTML Fallback über Scraper ---
|
||||
YahooQuoteSummaryModulesDto? modulesDto = null;
|
||||
if (!string.IsNullOrWhiteSpace(primaryTicker.Ticker) && primaryTicker.Ticker != cleanIsin)
|
||||
if (!string.IsNullOrWhiteSpace(activeQueryTicker.Ticker) && activeQueryTicker.Ticker != cleanIsin)
|
||||
{
|
||||
modulesDto = await _scraper.GetQuoteSummaryModulesAsync(
|
||||
primaryTicker.Ticker,
|
||||
activeQueryTicker.Ticker,
|
||||
forceHtmlScrape: false,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.FundamentalsChannel,
|
||||
"[DEBUG-YAHOO-SKIPPED] Yahoo-Abruf übersprungen. Ticker: '{Ticker}'", primaryTicker.Ticker);
|
||||
"[DEBUG-YAHOO-SKIPPED] Yahoo-Abruf übersprungen. Ticker: '{Ticker}'", activeQueryTicker.Ticker);
|
||||
}
|
||||
|
||||
// --- Update AssetDataEntity ---
|
||||
@@ -227,8 +220,8 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
Isin = cleanIsin,
|
||||
PrimaryTicker = new TickerEntity
|
||||
{
|
||||
Ticker = primaryTicker.Ticker,
|
||||
Exchange = primaryTicker.Exchange ?? "Unknown"
|
||||
Ticker = yahooPrimaryTicker.Ticker,
|
||||
Exchange = yahooPrimaryTicker.Exchange ?? "Unknown"
|
||||
},
|
||||
KeyExecutives = new List<KeyExecutiveEntity>(),
|
||||
AssetEvents = new List<AssetEventEntity>()
|
||||
@@ -241,27 +234,27 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
|
||||
string fallbackName = modulesDto?.QuoteType?.ShortName
|
||||
?? modulesDto?.QuoteType?.LongName
|
||||
?? primaryTicker.Ticker;
|
||||
?? activeQueryTicker.Ticker;
|
||||
|
||||
assetData.Name = !string.IsNullOrWhiteSpace(trName) ? trName : fallbackName;
|
||||
assetData.Description = !string.IsNullOrWhiteSpace(trDescription)
|
||||
? trDescription
|
||||
: (modulesDto?.AssetProfile?.LongBusinessSummary ?? string.Empty);
|
||||
|
||||
// PrimaryTicker ist FEST der erste von Yahoo Finance
|
||||
assetData.PrimaryTicker = new TickerEntity
|
||||
{
|
||||
Ticker = primaryTicker.Ticker,
|
||||
Exchange = primaryTicker.Exchange ?? "Unknown"
|
||||
Ticker = yahooPrimaryTicker.Ticker,
|
||||
Exchange = yahooPrimaryTicker.Exchange ?? "Unknown"
|
||||
};
|
||||
|
||||
var tickers = await _scraper.ResolveAllTickersFromIsinAsync(cleanIsin, cancellationToken);
|
||||
if (!tickers.Any(t => string.Equals(t.Ticker, primaryTicker.Ticker, StringComparison.OrdinalIgnoreCase)))
|
||||
if (!resolvedTickers.Any(t => string.Equals(t.Ticker, yahooPrimaryTicker.Ticker, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
tickers.Insert(0, primaryTicker);
|
||||
resolvedTickers.Insert(0, yahooPrimaryTicker);
|
||||
}
|
||||
|
||||
assetData.AvailableTickers.Clear();
|
||||
foreach (var a in tickers)
|
||||
foreach (var a in resolvedTickers)
|
||||
{
|
||||
assetData.AvailableTickers.Add(new TickerEntity
|
||||
{
|
||||
@@ -278,7 +271,21 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
// --- Process Trade Republic Corporate Events ---
|
||||
if (trDetails != null && (shouldUpdateAssetData || effectiveForceRefresh) && assetData != null)
|
||||
{
|
||||
assetData.AssetEvents ??= new List<AssetEventEntity>();
|
||||
// 1. Alte Events direkt in der DB löschen (bypasses Change Tracker)
|
||||
await context.AssetEvents
|
||||
.Where(e => e.AssetDataIsin == cleanIsin)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
// 2. ALLE tracked AssetEventEntity-Einträge aus dem Change Tracker entfernen
|
||||
foreach (var entry in context.ChangeTracker.Entries<AssetEventEntity>()
|
||||
.Where(e => e.Entity.AssetDataIsin == cleanIsin)
|
||||
.ToList())
|
||||
{
|
||||
entry.State = EntityState.Detached;
|
||||
}
|
||||
|
||||
// 3. Navigation-Collection zurücksetzen
|
||||
assetData.AssetEvents = new List<AssetEventEntity>();
|
||||
|
||||
var trEventList = new List<TradeRepublicEventDto>();
|
||||
if (trDetails.Events != null) trEventList.AddRange(trDetails.Events);
|
||||
@@ -298,17 +305,19 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
|
||||
if (!isDuplicate)
|
||||
{
|
||||
assetData.AssetEvents.Add(new AssetEventEntity
|
||||
var newEvent = new AssetEventEntity
|
||||
{
|
||||
AssetDataIsin = cleanIsin,
|
||||
Ticker = new TickerEntity
|
||||
{
|
||||
Ticker = primaryTicker.Ticker,
|
||||
Exchange = primaryTicker.Exchange ?? "Unknown"
|
||||
Ticker = yahooPrimaryTicker.Ticker,
|
||||
Exchange = yahooPrimaryTicker.Exchange ?? "Unknown"
|
||||
},
|
||||
Type = evtType,
|
||||
Date = evtDate
|
||||
});
|
||||
};
|
||||
context.AssetEvents.Add(newEvent);
|
||||
assetData.AssetEvents.Add(newEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,6 +348,7 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
// 4. Neue Executives aufbauen und direkt über den DbSet hinzufügen
|
||||
if (modulesDto.AssetProfile?.CompanyOfficers != null)
|
||||
{
|
||||
int sortIdx = 0;
|
||||
foreach (var officer in modulesDto.AssetProfile.CompanyOfficers)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(officer.Name))
|
||||
@@ -349,7 +359,8 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
Name = officer.Name,
|
||||
Title = officer.Title ?? string.Empty,
|
||||
Payment = officer.TotalPay?.Fmt ??
|
||||
(officer.TotalPay?.Raw?.ToString() ?? string.Empty)
|
||||
(officer.TotalPay?.Raw?.ToString() ?? string.Empty),
|
||||
SortOrder = sortIdx++
|
||||
};
|
||||
context.KeyExecutives.Add(newExec);
|
||||
assetData.KeyExecutives.Add(newExec);
|
||||
@@ -377,8 +388,8 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
|
||||
fundamentalData.Ticker = new TickerEntity
|
||||
{
|
||||
Ticker = primaryTicker.Ticker,
|
||||
Exchange = primaryTicker.Exchange ?? "Unknown"
|
||||
Ticker = activeQueryTicker.Ticker,
|
||||
Exchange = activeQueryTicker.Exchange ?? "Unknown"
|
||||
};
|
||||
fundamentalData.MarketCap = (decimal?)modulesDto.SummaryDetail?.MarketCap?.Raw;
|
||||
fundamentalData.EnterpriseValue =
|
||||
@@ -452,7 +463,10 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
|
||||
if (assetData == null) return null;
|
||||
|
||||
var executivesList = assetData.KeyExecutives?.ToList() ?? new List<KeyExecutiveEntity>();
|
||||
var executivesList = (assetData.KeyExecutives ?? Enumerable.Empty<KeyExecutiveEntity>())
|
||||
.OrderBy(e => e.SortOrder > 0 ? e.SortOrder : GetExecutiveRank(e.Title))
|
||||
.ThenBy(e => GetExecutiveRank(e.Title))
|
||||
.ToList();
|
||||
var eventsList = assetData.AssetEvents?.ToList() ?? new List<AssetEventEntity>();
|
||||
|
||||
return MapToDto(assetData, fundamentalData, executivesList, eventsList);
|
||||
@@ -610,12 +624,16 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
LastUpdatedUtc = fundData.LastUpdatedUtc
|
||||
}
|
||||
: null,
|
||||
Executives = executives.Select(e => new KeyExecutiveDto
|
||||
Executives = executives
|
||||
.OrderBy(e => e.SortOrder > 0 ? e.SortOrder : GetExecutiveRank(e.Title))
|
||||
.ThenBy(e => GetExecutiveRank(e.Title))
|
||||
.Select(e => new KeyExecutiveDto
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name,
|
||||
Title = e.Title,
|
||||
Payment = e.Payment
|
||||
Payment = e.Payment,
|
||||
SortOrder = e.SortOrder
|
||||
}).ToList(),
|
||||
Events = events.Select(e => new CorporateEventDto
|
||||
{
|
||||
@@ -666,4 +684,21 @@ public class FundamentalsDbService : IFundamentalsDbService
|
||||
|
||||
return "Other";
|
||||
}
|
||||
|
||||
private static int GetExecutiveRank(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title)) return 99;
|
||||
var t = title.ToUpperInvariant();
|
||||
|
||||
if (t.Contains("CEO") || t.Contains("CHIEF EXECUTIVE") || t.Contains("VORSTANDSVORSITZEND") || t.Contains("MANAGING DIRECTOR")) return 1;
|
||||
if (t.Contains("CFO") || t.Contains("CHIEF FINANCIAL") || t.Contains("FINANZVORSTAND")) return 2;
|
||||
if (t.Contains("COO") || t.Contains("CHIEF OPERATING")) return 3;
|
||||
if (t.Contains("CTO") || t.Contains("CHIEF TECHNOLOGY") || t.Contains("CIO") || t.Contains("CHIEF INFORMATION")) return 4;
|
||||
if (t.Contains("CMO") || t.Contains("CHIEF MARKETING") || t.Contains("CHIEF COMMERCIAL")) return 5;
|
||||
if (t.Contains("PRESIDENT") || t.Contains("EXECUTIVE VICE PRESIDENT") || t.Contains("EVP") || t.Contains("GENERAL COUNSEL") || t.Contains("CHIEF LEGAL")) return 6;
|
||||
if (t.Contains("SENIOR VICE PRESIDENT") || t.Contains("SVP") || t.Contains("VICE PRESIDENT") || t.Contains("VP")) return 7;
|
||||
if (t.Contains("DIRECTOR") || t.Contains("AUFSICHTSRAT") || t.Contains("VORSTAND") || t.Contains("BOARD")) return 8;
|
||||
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user