feat(fundamentals): preserve ticker selection and add deterministic key executives sorting

This commit is contained in:
2026-08-15 19:30:04 +02:00
parent 960a4bdbd6
commit 7e18257e3e
11 changed files with 942 additions and 235 deletions
@@ -77,6 +77,8 @@ public class FundamentalsDbContext : DbContext
modelBuilder.Entity<KeyExecutiveEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.SortOrder).HasDefaultValue(0);
entity.HasIndex(e => new { e.AssetDataIsin, e.SortOrder });
});
modelBuilder.Entity<AssetEventEntity>(entity =>
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace FinlyticFundamentals.Entities;
@@ -10,6 +10,7 @@ public class KeyExecutiveEntity
public string Name { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Payment { get; set; } = string.Empty;
public int SortOrder { get; set; } = 0;
[Required] public string AssetDataIsin { get; set; } = string.Empty;
@@ -0,0 +1,429 @@
// <auto-generated />
using System;
using FinlyticFundamentals.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticFundamentals.Migrations
{
[DbContext(typeof(FundamentalsDbContext))]
[Migration("20260815161429_AddSortOrderToKeyExecutives")]
partial class AddSortOrderToKeyExecutives
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key");
b.ToTable("DynamicSettings");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
{
b.Property<string>("Isin")
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Isin");
b.ToTable("AssetData");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetEventEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AssetDataIsin");
b.ToTable("AssetEvents");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalDataEntity", b =>
{
b.Property<string>("Isin")
.HasColumnType("text");
b.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ConsensusRating")
.HasColumnType("text");
b.Property<decimal?>("CurrentRatio")
.HasColumnType("numeric");
b.Property<decimal?>("DebtToEquity")
.HasColumnType("numeric");
b.Property<decimal?>("DilutedEps")
.HasColumnType("numeric");
b.Property<decimal?>("Ebitda")
.HasColumnType("numeric");
b.Property<decimal?>("EnterpriseValue")
.HasColumnType("numeric");
b.Property<decimal?>("EvToEbitda")
.HasColumnType("numeric");
b.Property<decimal?>("FiftyTwoWeekHigh")
.HasColumnType("numeric");
b.Property<decimal?>("FiftyTwoWeekLow")
.HasColumnType("numeric");
b.Property<decimal?>("ForwardDividendYield")
.HasColumnType("numeric");
b.Property<decimal?>("ForwardPe")
.HasColumnType("numeric");
b.Property<decimal?>("FreeCashFlow")
.HasColumnType("numeric");
b.Property<decimal?>("GrossProfit")
.HasColumnType("numeric");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("MarketCap")
.HasColumnType("numeric");
b.Property<decimal?>("NetIncome")
.HasColumnType("numeric");
b.Property<decimal?>("OperatingCashFlow")
.HasColumnType("numeric");
b.Property<decimal?>("OperatingIncome")
.HasColumnType("numeric");
b.Property<decimal?>("PayoutRatio")
.HasColumnType("numeric");
b.Property<decimal?>("PegRatio")
.HasColumnType("numeric");
b.Property<decimal?>("PercentHeldByInsiders")
.HasColumnType("numeric");
b.Property<decimal?>("PercentHeldByInstitutions")
.HasColumnType("numeric");
b.Property<decimal?>("PriceTargetHigh")
.HasColumnType("numeric");
b.Property<decimal?>("PriceTargetLow")
.HasColumnType("numeric");
b.Property<decimal?>("PriceTargetMean")
.HasColumnType("numeric");
b.Property<decimal?>("PriceToBook")
.HasColumnType("numeric");
b.Property<decimal?>("PriceToSales")
.HasColumnType("numeric");
b.Property<decimal?>("ReturnOnAssets")
.HasColumnType("numeric");
b.Property<decimal?>("ReturnOnEquity")
.HasColumnType("numeric");
b.Property<decimal?>("RevenueGrowthYoY")
.HasColumnType("numeric");
b.Property<decimal?>("ShortPercentOfFloat")
.HasColumnType("numeric");
b.Property<decimal?>("ShortRatio")
.HasColumnType("numeric");
b.Property<decimal?>("TotalCash")
.HasColumnType("numeric");
b.Property<decimal?>("TotalDebt")
.HasColumnType("numeric");
b.Property<decimal?>("TotalRevenue")
.HasColumnType("numeric");
b.Property<decimal?>("TrailingPe")
.HasColumnType("numeric");
b.HasKey("Isin");
b.HasIndex("AssetDataIsin");
b.ToTable("FundamentalData");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.KeyExecutiveEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Payment")
.IsRequired()
.HasColumnType("text");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AssetDataIsin", "SortOrder");
b.ToTable("KeyExecutives");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
{
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "PrimaryTicker", b1 =>
{
b1.Property<string>("AssetDataEntityIsin")
.HasColumnType("text");
b1.Property<string>("Exchange")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("PrimaryTickerExchange");
b1.Property<string>("Ticker")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("PrimaryTicker");
b1.HasKey("AssetDataEntityIsin");
b1.ToTable("AssetData");
b1.WithOwner()
.HasForeignKey("AssetDataEntityIsin");
});
b.OwnsMany("FinlyticFundamentals.Entities.TickerEntity", "AvailableTickers", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Exchange")
.IsRequired()
.HasColumnType("text")
.HasColumnName("Exchange");
b1.Property<string>("Ticker")
.IsRequired()
.HasColumnType("text")
.HasColumnName("Ticker");
b1.HasKey("Id");
b1.HasIndex("AssetDataIsin");
b1.HasIndex("Ticker");
b1.ToTable("Tickers", (string)null);
b1.WithOwner()
.HasForeignKey("AssetDataIsin");
});
b.Navigation("AvailableTickers");
b.Navigation("PrimaryTicker")
.IsRequired();
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetEventEntity", b =>
{
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
.WithMany("AssetEvents")
.HasForeignKey("AssetDataIsin")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "Ticker", b1 =>
{
b1.Property<Guid>("AssetEventEntityId")
.HasColumnType("uuid");
b1.Property<string>("Exchange")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("TickerExchange");
b1.Property<string>("Ticker")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("Ticker");
b1.HasKey("AssetEventEntityId");
b1.ToTable("AssetEvents");
b1.WithOwner()
.HasForeignKey("AssetEventEntityId");
});
b.Navigation("AssetData");
b.Navigation("Ticker")
.IsRequired();
});
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalDataEntity", b =>
{
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
.WithMany("FundamentalData")
.HasForeignKey("AssetDataIsin")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "Ticker", b1 =>
{
b1.Property<string>("FundamentalDataEntityIsin")
.HasColumnType("text");
b1.Property<string>("Exchange")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("TickerExchange");
b1.Property<string>("Ticker")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("Ticker");
b1.HasKey("FundamentalDataEntityIsin");
b1.ToTable("FundamentalData");
b1.WithOwner()
.HasForeignKey("FundamentalDataEntityIsin");
});
b.Navigation("AssetData");
b.Navigation("Ticker")
.IsRequired();
});
modelBuilder.Entity("FinlyticFundamentals.Entities.KeyExecutiveEntity", b =>
{
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
.WithMany("KeyExecutives")
.HasForeignKey("AssetDataIsin")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AssetData");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
{
b.Navigation("AssetEvents");
b.Navigation("FundamentalData");
b.Navigation("KeyExecutives");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticFundamentals.Migrations
{
/// <inheritdoc />
public partial class AddSortOrderToKeyExecutives : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_KeyExecutives_AssetDataIsin",
table: "KeyExecutives");
migrationBuilder.AddColumn<int>(
name: "SortOrder",
table: "KeyExecutives",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_KeyExecutives_AssetDataIsin_SortOrder",
table: "KeyExecutives",
columns: new[] { "AssetDataIsin", "SortOrder" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_KeyExecutives_AssetDataIsin_SortOrder",
table: "KeyExecutives");
migrationBuilder.DropColumn(
name: "SortOrder",
table: "KeyExecutives");
migrationBuilder.CreateIndex(
name: "IX_KeyExecutives_AssetDataIsin",
table: "KeyExecutives",
column: "AssetDataIsin");
}
}
}
@@ -236,13 +236,18 @@ namespace FinlyticFundamentals.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AssetDataIsin");
b.HasIndex("AssetDataIsin", "SortOrder");
b.ToTable("KeyExecutives");
});
@@ -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;
}
}
@@ -130,6 +130,7 @@ public class YahooFinanceScraper : IYahooFinanceScraper
foreach (var q in validQuotes.Skip(1))
{
if (!symbols.Any(s => s.symbol.Equals(q.Symbol, StringComparison.OrdinalIgnoreCase)))
{
symbols.Add((q.Symbol, q.Exchange ?? string.Empty, Math.Max(1, GetExchangePriority(q.Symbol, cleanIsin))));