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
@@ -0,0 +1,64 @@
import '../../favorites/models/favorite_asset_model.dart';
import '../models/fundamental_data_model.dart';
class TickerResolver {
/// Resolves the optimal ticker according to user prioritization:
/// 1. Candidate / active user-selected symbol (if valid and not equal to ISIN)
/// 2. Favorite selected ticker (if asset is in favorites and has a valid ticker)
/// 3. Primary Ticker (from Fundamentals header)
/// 4. First available ticker from AvailableTickers list
/// 5. ISIN fallback
static String? resolve({
required String isin,
String? candidateSymbol,
List<FavoriteAssetModel>? favoriteDetails,
FundamentalDataModel? fundamentals,
}) {
final cleanIsin = isin.trim().toUpperCase();
// 1. Check Candidate / User-selected symbol
if (candidateSymbol != null && candidateSymbol.trim().isNotEmpty) {
final cClean = candidateSymbol.trim();
if (cClean.toUpperCase() != cleanIsin) {
return cClean;
}
}
// 2. Check Favorite selected ticker
if (favoriteDetails != null && favoriteDetails.isNotEmpty) {
for (final f in favoriteDetails) {
final fIsin = f.isin.trim().toUpperCase();
final fSym = f.symbol.trim().toUpperCase();
if (fIsin == cleanIsin || fSym == cleanIsin) {
if (f.symbol.trim().isNotEmpty && f.symbol.trim().toUpperCase() != cleanIsin) {
return f.symbol.trim();
}
}
}
}
// 3. Check Fundamentals Primary Ticker
if (fundamentals != null) {
final primary = fundamentals.primaryTicker.trim();
if (primary.isNotEmpty && primary.toUpperCase() != cleanIsin) {
return primary;
}
final fTicker = fundamentals.ticker.trim();
if (fTicker.isNotEmpty && fTicker.toUpperCase() != cleanIsin) {
return fTicker;
}
// 4. First available ticker in availableTickers
for (final t in fundamentals.availableTickers) {
final tTick = t.ticker.trim();
if (tTick.isNotEmpty && tTick.toUpperCase() != cleanIsin) {
return tTick;
}
}
}
// 5. Fallback: ISIN
return isin.trim().isNotEmpty ? isin.trim() : null;
}
}
@@ -4,10 +4,12 @@ import '../../../../core/theme/app_theme.dart';
import '../../../favorites/cubit/favorites_cubit.dart'; import '../../../favorites/cubit/favorites_cubit.dart';
import '../../bloc/fundamentals/asset_fundamentals_bloc.dart'; import '../../bloc/fundamentals/asset_fundamentals_bloc.dart';
import '../../bloc/fundamentals/asset_fundamentals_event.dart'; import '../../bloc/fundamentals/asset_fundamentals_event.dart';
import '../../bloc/fundamentals/asset_fundamentals_state.dart';
import '../../bloc/technical/asset_technical_bloc.dart'; import '../../bloc/technical/asset_technical_bloc.dart';
import '../../bloc/technical/asset_technical_event.dart'; import '../../bloc/technical/asset_technical_event.dart';
import '../../bloc/trades/asset_trades_bloc.dart'; import '../../bloc/trades/asset_trades_bloc.dart';
import '../../bloc/trades/asset_trades_event.dart'; import '../../bloc/trades/asset_trades_event.dart';
import '../../utils/ticker_resolver.dart';
import '../../widgets/header/asset_hero_header.dart'; import '../../widgets/header/asset_hero_header.dart';
import '../tabs/fundamentals_tab.dart'; import '../tabs/fundamentals_tab.dart';
import '../tabs/technical_tab.dart'; import '../tabs/technical_tab.dart';
@@ -74,7 +76,36 @@ class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = AppTheme.activePreset; final theme = AppTheme.activePreset;
return SingleChildScrollView( return BlocListener<AssetFundamentalsBloc, AssetFundamentalsState>(
listener: (context, state) {
if (state is AssetFundamentalsLoaded && state.data != null) {
if (_selectedTicker == null ||
_selectedTicker!.trim().isEmpty ||
_selectedTicker!.trim().toUpperCase() == widget.isin.trim().toUpperCase()) {
final favList = context.read<FavoritesCubit>().state.favoriteDetails;
final bestTicker = TickerResolver.resolve(
isin: widget.isin,
candidateSymbol: widget.selectedTicker,
favoriteDetails: favList,
fundamentals: state.data,
);
if (bestTicker != null &&
bestTicker.isNotEmpty &&
bestTicker.toUpperCase() != widget.isin.toUpperCase()) {
setState(() {
_selectedTicker = bestTicker;
});
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(
widget.isin,
ticker: bestTicker,
forceRefresh: false,
));
}
}
}
},
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -169,6 +200,6 @@ class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout>
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
), ),
); ));
} }
} }
@@ -4,10 +4,12 @@ import '../../../../core/theme/app_theme.dart';
import '../../../favorites/cubit/favorites_cubit.dart'; import '../../../favorites/cubit/favorites_cubit.dart';
import '../../bloc/fundamentals/asset_fundamentals_bloc.dart'; import '../../bloc/fundamentals/asset_fundamentals_bloc.dart';
import '../../bloc/fundamentals/asset_fundamentals_event.dart'; import '../../bloc/fundamentals/asset_fundamentals_event.dart';
import '../../bloc/fundamentals/asset_fundamentals_state.dart';
import '../../bloc/technical/asset_technical_bloc.dart'; import '../../bloc/technical/asset_technical_bloc.dart';
import '../../bloc/technical/asset_technical_event.dart'; import '../../bloc/technical/asset_technical_event.dart';
import '../../bloc/trades/asset_trades_bloc.dart'; import '../../bloc/trades/asset_trades_bloc.dart';
import '../../bloc/trades/asset_trades_event.dart'; import '../../bloc/trades/asset_trades_event.dart';
import '../../utils/ticker_resolver.dart';
import '../../widgets/header/asset_hero_header.dart'; import '../../widgets/header/asset_hero_header.dart';
import '../tabs/fundamentals_tab.dart'; import '../tabs/fundamentals_tab.dart';
import '../tabs/technical_tab.dart'; import '../tabs/technical_tab.dart';
@@ -74,7 +76,36 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = AppTheme.activePreset; final theme = AppTheme.activePreset;
return SingleChildScrollView( return BlocListener<AssetFundamentalsBloc, AssetFundamentalsState>(
listener: (context, state) {
if (state is AssetFundamentalsLoaded && state.data != null) {
if (_selectedTicker == null ||
_selectedTicker!.trim().isEmpty ||
_selectedTicker!.trim().toUpperCase() == widget.isin.trim().toUpperCase()) {
final favList = context.read<FavoritesCubit>().state.favoriteDetails;
final bestTicker = TickerResolver.resolve(
isin: widget.isin,
candidateSymbol: widget.selectedTicker,
favoriteDetails: favList,
fundamentals: state.data,
);
if (bestTicker != null &&
bestTicker.isNotEmpty &&
bestTicker.toUpperCase() != widget.isin.toUpperCase()) {
setState(() {
_selectedTicker = bestTicker;
});
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(
widget.isin,
ticker: bestTicker,
forceRefresh: false,
));
}
}
}
},
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -115,27 +146,27 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
border: Border.all(color: theme.glassBorder), border: Border.all(color: theme.glassBorder),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TabBar( TabBar(
controller: _tabController, controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
isScrollable: true, isScrollable: true,
tabAlignment: TabAlignment.start, tabAlignment: TabAlignment.start,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textSecondary,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
labelStyle: const TextStyle( labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 12), fontWeight: FontWeight.bold, fontSize: 13),
tabs: const [ tabs: const [
Tab( Tab(
icon: Icon(Icons.analytics_outlined, size: 16), icon: Icon(Icons.analytics_outlined, size: 18),
text: 'FUNDAMENTALS'), text: 'ÜBERSICHT'),
Tab( Tab(
icon: Icon(Icons.architecture_outlined, size: 16), icon: Icon(Icons.architecture_outlined, size: 18),
text: 'MUSTER & SIGNALE'), text: 'MUSTER'),
Tab( Tab(
icon: Icon(Icons.candlestick_chart_outlined, size: 16), icon:
Icon(Icons.candlestick_chart_outlined, size: 18),
text: 'TRADES'), text: 'TRADES'),
], ],
), ),
@@ -171,6 +202,7 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
), ),
),
); );
} }
} }
@@ -33,6 +33,7 @@ class AssetHeroHeader extends StatelessWidget {
return BlocBuilder<AssetFundamentalsBloc, AssetFundamentalsState>( return BlocBuilder<AssetFundamentalsBloc, AssetFundamentalsState>(
builder: (context, fundState) { builder: (context, fundState) {
String displayName = name; String displayName = name;
String primaryTicker = '';
final String? logoUrl = isin.isNotEmpty ? '/api/v1/logo/$isin' : null; final String? logoUrl = isin.isNotEmpty ? '/api/v1/logo/$isin' : null;
List<TickerModel> tickerOptions = [ List<TickerModel> tickerOptions = [
TickerModel(ticker: symbol ?? 'Loading', exchange: 'Loading', tradingCurrency: '', currentPrice: null) TickerModel(ticker: symbol ?? 'Loading', exchange: 'Loading', tradingCurrency: '', currentPrice: null)
@@ -43,14 +44,21 @@ class AssetHeroHeader extends StatelessWidget {
if (data.companyName.isNotEmpty) { if (data.companyName.isNotEmpty) {
displayName = data.companyName; displayName = data.companyName;
} }
if (data.primaryTicker.isNotEmpty) {
primaryTicker = data.primaryTicker;
}
if (data.availableTickers.isNotEmpty) { if (data.availableTickers.isNotEmpty) {
tickerOptions = data.availableTickers; tickerOptions = data.availableTickers;
} }
} }
final selectedOption = tickerOptions.firstWhere( final selectedOption = tickerOptions.firstWhere(
(t) => t.ticker == symbol || t.exchange == symbol, (t) => (symbol != null && symbol!.isNotEmpty) &&
(t.ticker.toLowerCase() == symbol!.toLowerCase() || (t.exchange != null && t.exchange!.toLowerCase() == symbol!.toLowerCase())),
orElse: () => tickerOptions.firstWhere(
(t) => primaryTicker.isNotEmpty && t.ticker.toLowerCase() == primaryTicker.toLowerCase(),
orElse: () => tickerOptions.first, orElse: () => tickerOptions.first,
),
); );
return Container( return Container(
@@ -203,10 +211,10 @@ class AssetHeroHeader extends StatelessWidget {
); );
}, },
), ),
// Interactive Ticker & Exchange Selector Dropdown
PopupMenuButton<String>( PopupMenuButton<String>(
initialValue: selectedOption.ticker, initialValue: selectedOption.ticker,
tooltip: 'Select Exchange & Ticker', tooltip: 'Börsenplatz & Ticker auswählen',
color: theme.cardSurface,
onSelected: (newTicker) { onSelected: (newTicker) {
if (onExchangeChanged != null) { if (onExchangeChanged != null) {
final opt = tickerOptions.firstWhere( final opt = tickerOptions.firstWhere(
@@ -220,27 +228,57 @@ class AssetHeroHeader extends StatelessWidget {
return tickerOptions.map((opt) { return tickerOptions.map((opt) {
final ex = opt.exchange ?? 'Unknown'; final ex = opt.exchange ?? 'Unknown';
final tick = opt.ticker; final tick = opt.ticker;
final label = '$tick ($ex)'; final isPrimary = primaryTicker.isNotEmpty &&
(tick.toLowerCase() == primaryTicker.toLowerCase());
final isSelected = tick == symbol || ex == symbol; final isSelected = tick == symbol || ex == symbol;
return PopupMenuItem<String>( return PopupMenuItem<String>(
value: tick, value: tick,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2),
child: Row( child: Row(
children: [ children: [
Icon( Icon(
Icons.business, isPrimary ? Icons.star_rounded : Icons.business,
size: 16, size: 18,
color: isSelected ? theme.primaryColor : theme.textMuted, color: isPrimary
? AppTheme.accentCyan
: (isSelected ? theme.primaryColor : theme.textMuted),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Expanded(
label, child: Text(
'$tick ($ex)',
style: TextStyle( style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: (isSelected || isPrimary) ? FontWeight.bold : FontWeight.normal,
color: isSelected ? theme.primaryColor : theme.textPrimary, color: isSelected
? theme.primaryColor
: (isPrimary ? Colors.white : theme.textPrimary),
),
),
),
if (isPrimary) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.accentCyan.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.accentCyan.withValues(alpha: 0.5)),
),
child: Text(
'PRIMARY',
style: TextStyle(
color: AppTheme.accentCyan,
fontSize: 9,
fontWeight: FontWeight.w900,
letterSpacing: 0.6,
),
), ),
), ),
], ],
],
),
), ),
); );
}).toList(); }).toList();
@@ -248,24 +286,46 @@ class AssetHeroHeader extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.accentColor.withValues(alpha: 0.15), color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan.withValues(alpha: 0.15)
: theme.accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: theme.accentColor.withValues(alpha: 0.4)), border: Border.all(
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan.withValues(alpha: 0.5)
: theme.accentColor.withValues(alpha: 0.4),
),
), ),
child: Row( child: Row(
children: [ children: [
Icon(Icons.business, size: 14, color: theme.accentColor), Icon(
(primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? Icons.star_rounded
: Icons.business,
size: 15,
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan
: theme.accentColor,
),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
'${selectedOption.ticker} (${selectedOption.exchange ?? 'Unknown'})', '${selectedOption.ticker} (${selectedOption.exchange ?? 'Unknown'})',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.accentColor, color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan
: theme.accentColor,
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Icon(Icons.arrow_drop_down, size: 16, color: theme.accentColor), Icon(
Icons.arrow_drop_down,
size: 16,
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan
: theme.accentColor,
),
], ],
), ),
), ),
@@ -77,6 +77,8 @@ public class FundamentalsDbContext : DbContext
modelBuilder.Entity<KeyExecutiveEntity>(entity => modelBuilder.Entity<KeyExecutiveEntity>(entity =>
{ {
entity.HasKey(e => e.Id); entity.HasKey(e => e.Id);
entity.Property(e => e.SortOrder).HasDefaultValue(0);
entity.HasIndex(e => new { e.AssetDataIsin, e.SortOrder });
}); });
modelBuilder.Entity<AssetEventEntity>(entity => modelBuilder.Entity<AssetEventEntity>(entity =>
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace FinlyticFundamentals.Entities; namespace FinlyticFundamentals.Entities;
@@ -10,6 +10,7 @@ public class KeyExecutiveEntity
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
public string Payment { 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; [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() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("AssetDataIsin"); b.HasIndex("AssetDataIsin", "SortOrder");
b.ToTable("KeyExecutives"); b.ToTable("KeyExecutives");
}); });
@@ -129,92 +129,85 @@ public class FundamentalsDbService : IFundamentalsDbService
"[DEBUG-TR-ERROR] Could not fetch Trade Republic details for {Isin}", cleanIsin); "[DEBUG-TR-ERROR] Could not fetch Trade Republic details for {Isin}", cleanIsin);
} }
// --- STEP 2: Ticker auflösen (Null-safe) --- // --- STEP 2: Ticker auflösen (Der Primary Ticker ist IMMER der 1. von Yahoo Finance) ---
TickerInfoDto primaryTicker; 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(requestedTicker)) if (string.IsNullOrWhiteSpace(yahooPrimaryTicker.Exchange))
{ {
var match = assetData?.AvailableTickers? yahooPrimaryTicker = new TickerInfoDto
.FirstOrDefault(a => string.Equals(a.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase));
if (match != null)
{ {
primaryTicker = new TickerInfoDto Ticker = yahooPrimaryTicker.Ticker,
{ Exchange = GetExchangeDisplayName(yahooPrimaryTicker.Ticker)
Ticker = match.Ticker,
Exchange = !string.IsNullOrWhiteSpace(match.Exchange)
? match.Exchange
: GetExchangeDisplayName(match.Ticker)
}; };
} }
else if (assetData?.PrimaryTicker != null &&
string.Equals(assetData.PrimaryTicker.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase)) // 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))
{ {
primaryTicker = new TickerInfoDto 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 (matchDto != null)
{ {
Ticker = assetData.PrimaryTicker.Ticker, activeQueryTicker = new TickerInfoDto
Exchange = !string.IsNullOrWhiteSpace(assetData.PrimaryTicker.Exchange) {
? assetData.PrimaryTicker.Exchange Ticker = matchDto.Ticker,
: GetExchangeDisplayName(assetData.PrimaryTicker.Ticker) Exchange = !string.IsNullOrWhiteSpace(matchDto.Exchange) ? matchDto.Exchange : GetExchangeDisplayName(matchDto.Ticker)
};
}
else if (matchEntity != null)
{
activeQueryTicker = new TickerInfoDto
{
Ticker = matchEntity.Ticker,
Exchange = !string.IsNullOrWhiteSpace(matchEntity.Exchange) ? matchEntity.Exchange : GetExchangeDisplayName(matchEntity.Ticker)
}; };
} }
else else
{ {
primaryTicker = new TickerInfoDto activeQueryTicker = new TickerInfoDto
{ {
Ticker = requestedTicker, Ticker = requestedTicker,
Exchange = GetExchangeDisplayName(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 else
{ {
var resolved = await _scraper.ResolveTickerFromIsinAsync(cleanIsin, cancellationToken); activeQueryTicker = yahooPrimaryTicker;
primaryTicker = resolved != null && !string.IsNullOrWhiteSpace(resolved.Ticker)
? resolved
: new TickerInfoDto
{
Ticker = cleanIsin,
Exchange = "Unknown"
};
} }
if (string.IsNullOrWhiteSpace(primaryTicker.Exchange)) if (string.IsNullOrWhiteSpace(activeQueryTicker.Exchange))
{ {
primaryTicker = new TickerInfoDto activeQueryTicker = new TickerInfoDto
{ {
Ticker = primaryTicker.Ticker, Ticker = activeQueryTicker.Ticker,
Exchange = GetExchangeDisplayName(primaryTicker.Ticker) Exchange = GetExchangeDisplayName(activeQueryTicker.Ticker)
}; };
} }
await _finlyticLogger.LogInfoAsync(SettingKeys.FundamentalsChannel, await _finlyticLogger.LogInfoAsync(SettingKeys.FundamentalsChannel,
"[DEBUG-TICKER-RESOLVED] Ticker aufgelöst zu: '{Ticker}' (Exchange: '{Exchange}') für ISIN {Isin}", "[DEBUG-TICKER-RESOLVED] PrimaryTicker: '{Primary}' | ActiveQueryTicker: '{Active}' für ISIN {Isin}",
primaryTicker.Ticker, primaryTicker.Exchange ?? "Unknown", cleanIsin); yahooPrimaryTicker.Ticker, activeQueryTicker.Ticker, cleanIsin);
// --- STEP 3 & 4: Yahoo Finance API & HTML Fallback über Scraper --- // --- STEP 3 & 4: Yahoo Finance API & HTML Fallback über Scraper ---
YahooQuoteSummaryModulesDto? modulesDto = null; YahooQuoteSummaryModulesDto? modulesDto = null;
if (!string.IsNullOrWhiteSpace(primaryTicker.Ticker) && primaryTicker.Ticker != cleanIsin) if (!string.IsNullOrWhiteSpace(activeQueryTicker.Ticker) && activeQueryTicker.Ticker != cleanIsin)
{ {
modulesDto = await _scraper.GetQuoteSummaryModulesAsync( modulesDto = await _scraper.GetQuoteSummaryModulesAsync(
primaryTicker.Ticker, activeQueryTicker.Ticker,
forceHtmlScrape: false, forceHtmlScrape: false,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
} }
else else
{ {
await _finlyticLogger.LogWarningAsync(SettingKeys.FundamentalsChannel, 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 --- // --- Update AssetDataEntity ---
@@ -227,8 +220,8 @@ public class FundamentalsDbService : IFundamentalsDbService
Isin = cleanIsin, Isin = cleanIsin,
PrimaryTicker = new TickerEntity PrimaryTicker = new TickerEntity
{ {
Ticker = primaryTicker.Ticker, Ticker = yahooPrimaryTicker.Ticker,
Exchange = primaryTicker.Exchange ?? "Unknown" Exchange = yahooPrimaryTicker.Exchange ?? "Unknown"
}, },
KeyExecutives = new List<KeyExecutiveEntity>(), KeyExecutives = new List<KeyExecutiveEntity>(),
AssetEvents = new List<AssetEventEntity>() AssetEvents = new List<AssetEventEntity>()
@@ -241,27 +234,27 @@ public class FundamentalsDbService : IFundamentalsDbService
string fallbackName = modulesDto?.QuoteType?.ShortName string fallbackName = modulesDto?.QuoteType?.ShortName
?? modulesDto?.QuoteType?.LongName ?? modulesDto?.QuoteType?.LongName
?? primaryTicker.Ticker; ?? activeQueryTicker.Ticker;
assetData.Name = !string.IsNullOrWhiteSpace(trName) ? trName : fallbackName; assetData.Name = !string.IsNullOrWhiteSpace(trName) ? trName : fallbackName;
assetData.Description = !string.IsNullOrWhiteSpace(trDescription) assetData.Description = !string.IsNullOrWhiteSpace(trDescription)
? trDescription ? trDescription
: (modulesDto?.AssetProfile?.LongBusinessSummary ?? string.Empty); : (modulesDto?.AssetProfile?.LongBusinessSummary ?? string.Empty);
// PrimaryTicker ist FEST der erste von Yahoo Finance
assetData.PrimaryTicker = new TickerEntity assetData.PrimaryTicker = new TickerEntity
{ {
Ticker = primaryTicker.Ticker, Ticker = yahooPrimaryTicker.Ticker,
Exchange = primaryTicker.Exchange ?? "Unknown" Exchange = yahooPrimaryTicker.Exchange ?? "Unknown"
}; };
var tickers = await _scraper.ResolveAllTickersFromIsinAsync(cleanIsin, cancellationToken); if (!resolvedTickers.Any(t => string.Equals(t.Ticker, yahooPrimaryTicker.Ticker, StringComparison.OrdinalIgnoreCase)))
if (!tickers.Any(t => string.Equals(t.Ticker, primaryTicker.Ticker, StringComparison.OrdinalIgnoreCase)))
{ {
tickers.Insert(0, primaryTicker); resolvedTickers.Insert(0, yahooPrimaryTicker);
} }
assetData.AvailableTickers.Clear(); assetData.AvailableTickers.Clear();
foreach (var a in tickers) foreach (var a in resolvedTickers)
{ {
assetData.AvailableTickers.Add(new TickerEntity assetData.AvailableTickers.Add(new TickerEntity
{ {
@@ -278,7 +271,21 @@ public class FundamentalsDbService : IFundamentalsDbService
// --- Process Trade Republic Corporate Events --- // --- Process Trade Republic Corporate Events ---
if (trDetails != null && (shouldUpdateAssetData || effectiveForceRefresh) && assetData != null) 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>(); var trEventList = new List<TradeRepublicEventDto>();
if (trDetails.Events != null) trEventList.AddRange(trDetails.Events); if (trDetails.Events != null) trEventList.AddRange(trDetails.Events);
@@ -298,17 +305,19 @@ public class FundamentalsDbService : IFundamentalsDbService
if (!isDuplicate) if (!isDuplicate)
{ {
assetData.AssetEvents.Add(new AssetEventEntity var newEvent = new AssetEventEntity
{ {
AssetDataIsin = cleanIsin, AssetDataIsin = cleanIsin,
Ticker = new TickerEntity Ticker = new TickerEntity
{ {
Ticker = primaryTicker.Ticker, Ticker = yahooPrimaryTicker.Ticker,
Exchange = primaryTicker.Exchange ?? "Unknown" Exchange = yahooPrimaryTicker.Exchange ?? "Unknown"
}, },
Type = evtType, Type = evtType,
Date = evtDate 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 // 4. Neue Executives aufbauen und direkt über den DbSet hinzufügen
if (modulesDto.AssetProfile?.CompanyOfficers != null) if (modulesDto.AssetProfile?.CompanyOfficers != null)
{ {
int sortIdx = 0;
foreach (var officer in modulesDto.AssetProfile.CompanyOfficers) foreach (var officer in modulesDto.AssetProfile.CompanyOfficers)
{ {
if (!string.IsNullOrWhiteSpace(officer.Name)) if (!string.IsNullOrWhiteSpace(officer.Name))
@@ -349,7 +359,8 @@ public class FundamentalsDbService : IFundamentalsDbService
Name = officer.Name, Name = officer.Name,
Title = officer.Title ?? string.Empty, Title = officer.Title ?? string.Empty,
Payment = officer.TotalPay?.Fmt ?? Payment = officer.TotalPay?.Fmt ??
(officer.TotalPay?.Raw?.ToString() ?? string.Empty) (officer.TotalPay?.Raw?.ToString() ?? string.Empty),
SortOrder = sortIdx++
}; };
context.KeyExecutives.Add(newExec); context.KeyExecutives.Add(newExec);
assetData.KeyExecutives.Add(newExec); assetData.KeyExecutives.Add(newExec);
@@ -377,8 +388,8 @@ public class FundamentalsDbService : IFundamentalsDbService
fundamentalData.Ticker = new TickerEntity fundamentalData.Ticker = new TickerEntity
{ {
Ticker = primaryTicker.Ticker, Ticker = activeQueryTicker.Ticker,
Exchange = primaryTicker.Exchange ?? "Unknown" Exchange = activeQueryTicker.Exchange ?? "Unknown"
}; };
fundamentalData.MarketCap = (decimal?)modulesDto.SummaryDetail?.MarketCap?.Raw; fundamentalData.MarketCap = (decimal?)modulesDto.SummaryDetail?.MarketCap?.Raw;
fundamentalData.EnterpriseValue = fundamentalData.EnterpriseValue =
@@ -452,7 +463,10 @@ public class FundamentalsDbService : IFundamentalsDbService
if (assetData == null) return null; 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>(); var eventsList = assetData.AssetEvents?.ToList() ?? new List<AssetEventEntity>();
return MapToDto(assetData, fundamentalData, executivesList, eventsList); return MapToDto(assetData, fundamentalData, executivesList, eventsList);
@@ -610,12 +624,16 @@ public class FundamentalsDbService : IFundamentalsDbService
LastUpdatedUtc = fundData.LastUpdatedUtc LastUpdatedUtc = fundData.LastUpdatedUtc
} }
: null, : 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, Id = e.Id,
Name = e.Name, Name = e.Name,
Title = e.Title, Title = e.Title,
Payment = e.Payment Payment = e.Payment,
SortOrder = e.SortOrder
}).ToList(), }).ToList(),
Events = events.Select(e => new CorporateEventDto Events = events.Select(e => new CorporateEventDto
{ {
@@ -666,4 +684,21 @@ public class FundamentalsDbService : IFundamentalsDbService
return "Other"; 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)) foreach (var q in validQuotes.Skip(1))
{ {
if (!symbols.Any(s => s.symbol.Equals(q.Symbol, StringComparison.OrdinalIgnoreCase))) 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)))); symbols.Add((q.Symbol, q.Exchange ?? string.Empty, Math.Max(1, GetExchangePriority(q.Symbol, cleanIsin))));