20250526 - cywongg/2025 GitHub Wiki

Below is one way to “translate” your Python‐logic into an implementation of the IPdiLoader interface. In this example, we build a loader class called `DipsReportPdiLoader` that:

1. Implements the `IPdiLoader` interface (which simply returns a collection of `ICorporateAction` objects representing dividend adjustments).  
2. Uses a “translated” version of your Python code to:  
   – Retrieve dividend records from the database (via `DividendService`),  
   – Group them by ex‑date and underlying instrument,  
   – Retrieve basket data (via `BasketService`), FX data (via `ICOMAdapter` and a helper DataAccess call), and country settings (via `ConfigHelper`), and  
   – Process each basket (its components and the associated dividends) to compute “dips” (adjustments) for each day.  
3. For each basket and each dividend day, a new composite corporate action is built (here in a simple form using our `CompositeCorporateAction` class) whose properties—such as `DivGross` and `DivNet`—contain the computed short and long dips.

In a production solution you’d likely break these classes into separate files and use proper dependency injection and logging. For clarity, the sample below is self‑contained and uses synchronous calls (by “blocking” on tasks) for the IPdiLoader method. You may later choose to make the IPdiLoader asynchronous.

The code sample assumes that many of the domain classes (such as `Dividend`, `Basket`, `BasketDayDip`, `CountrySetting`, etc.) already exist in your project (as in your Python translation). For completeness, minimal stub definitions are provided.

---

```csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace FactorMonitor.DataAcquisition.CorporateAction
{
    // ============================================================
    // DOMAIN INTERFACES & BASE CLASSES (STUBS)
    // ============================================================
    
    public interface ICorporateAction
    {
        string UdlEliotCode { get; }
        DateTime ExerciseDate { get; }
        double DivGross { get; set; }
        double DivNet { get; set; }
        double? REactor { get; set; }
        string DivStatus { get; set; }
        string EFactorStatus { get; set; }
    }

    public abstract class CorporateActionBase : ICorporateAction
    {
        public virtual string UdlEliotCode { get; protected set; }
        public virtual DateTime ExerciseDate { get; protected set; }
        public virtual double DivGross { get; set; }
        public virtual double DivNet { get; set; }
        public virtual double? REactor { get; set; }
        public virtual string DivStatus { get; set; }
        public virtual string EFactorStatus { get; set; }

        public virtual ICorporateAction Aggregate(IEnumerable<ICorporateAction> actions)
        {
            double totalGross = actions.Sum(a => a.DivGross);
            double totalNet = actions.Sum(a => a.DivNet);
            var clone = (CorporateActionBase)this.MemberwiseClone();
            clone.DivGross = totalGross;
            clone.DivNet = totalNet;
            return clone;
        }
    }

    // A simple composite corporate action; you can extend it as needed.
    public class CompositeCorporateAction : CorporateActionBase
    {
        // Additional properties for upload status or computed results can be added here.
    }

    // ============================================================
    // IPdiLoader INTERFACE
    // ============================================================
    
    public interface IPdiLoader
    {
        /// <summary>
        /// Retrieves a collection of dividend (PDI) corporate actions for the specified date range and indexes.
        /// </summary>
        /// <param name="from">Start date.</param>
        /// <param name="to">End date.</param>
        /// <param name="prIndexes">A collection of basket unique names (or indexes) to process.</param>
        /// <returns>A collection of computed corporate actions (dividend adjustments) for each basket day.</returns>
        Collection<ICorporateAction> GetPdis(DateTime from, DateTime to, ICollection<string> prIndexes);
    }

    // ============================================================
    // STUB DOMAIN CLASSES (from your Python translation)
    // ============================================================
    
    public class Dividend
    {
        public decimal DividendGrossAmount { get; set; }
        public decimal DividendNetAmount { get; set; }
        public DateTime CorporateActionExDate { get; set; }
        public string UniqueName { get; set; }
        public string DividendCurrency { get; set; }
        public string MICCode { get; set; }
        public string Segment { get; set; }
        public string DividendType { get; set; }
    }

    public class CurrencyPair
    {
        public string SourceTarget { get; set; }
        public string ListingId { get; set; }
        public string CurrencyISOCode { get; set; }
    }

    public class FXSnap
    {
        public string InstrumentId { get; set; }
        public decimal Value { get; set; }
    }

    public class Component
    {
        public string UniqueName { get; set; }
        public decimal Unit { get; set; }
        public string ISIN { get; set; }
    }

    public class Composition
    {
        public DateTime ValidFrom { get; set; }
        public DateTime ValidTo { get; set; }
        public decimal Divisor { get; set; }
    }

    public class Basket
    {
        public string BasketUniqueName { get; set; }
        public string Type { get; set; } // "TR" or "Price"
        public string BasketCurrency { get; set; }
        public Composition Pcf { get; set; }
        public Composition Ecf { get; set; }
        public Composition Acf { get; set; }
        public List<Component> Composition { get; set; } = new List<Component>();
        public Dictionary<DateTime, BasketDayDip> DaysDips { get; set; } = new Dictionary<DateTime, BasketDayDip>();
    }

    public class BasketDayDip
    {
        public decimal ShortDip { get; set; }
        public decimal LongDip { get; set; }
    }

    public class CountrySetting
    {
        public decimal Receive { get; set; }
        public decimal Reinvest { get; set; }
    }

    // ============================================================
    // DATA ACCESS & HELPER CLASSES (simplified versions)
    // ============================================================
    
    public class DataAccess
    {
        private readonly string _connectionString;
        public DataAccess()
        {
            _connectionString = ConfigurationManager.AppSettings["MyDbConnection"];
        }
        public async Task<List<T>> RunQueryAsync<T>(string sql, Func<IDataRecord, T> map)
        {
            var results = new List<T>();
            using (SqlConnection conn = new SqlConnection(_connectionString))
            using (SqlCommand cmd = new SqlCommand(sql, conn))
            {
                await conn.OpenAsync();
                using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        results.Add(map(reader));
                    }
                }
            }
            return results;
        }
    }

    public class DividendService
    {
        private readonly DataAccess _dataAccess;
        public DividendService()
        {
            _dataAccess = new DataAccess();
        }
        public async Task<List<Dividend>> GetDividendsAsync(DateTime reportFrom, DateTime reportTo)
        {
            string sql = string.Format(@"
                SELECT DISTINCT
                    li.UniqueName,
                    cdd.ExDate AS CorporateActionExDate,
                    cdd.CurrencyISOCode AS DividendCurrency,
                    cdd.GrossAmount,
                    cdd.NetAmount,
                    m.MIC AS MICCode,
                    m.Segment,
                    cdd.DividendType
                FROM CashDividendDetail cdd
                INNER JOIN Listing li ON li.Id = cdd.ListingId
                INNER JOIN Market m ON m.Id = li.MarketId
                WHERE cdd.ExDate >= '{0}'
                  AND cdd.ExDate < '{1}'
                  AND LOWER(cdd.DividendType) NOT IN ('omitted','discontinued','return capital','cancelled')
                  AND cdd.IsPaidInPercentage = 0",
                reportFrom.ToString("yyyy-MM-dd"), reportTo.ToString("yyyy-MM-dd"));

            List<Dividend> dividends = await _dataAccess.RunQueryAsync(sql, record => new Dividend
            {
                UniqueName = record["UniqueName"].ToString(),
                CorporateActionExDate = Convert.ToDateTime(record["CorporateActionExDate"]),
                DividendCurrency = record["DividendCurrency"].ToString(),
                DividendGrossAmount = Convert.ToDecimal(record["GrossAmount"]),
                DividendNetAmount = Convert.ToDecimal(record["NetAmount"]),
                MICCode = record["MICCode"].ToString(),
                Segment = record["Segment"].ToString(),
                DividendType = record["DividendType"].ToString()
            });
            return dividends;
        }
    }

    public class BasketService
    {
        private readonly DataAccess _dataAccess;
        public BasketService()
        {
            _dataAccess = new DataAccess();
        }
        public async Task<Dictionary<string, Basket>> GetBasketDataAsync(List<string> basketUniqueNames)
        {
            string inClause = string.Join(",", basketUniqueNames.Select(name => $"'{name}'"));
            string sql = $@"
                SELECT 
                    rec.BasketUniqueName,
                    rec.ValidFrom,
                    rec.ValidTo,
                    rec.Divisor,
                    bli.CurrencyISOCode AS BasketCurrency,
                    wli.UniqueName AS ComponentUniqueName,
                    wsi.ISIN AS ComponentISIN,
                    w.Unit AS ComponentUnit
                FROM RecentBasketChange rec
                INNER JOIN Listing bli ON bli.InstrumentId = rec.BasketUniqueName
                INNER JOIN Weight w ON w.BasketUniqueName = rec.BasketUniqueName
                INNER JOIN Listing wli ON wli.Id = w.ListingId
                INNER JOIN SecurityInstrument wsi ON wsi.Id = w.ComponentUnit
                WHERE rec.BasketUniqueName IN ({inClause})
                ORDER BY rec.BasketUniqueName";

            List<dynamic> rawData = await _dataAccess.RunQueryAsync(sql, record => new
            {
                BasketUniqueName = record["BasketUniqueName"].ToString(),
                ValidFrom = Convert.ToDateTime(record["ValidFrom"]),
                ValidTo = Convert.ToDateTime(record["ValidTo"]),
                Divisor = Convert.ToDecimal(record["Divisor"]),
                BasketCurrency = record["BasketCurrency"].ToString(),
                ComponentUniqueName = record["ComponentUniqueName"].ToString(),
                ComponentISIN = record["ComponentISIN"].ToString(),
                ComponentUnit = Convert.ToDecimal(record["ComponentUnit"])
            });

            Dictionary<string, Basket> baskets = new Dictionary<string, Basket>();
            foreach (var row in rawData)
            {
                if (!baskets.ContainsKey(row.BasketUniqueName))
                {
                    baskets[row.BasketUniqueName] = new Basket
                    {
                        BasketUniqueName = row.BasketUniqueName,
                        Type = "Price",  // Assume type from config or default.
                        BasketCurrency = row.BasketCurrency,
                        Pcf = new Composition { ValidFrom = row.ValidFrom, ValidTo = row.ValidTo, Divisor = row.Divisor },
                        Ecf = new Composition { ValidFrom = row.ValidFrom, ValidTo = row.ValidTo, Divisor = row.Divisor },
                        Acf = new Composition { ValidFrom = row.ValidFrom, ValidTo = row.ValidTo, Divisor = row.Divisor }
                    };
                }
                baskets[row.BasketUniqueName].Composition.Add(new Component
                {
                    UniqueName = row.ComponentUniqueName,
                    ISIN = row.ComponentISIN,
                    Unit = row.ComponentUnit
                });
            }
            return baskets;
        }
    }

    public static class ConfigHelper
    {
        public static string GetSetting(string key)
        {
            return ConfigurationManager.AppSettings[key] ?? string.Empty;
        }

        public static Dictionary<string, CountrySetting> GetCountrySettings(string key)
        {
            string json = GetSetting(key);
            if (!string.IsNullOrWhiteSpace(json))
            {
                return JsonConvert.DeserializeObject<Dictionary<string, CountrySetting>>(json);
            }
            // Default country settings.
            return new Dictionary<string, CountrySetting>
            {
                { "US", new CountrySetting { Receive = 0.85m, Reinvest = 0.70m } },
                { "HK", new CountrySetting { Receive = 0.90m, Reinvest = 1.00m } },
                { "SG", new CountrySetting { Receive = 1.00m, Reinvest = 1.00m } }
            };
        }
    }

    public class ICOMAdapter
    {
        private readonly string _baseUrl;
        private readonly HttpClient _httpClient;
        public ICOMAdapter()
        {
            _baseUrl = ConfigurationManager.AppSettings["ICOMRestUrl"] ?? "https://abc-rest.de.world.abc:10443";
            _httpClient = new HttpClient { BaseAddress = new Uri(_baseUrl) };
        }
        public async Task<List<FXSnap>> GetRefDataByListAsync(List<string> instrumentIds, List<string> snapTypes)
        {
            string path = "/reference-data/fbi-unique-name";
            var payload = instrumentIds.Select((id, index) => new { instrumentId = id, type = snapTypes.ElementAtOrDefault(index) }).ToList();
            string jsonPayload = JsonConvert.SerializeObject(payload);
            HttpResponseMessage response = await _httpClient.PostAsync(path,
                new StringContent(jsonPayload, Encoding.UTF8, "application/json"));
            response.EnsureSuccessStatusCode();
            string responseContent = await response.Content.ReadAsStringAsync();
            List<FXSnap> fxSnaps = JsonConvert.DeserializeObject<List<FXSnap>>(responseContent);
            return fxSnaps;
        }
    }
    
    // ============================================================
    // DIPS REPORT PDI LOADER
    // ============================================================
    
    /// <summary>
    /// This class implements the IPdiLoader interface using the full dividend and basket processing
    /// logic translated from Python. It retrieves dividend records, basket data, FX snapshots, and country
    /// settings; then it computes “dips” (dividend adjustments) for each basket on each dividend day.
    /// Finally, it builds a composite corporate action (using CompositeCorporateAction) per basket per day.
    /// </summary>
    public class DipsReportPdiLoader : IPdiLoader
    {
        public Collection<ICorporateAction> GetPdis(DateTime from, DateTime to, ICollection<string> prIndexes)
        {
            // Block on the async processing method (in production, consider making IPdiLoader async).
            return Task.Run(() => ProcessDipsAsync(from, to, prIndexes)).Result;
        }

        private async Task<Collection<ICorporateAction>> ProcessDipsAsync(DateTime from, DateTime to, ICollection<string> prIndexes)
        {
            // Step 1: Retrieve Dividends
            DividendService dividendService = new DividendService();
            List<Dividend> dividends = await dividendService.GetDividendsAsync(from, to);

            // Group dividends by ex–date and then by underlying instrument.
            Dictionary<DateTime, Dictionary<string, List<Dividend>>> dividendDays = new Dictionary<DateTime, Dictionary<string, List<Dividend>>>();
            foreach (var div in dividends)
            {
                DateTime day = div.CorporateActionExDate.Date;
                if (!dividendDays.ContainsKey(day))
                    dividendDays[day] = new Dictionary<string, List<Dividend>>();
                if (!dividendDays[day].ContainsKey(div.UniqueName))
                    dividendDays[day][div.UniqueName] = new List<Dividend>();
                dividendDays[day][div.UniqueName].Add(div);
            }

            // Step 2: Retrieve Basket Data for the given basket unique names
            BasketService basketService = new BasketService();
            Dictionary<string, Basket> baskets = await basketService.GetBasketDataAsync(prIndexes.ToList());

            // Step 3: Retrieve Country Settings
            Dictionary<string, CountrySetting> countrySettings = ConfigHelper.GetCountrySettings("reporter.reports.DipsReport.countries");

            // Step 4: Retrieve FX Data using currency pair SQL query
            string currencyPairSql = @"
                SELECT 
                    cp.SourceCurrencyISOCode + cp.TargetCurrencyISOCode AS SourceTarget,
                    'FBILST' + RIGHT('0000000000' + CAST(li.Id AS VARCHAR), 10) AS ListingId,
                    li.CurrencyISOCode
                FROM CurrencyPair cp
                INNER JOIN Listing li ON li.InstrumentId = cp.Id
                WHERE cp.SourceCurrencyISOCode = 'EUR'
                  AND li.StatusId = 3";
            DataAccess dataAccess = new DataAccess();
            List<CurrencyPair> currencyPairs = await dataAccess.RunQueryAsync(currencyPairSql, record => new CurrencyPair
            {
                SourceTarget = record["SourceTarget"].ToString(),
                ListingId = record["ListingId"].ToString(),
                CurrencyISOCode = record["CurrencyISOCode"].ToString()
            });
            List<string> listingIds = currencyPairs.Select(cp => cp.ListingId).Distinct().ToList();
            List<string> snapTypes = new List<string>(); // Assume empty or defined as needed.
            ICOMAdapter icomAdapter = new ICOMAdapter();
            List<FXSnap> fxSnaps = await icomAdapter.GetRefDataByListAsync(listingIds, snapTypes);
            Dictionary<string, decimal> fxSnapsDict = fxSnaps.ToDictionary(snap => snap.InstrumentId, snap => snap.Value);

            // Step 5: Process each basket to calculate dividend dips
            List<ICorporateAction> computedPdis = new List<ICorporateAction>();
            bool force = false;
            List<string> basketsNotValid = new List<string>();

            foreach (var basketEntry in baskets)
            {
                Basket basket = basketEntry.Value;

                // Check basket validity (using today's date; adjust as needed)
                if (basket.Pcf == null || basket.Ecf == null ||
                   (!(basket.Pcf.ValidFrom < DateTime.Today && DateTime.Today <= basket.Ecf.ValidTo) && !force))
                {
                    basketsNotValid.Add(basket.BasketUniqueName);
                    continue;
                }

                // Initialize dip container for each dividend day
                foreach (var day in dividendDays.Keys)
                {
                    if (!basket.DaysDips.ContainsKey(day))
                        basket.DaysDips[day] = new BasketDayDip { ShortDip = 0, LongDip = 0 };
                }

                // Determine the FX conversion factor (default is 1 for EUR)
                decimal euroToBasketCurrency = 1.0m;
                if (!basket.BasketCurrency.Equals("EUR", StringComparison.OrdinalIgnoreCase))
                {
                    string pairKey = "EUR" + basket.BasketCurrency;
                    CurrencyPair cp = currencyPairs.FirstOrDefault(c => c.SourceTarget == pairKey);
                    if (cp == null)
                        throw new KeyNotFoundException($"Currency pair {pairKey} does not exist.");
                    if (!fxSnapsDict.ContainsKey(cp.ListingId))
                        throw new KeyNotFoundException($"FX snap for listing id {cp.ListingId} not found.");
                    euroToBasketCurrency = fxSnapsDict[cp.ListingId];
                }

                // Process each component in the basket
                foreach (var comp in basket.Composition)
                {
                    // For each dividend day, if the component has dividend records...
                    foreach (var day in dividendDays.Keys)
                    {
                        if (dividendDays[day].ContainsKey(comp.UniqueName))
                        {
                            foreach (var div in dividendDays[day][comp.UniqueName])
                            {
                                decimal dividendAmount = 0.0m;
                                bool considerSpecialDivs = false; // You may set this via config.
                                if (div.DividendType != "specialcash" || considerSpecialDivs)
                                {
                                    // Example: choose gross dividend if available
                                    dividendAmount = div.DividendGrossAmount != 0 ? div.DividendGrossAmount : div.DividendGrossAmount;
                                }

                                // Get country factors using the first two letters of the component's ISIN.
                                string countryCode = comp.ISIN.Substring(0, 2).ToUpper();
                                CountrySetting cs;
                                if (!countrySettings.TryGetValue(countryCode, out cs))
                                {
                                    Console.Error.WriteLine($"Country '{countryCode}' wasn't found in config. Using default factors.");
                                    cs = new CountrySetting { Receive = 0.85m, Reinvest = 0.85m };
                                }
                                decimal receive = cs.Receive;
                                decimal reinvest = cs.Reinvest;

                                // Compute dips based on basket type — TR (Total Return) or Price index.
                                if (basket.Type.Equals("TR", StringComparison.OrdinalIgnoreCase))
                                {
                                    decimal shortDip = (dividendAmount / 1.0m) * (1 - receive) * comp.Unit;
                                    decimal longDip = (dividendAmount / 1.0m) * (receive - reinvest) * comp.Unit;
                                    basket.DaysDips[day].ShortDip += shortDip;
                                    basket.DaysDips[day].LongDip += longDip;
                                }
                                else if (basket.Type.Equals("Price", StringComparison.OrdinalIgnoreCase))
                                {
                                    decimal shortDip = (dividendAmount / 1.0m) * comp.Unit;
                                    decimal longDip = (dividendAmount / 1.0m) * (receive - reinvest) * comp.Unit;
                                    basket.DaysDips[day].ShortDip += shortDip;
                                    basket.DaysDips[day].LongDip += longDip;
                                }
                            }
                        }
                    }
                }

                // Final adjustment for each dividend day in the basket:
                foreach (var day in basket.DaysDips.Keys.ToList())
                {
                    basket.DaysDips[day].ShortDip = basket.DaysDips[day].ShortDip * euroToBasketCurrency / basket.Pcf.Divisor;
                    basket.DaysDips[day].LongDip = basket.DaysDips[day].LongDip * euroToBasketCurrency / basket.Ecf.Divisor;
                    
                    // Build a composite corporate action representing the computed dips.
                    CompositeCorporateAction ca = new CompositeCorporateAction
                    {
                        UdlEliotCode = basket.BasketUniqueName,
                        ExerciseDate = day,
                        DivGross = (double)basket.DaysDips[day].ShortDip,
                        DivNet = (double)basket.DaysDips[day].LongDip
                    };
                    computedPdis.Add(ca);
                }
            }
            return new Collection<ICorporateAction>(computedPdis);
        }
    }
}
```

---

### Explanation

1. **IPdiLoader Implementation:**  
   The `DipsReportPdiLoader` class implements `IPdiLoader` by exposing the synchronous `GetPdis(...)` method. Internally, it calls an asynchronous method (`ProcessDipsAsync`) that wraps the full dividend–and–basket processing pipeline.

2. **Translated Python Logic:**  
   The code in `ProcessDipsAsync` mirrors your Python translation:
   - It retrieves dividends from the database via `DividendService` and groups them by ex‑date and instrument.
   - It retrieves basket composition (index) data using `BasketService` and country settings via `ConfigHelper`.
   - FX conversion data are obtained using an SQL query (executed by `DataAccess`) and an HTTP call via `ICOMAdapter`.
   - For each basket and for each day with dividend data, the code loops over each underlying component, checks if dividend records exist, chooses dividend amounts (and even applies special type logic), and then computes the “short” and “long” dip adjustments based on whether the basket is a TR or Price index.
   - Finally, after applying FX conversion and basket divisor adjustments, a `CompositeCorporateAction` is created for each basket–day pair.

3. **Integration:**  
   Once you add this implementation to your system, you can inject or call it wherever an `IPdiLoader` is needed. This design avoids string comparisons for source-specific logic and encapsulates the full Python translation into one dedicated class.

Feel free to expand or adjust error handling, logging, configuration reading, or the mapping details as needed in your production system. Happy coding!



Below is an expanded, complete refactoring that not only removes all string‑based source comparisons (using polymorphism instead) but also incorporates the domain classes—and the inheritance issues—you mentioned. In this design:

1. We define a common domain interface (ICorporateAction) and an abstract base class (CorporateActionBase) that contains the common properties and a virtual Aggregate method.  
2. We include concrete implementations for each source (Alcor, QData, and FBI). For example, the Alcor implementation has two parts: one for standard corporate actions (AlcorCa) and one for dividend/PDI records (AlcorRdi), including the detailed JSON mapping and transformation logic (including computing dividend gross/net amounts much like your Python‑to‑C# code).  
3. Similar classes (e.g. QDataCa for qData and FbiDiv for FBI dividends) are provided.  
4. The ICorporateActionSource interface declares asynchronous methods (GetCorporateActionsAsync and GetDividendsAsync) so that each source can encapsulate its own retrieval and transformation logic.  
5. The CorporateActionService (which uses dependency‑injection–style constructor parameters) then works exclusively with ICorporateAction and ICorporateActionSource types, completely avoiding any string comparisons for source detection.  
6. We also include dummy (stub) implementations for the POI connectors (e.g. MainWindow, Instrument) and fee‐calculation helpers.

In a production project it’s best to split these classes into separate files and use a DI container. For clarity, here is the self‑contained refactored code (note that some details are simplified or stubbed):

---

```csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;

//
// DOMAIN MODELS
//
namespace FactorMonitor.DataModel.CorporateAction
{
    /// <summary>
    /// The common interface for all corporate actions.
    /// </summary>
    public interface ICorporateAction : INotifyPropertyChanged
    {
        string UdlEliotCode { get; }
        DateTime ExerciseDate { get; }
        double DivGross { get; set; }
        double DivNet { get; set; }
        double? REactor { get; set; }
        string DivStatus { get; set; }
        string EFactorStatus { get; set; }
        
        /// <summary>
        /// Aggregates a collection of corporate actions.
        /// </summary>
        ICorporateAction Aggregate(IEnumerable<ICorporateAction> actions);
    }

    /// <summary>
    /// Base implementation of ICorporateAction using property notifications.
    /// It provides a default Aggregate that sums DivGross and DivNet.
    /// </summary>
    public abstract class CorporateActionBase : ICorporateAction
    {
        private string _udlEliotCode;
        private DateTime _exerciseDate;
        private double _divGross;
        private double _divNet;
        private double? _rEactor;
        private string _divStatus;
        private string _eFactorStatus;

        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged(string propName) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));

        public virtual string UdlEliotCode
        {
            get => _udlEliotCode;
            protected set { _udlEliotCode = value; NotifyPropertyChanged(nameof(UdlEliotCode)); }
        }
        public virtual DateTime ExerciseDate
        {
            get => _exerciseDate;
            protected set { _exerciseDate = value; NotifyPropertyChanged(nameof(ExerciseDate)); }
        }
        public virtual double DivGross
        {
            get => _divGross;
            set { _divGross = value; NotifyPropertyChanged(nameof(DivGross)); }
        }
        public virtual double DivNet
        {
            get => _divNet;
            set { _divNet = value; NotifyPropertyChanged(nameof(DivNet)); }
        }
        public virtual double? REactor
        {
            get => _rEactor;
            set { _rEactor = value; NotifyPropertyChanged(nameof(REactor)); }
        }
        public virtual string DivStatus
        {
            get => _divStatus;
            set { _divStatus = value; NotifyPropertyChanged(nameof(DivStatus)); }
        }
        public virtual string EFactorStatus
        {
            get => _eFactorStatus;
            set { _eFactorStatus = value; NotifyPropertyChanged(nameof(EFactorStatus)); }
        }

        public virtual ICorporateAction Aggregate(IEnumerable<ICorporateAction> actions)
        {
            double totalGross = actions.Sum(a => a.DivGross);
            double totalNet = actions.Sum(a => a.DivNet);
            var aggregated = (CorporateActionBase)this.MemberwiseClone();
            aggregated.DivGross = totalGross;
            aggregated.DivNet = totalNet;
            return aggregated;
        }
    }

    /// <summary>
    /// A composite corporate action used frequently in the system.
    /// </summary>
    public class CompositeCorporateAction : CorporateActionBase
    {
        // Additional composite or upload properties can be added here.
    }
}

//
// DOMAIN MODELS WITH SOURCE-SPECIFIC MAPPING
//
namespace FactorMonitor.DataModel.CorporateAction.Connector.Alcor
{
    /// <summary>
    /// Maps the Alcor corporate action data.
    /// </summary>
    public class AlcorCa : FactorMonitor.DataModel.CorporateAction.CorporateActionBase
    {
        [JsonProperty("udlEliotCode")]
        public override string UdlEliotCode { get; set; }
        [JsonProperty("grossAmount")]
        public override double DivGross { get; set; }
        [JsonProperty("netAmount")]
        public override double DivNet { get; set; }
        [JsonProperty("exchangeAdjustmentFactor")]
        public override double? REactor { get; set; }
        [JsonProperty("exerciseDate")]
        public override DateTime ExerciseDate { get; set; }
        [JsonProperty("status")]
        public string Status { get; set; }
        [JsonProperty("currency")]
        public string DivCurrency { get; set; }
        [JsonProperty("udiCurrency")]
        public string UdlCurrency { get; set; }
    }

    /// <summary>
    /// Maps the Alcor dividend (PDI) data. Includes transformation logic similar to your Python‑to‑C# code.
    /// </summary>
    public class AlcorRdi : FactorMonitor.DataModel.CorporateAction.CorporateActionBase
    {
        [JsonProperty("indexCode")]
        public override string UdlEliotCode { get; set; }
        [JsonProperty("valueDate")]
        public override DateTime ExerciseDate { get; set; }
        [JsonProperty("amount")]
        public double? Factor { get; set; }
        [JsonProperty("details")]
        public List<PdiDetail> Details { get; set; }
        // Computed properties (not serialized)
        [JsonIgnore]
        public double PdiGross { get; set; }
        [JsonIgnore]
        public double PdiNet { get; set; }
        [JsonIgnore]
        public double ShivaPdiNet { get; set; }

        public AlcorRdi()
        {
            Details = new List<PdiDetail>();
        }

        /// <summary>
        /// Transforms the details into computed dividend values.
        /// </summary>
        public AlcorRdi ComputeRdi()
        {
            PdiGross = 0;
            PdiNet = 0;
            ShivaPdiNet = 0;
            foreach (var detail in Details)
            {
                PdiNet += detail.FUsed * detail.UdlQuantity *
                    (detail.DivAmount.NetAmountProvider - detail.DivAmount.ReinvestedAmountProvider);
                PdiGross += detail.FUsed * detail.UdlQuantity *
                    (detail.DivAmount.GrossAmount - detail.DivAmount.ReinvestedAmountProvider);
                ShivaPdiNet += detail.FUsed * detail.UdlQuantity *
                    (detail.DivAmount.ShivaNetAmount - detail.DivAmount.ReinvestedAmountProvider);
            }
            // In a real implementation, you might also incorporate FX conversion and dividend factors.
            this.DivGross = PdiGross;
            this.DivNet = PdiNet;
            return this;
        }
    }

    public class PdiDetail
    {
        [JsonProperty("fUsed")]
        public double FUsed { get; set; }
        [JsonProperty("udiQuantity")]
        public double UdlQuantity { get; set; }
        [JsonProperty("divAmount")]
        public PdiDivAmount DivAmount { get; set; }
    }

    public class PdiDivAmount
    {
        [JsonProperty("netAmountProvider")]
        public double NetAmountProvider { get; set; }
        [JsonProperty("reinvestedAmountProvider")]
        public double ReinvestedAmountProvider { get; set; }
        [JsonProperty("grossAmount")]
        public double GrossAmount { get; set; }
        [JsonProperty("shivaNetAmount")]
        public double ShivaNetAmount { get; set; }
    }
}

namespace FactorMonitor.DataModel.CorporateAction.Connector.qData
{
    /// <summary>
    /// Maps qData corporate action data.
    /// </summary>
    public class QDataCa : FactorMonitor.DataModel.CorporateAction.CorporateActionBase
    {
        [JsonProperty("code")]
        public override string UdlEliotCode { get; set; }
        [JsonProperty("exDate")]
        public override DateTime ExerciseDate { get; set; }
        [JsonProperty("grossAmount")]
        public override double DivGross { get; set; }
        [JsonProperty("netAmount")]
        public override double DivNet { get; set; }
    }
}

namespace FactorMonitor.DataModel.CorporateAction.Connector.Fbi
{
    /// <summary>
    /// Represents FBI dividend data (FbiDiv) for PDIs.
    /// </summary>
    public class FbiDiv : FactorMonitor.DataModel.CorporateAction.CorporateActionBase
    {
        // Additional FBI-specific properties can be defined here.
        public string SourceInfo { get; set; }
    }
}

//
// DATA ACCESS INTERFACE & DIVIDEND MODEL (for transformation)
//
namespace FactorMonitor.DataAcquisition.CorporateAction
{
    public interface IDataAccess
    {
        Task<List<Dividend>> GetDividendsAsync(DateTime from, DateTime to);
    }

    public class Dividend
    {
        public string UniqueName { get; set; }
        public DateTime CorporateActionExDate { get; set; }
        public decimal DividendGrossAmount { get; set; }
        public decimal DividendNetAmount { get; set; }
        public string DividendCurrency { get; set; }
    }
}

//
// SAMPLE SOURCE IMPLEMENTATION FOR ALCOR (including GetDividendsAsync)
//
namespace FactorMonitor.DataAcquisition.CorporateAction.Connector
{
    using FactorMonitor.DataModel.CorporateAction;
    using FactorMonitor.DataModel.CorporateAction.Connector.Alcor;
    
    /// <summary>
    /// Implements ICorporateActionSource for Alcor.
    /// </summary>
    public class AlcorCorporateActionSource : ICorporateActionSource
    {
        private readonly IDataAccess _dataAccess;
        public AlcorCorporateActionSource(IDataAccess dataAccess)
        {
            _dataAccess = dataAccess;
        }

        public async Task<IEnumerable<ICorporateAction>> GetCorporateActionsAsync(DateTime from, DateTime to, ICollection<string> codes)
        {
            // Implement corporate action retrieval for Alcor. For demonstration, return an empty list.
            return await Task.FromResult(new List<ICorporateAction>());
        }

        public async Task<IEnumerable<ICorporateAction>> GetDividendsAsync(DateTime from, DateTime to, ICollection<string> codes)
        {
            // Retrieve raw dividend data.
            List<Dividend> dividends = await _dataAccess.GetDividendsAsync(from, to);

            // Group dividends by ex-date.
            var groupedDividends = dividends.GroupBy(d => d.CorporateActionExDate);

            List<ICorporateAction> results = new List<ICorporateAction>();
            foreach (var group in groupedDividends)
            {
                // Create a composite corporate action representing this group.
                var composite = new CompositeCorporateAction
                {
                    UdlEliotCode = "ALCOR_DIV_" + group.Key.ToString("yyyyMMdd"),
                    ExerciseDate = group.Key,
                    DivGross = group.Sum(d => (double)d.DividendGrossAmount),
                    DivNet = group.Sum(d => (double)d.DividendNetAmount)
                };

                // In a more detailed implementation, you would apply FX conversion,
                // receive/reinvest factors, etc.
                results.Add(composite);
            }
            return results;
        }
    }
}

//
// (Similar implementations would be written for QData and FBI sources, e.g. FbiCorporateActionSource,
// which would transform raw FBI dividend data into FbiDiv objects.)
//

//
// THE REFACTORED CORPORATE ACTION SERVICE
//
namespace FactorMonitor.DataAcquisition.CorporateAction
{
    using FactorMonitor.DataModel.CorporateAction;
    using FactorMonitor.DataAcquisition.CorporateAction.Connector;
    using FactorMonitor.DataModel.CorporateAction.Connector.Alcor;
    using FactorMonitor.DataModel.CorporateAction.Connector.qData;
    using FactorMonitor.DataModel.CorporateAction.Connector.Fbi;

    /// <summary>
    /// The service retrieves and processes corporate actions and dividend data from Alcor, QData, and FBI.
    /// It uses polymorphism (via ICorporateActionSource) so that no string comparisons are needed.
    /// </summary>
    public interface ICorporateActionSource
    {
        Task<IEnumerable<ICorporateAction>> GetCorporateActionsAsync(DateTime from, DateTime to, ICollection<string> codes);
        Task<IEnumerable<ICorporateAction>> GetDividendsAsync(DateTime from, DateTime to, ICollection<string> codes);
    }

    public class CorporateActionService
    {
        private static readonly int divFeeType = 13;
        private static readonly int eFactorFeeType = 19;

        private readonly ObservableCollection<ICorporateAction> _pdis;
        private readonly ObservableCollection<ICorporateAction> _cas;
        private readonly ICorporateActionSource _alcorSource;
        private readonly ICorporateActionSource _qDataSource;
        private readonly ICorporateActionSource _fbiSource;
        private Dictionary<int, List<AsiaFactorDataSet>> _factorDataSetsByBasket;

        public CorporateActionService(
            ObservableCollection<ICorporateAction> pdis,
            ObservableCollection<ICorporateAction> cas,
            ICorporateActionSource alcorSource,
            ICorporateActionSource qDataSource,
            ICorporateActionSource fbiSource)
        {
            _pdis = pdis;
            _cas = cas;
            _alcorSource = alcorSource;
            _qDataSource = qDataSource;
            _fbiSource = fbiSource;
        }

        public async Task InitAsync(
            ICollection<string> prIndexes,
            ICollection<string> prStocks,
            DateTime from,
            DateTime to,
            Dictionary<int, List<AsiaFactorDataSet>> factorDataSetsByBasket)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            _factorDataSetsByBasket = factorDataSetsByBasket;

            var caSetupTask = SetupCasAsync(from, to, prStocks);
            var pdiSetupTask = SetupPdisAsync(from, to, prIndexes);
            await Task.WhenAll(caSetupTask, pdiSetupTask);

            watch.Stop();
            // (Log elapsed time if desired)
        }

        public void UpdateCorporateActionUploadStatus<T>(ICollection<T> corporateActions, int feeType)
            where T : ISelectedCorporateAction
        {
            System.Threading.Tasks.Parallel.ForEach(corporateActions, corporateAction =>
            {
                var basketInstrumentIds = MainWindow.GetInstrumentByUdlEliot(corporateAction.UdlEliotCode)
                                                     .Select(instr => instr.BasketInstrumentID)
                                                     .ToList();

                int updatedFees = FeeCalculator.GetUpdatedFees(_factorDataSetsByBasket, basketInstrumentIds, feeType);
                var actionToUpdate = GetCorporateActionToUpdate(corporateAction);
                UpdateCorporateActionStatus(basketInstrumentIds.Count, corporateAction, actionToUpdate, updatedFees);
            });
        }

        private ICorporateAction GetCorporateActionToUpdate(ISelectedCorporateAction corporateAction)
        {
            if (corporateAction is SelectedCa)
                return FindInPdisForCas(corporateAction) ?? FindInCas(corporateAction);
            else if (corporateAction is SelectedRfactor)
                return FindInCas(corporateAction);
            else
                return null;
        }

        private ICorporateAction FindInPdisForCas(ISelectedCorporateAction corporateAction)
        {
            return _pdis.FirstOrDefault(pdi =>
                pdi.UdlEliotCode.Equals(corporateAction.UdlEliotCode, StringComparison.OrdinalIgnoreCase) &&
                pdi.ExerciseDate.Equals(corporateAction.ExerciseDate))
                ?? FindInCas(corporateAction);
        }

        private ICorporateAction FindInCas(ISelectedCorporateAction corporateAction)
        {
            return _cas.FirstOrDefault(ca =>
                ca.UdlEliotCode.Equals(corporateAction.UdlEliotCode, StringComparison.OrdinalIgnoreCase) &&
                ca.ExerciseDate.Equals(corporateAction.ExerciseDate));
        }

        private void UpdateCorporateActionStatus(int basketInstrumentCount, ISelectedCorporateAction selectedCorporateAction,
            ICorporateAction corporateActionToUpdate, int updatedFees)
        {
            string status = $"{updatedFees}/{basketInstrumentCount}";
            if (selectedCorporateAction is SelectedCa)
                corporateActionToUpdate.DivStatus = status;
            else if (selectedCorporateAction is SelectedRfactor)
                corporateActionToUpdate.EFactorStatus = status;
        }

        private async Task SetupPdisAsync(DateTime from, DateTime to, ICollection<string> prIndexes)
        {
            var alcorDivsTask = _alcorSource.GetDividendsAsync(from, to, prIndexes);
            var qDataDivsTask = _qDataSource.GetDividendsAsync(from, to, prIndexes);
            var fbiDivsTask = _fbiSource.GetDividendsAsync(from, to, prIndexes);
            await Task.WhenAll(alcorDivsTask, qDataDivsTask, fbiDivsTask);

            var combinedPDIs = alcorDivsTask.Result
                                    .Concat(qDataDivsTask.Result)
                                    .Concat(fbiDivsTask.Result);
            foreach (var pdi in combinedPDIs)
            {
                _pdis.Add(pdi);
            }
        }

        private async Task SetupCasAsync(DateTime from, DateTime to, ICollection<string> prStocks)
        {
            var alcorCAsTask = _alcorSource.GetCorporateActionsAsync(from, to, prStocks);
            var qDataCAsTask = _qDataSource.GetCorporateActionsAsync(from, to, prStocks);
            await Task.WhenAll(alcorCAsTask, qDataCAsTask);

            var combinedCAs = alcorCAsTask.Result.Concat(qDataCAsTask.Result);
            foreach (var ca in combinedCAs)
            {
                _cas.Add(ca);
            }
        }

        public Dictionary<Tuple<string, DateTime>, ICorporateAction> ProcessCorporateActions(IEnumerable<ICorporateAction> actions)
        {
            var duplicateGroups = actions.GroupBy(ca => Tuple.Create(ca.UdlEliotCode, ca.ExerciseDate))
                                           .Where(g => g.Count() > 1)
                                           .ToDictionary(g => g.Key, g => g.ToList());
            if (duplicateGroups.Any())
            {
                var message = "Duplicate Corporate Actions detected for:\n" +
                              string.Join("\n", duplicateGroups.Select(g => $"{g.Key.Item1} on {g.Key.Item2:yyyy-MM-dd}"));
                System.Windows.MessageBox.Show(message, "Duplicate Corporate Actions");
            }

            var aggregatedActions = actions.GroupBy(ca => Tuple.Create(ca.UdlEliotCode, ca.ExerciseDate))
                                             .ToDictionary(g => g.Key, g => AggregateCorporateActions(g.ToList()));
            return aggregatedActions;
        }

        private ICorporateAction AggregateCorporateActions(List<ICorporateAction> actions)
        {
            if (actions.Count == 1)
                return actions.First();
            return ((CorporateActionBase)actions.First()).Aggregate(actions);
        }
    }

    public interface ISelectedCorporateAction
    {
         string UdlEliotCode { get; }
         DateTime ExerciseDate { get; }
    }

    public class SelectedCa : ISelectedCorporateAction
    {
         public string UdlEliotCode { get; set; }
         public DateTime ExerciseDate { get; set; }
    }

    public class SelectedRfactor : ISelectedCorporateAction
    {
         public string UdlEliotCode { get; set; }
         public DateTime ExerciseDate { get; set; }
    }

    public static class MainWindow
    {
         public static IEnumerable<Instrument> GetInstrumentByUdlEliot(string udlEliotCode)
         {
              return new List<Instrument>
              {
                   new Instrument { BasketInstrumentID = 1, fixingDateNext = DateTime.Today }
              };
         }
    }

    public class Instrument
    {
         public int BasketInstrumentID { get; set; }
         public DateTime fixingDateNext { get; set; }
    }
}
```

---

### Explanation

1. **Polymorphism Over String Comparisons:**  
   Each source’s data retrieval logic is encapsulated in its own class (for example, AlcorCorporateActionSource). The domain objects (AlcorCa, AlcorRdi, QDataCa, FbiDiv) implement ICorporateAction with JSON‐based mapping. No if‑else or string comparisons are needed in the service.

2. **Incorporation of Inheritance Issues:**  
   The domain’s original interface (from your “using System; …”) is integrated in ICorporateAction and implemented by CorporateActionBase. Classes such as AlcorCa and AlcorRdi use attributes from Newtonsoft.Json to map JSON fields. The AlcorRdi class even includes a ComputeRdi method that mirrors your Python‑to‑C# transformation logic.

3. **FBI Div for PDI:**  
   A separate FBI dividend class (FbiDiv) is provided in its own namespace so that FBI’s specific logic can be implemented there without mixing with the other sources.

4. **Modern C# Features:**  
   The code makes extensive use of interfaces, abstract classes, properties with getters/setters and property changed notifications, async/await for asynchronous operations, LINQ for grouping/aggregation, and dependency injection via constructor parameters.

This complete implementation demonstrates a clean, extensible architecture that addresses your concerns while incorporating the inheritance patterns and FBI dividend source you mentioned. Happy coding!


Below is an example of how you might implement the dividend‐retrieval method (GetDividendsAsync) in one of your source classes (here we use the Alcor source as an example) while incorporating the dividend–transformation logic from your Python-to‑C# translation. Notice that in this design you do not compare string literals; instead, each source is encapsulated in its own class. In a real application you would create similar implementations for QData and FBI. 

The complete code below shows:  
• The refactored common domain and service classes (which use polymorphism and dependency injection)  
• An IDataAccess interface and a sample Dividend data class  
• A concrete implementation of ICorporateActionSource (named AlcorCorporateActionSource) whose GetDividendsAsync method demonstrates the transformation—grouping dividend records by ex‑date, applying (sample) formulas, and creating CompositeCorporateAction objects.

You can adapt the formulas and logic as needed. Notice the inline comments explaining how the transformation resembles the logic from the Python–to–C# version.

---

```csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;

namespace FactorMonitor.DataAcquisition.CorporateAction
{
    // ============================================================
    // DOMAIN MODELS & ABSTRACTS
    // ============================================================
    
    /// <summary>
    /// Common interface for corporate actions.
    /// </summary>
    public interface ICorporateAction : INotifyPropertyChanged
    {
        string UdlEliotCode { get; }
        DateTime ExerciseDate { get; }
        double DivGross { get; set; }
        double DivNet { get; set; }
        double? REactor { get; set; }
        string DivStatus { get; set; }
        string EFactorStatus { get; set; }

        /// <summary>
        /// Aggregates a group of corporate actions into one.
        /// </summary>
        ICorporateAction Aggregate(IEnumerable<ICorporateAction> actions);
    }

    /// <summary>
    /// Base class that implements ICorporateAction with property notifications.
    /// It also provides a default aggregation (summing dividend values).
    /// </summary>
    public abstract class CorporateActionBase : ICorporateAction
    {
        private string _udlEliotCode;
        private DateTime _exerciseDate;
        private double _divGross;
        private double _divNet;
        private double? _rEactor;
        private string _divStatus;
        private string _eFactorStatus;

        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged(string propName) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));

        public string UdlEliotCode
        {
            get => _udlEliotCode;
            protected set { _udlEliotCode = value; NotifyPropertyChanged(nameof(UdlEliotCode)); }
        }

        public DateTime ExerciseDate
        {
            get => _exerciseDate;
            protected set { _exerciseDate = value; NotifyPropertyChanged(nameof(ExerciseDate)); }
        }

        public double DivGross
        {
            get => _divGross;
            set { _divGross = value; NotifyPropertyChanged(nameof(DivGross)); }
        }
        public double DivNet
        {
            get => _divNet;
            set { _divNet = value; NotifyPropertyChanged(nameof(DivNet)); }
        }
        public double? REactor
        {
            get => _rEactor;
            set { _rEactor = value; NotifyPropertyChanged(nameof(REactor)); }
        }
        public string DivStatus
        {
            get => _divStatus;
            set { _divStatus = value; NotifyPropertyChanged(nameof(DivStatus)); }
        }
        public string EFactorStatus
        {
            get => _eFactorStatus;
            set { _eFactorStatus = value; NotifyPropertyChanged(nameof(EFactorStatus)); }
        }

        /// <summary>
        /// By default, aggregate corporate actions by summing DivGross and DivNet.
        /// </summary>
        public virtual ICorporateAction Aggregate(IEnumerable<ICorporateAction> actions)
        {
            double totalGross = actions.Sum(a => a.DivGross);
            double totalNet = actions.Sum(a => a.DivNet);
            var aggregated = (CorporateActionBase)this.MemberwiseClone();
            aggregated.DivGross = totalGross;
            aggregated.DivNet = totalNet;
            return aggregated;
        }
    }

    /// <summary>
    /// A composite corporate action used by many parts of the system.
    /// </summary>
    public class CompositeCorporateAction : CorporateActionBase
    {
        // Additional properties (like upload status) can be added here.
    }

    // Specific implementations for the various sources.
    public class AlcorCorporateAction : CorporateActionBase { }
    public class FbiCorporateAction : CorporateActionBase { }
    public class QDataCorporateAction : CorporateActionBase { }

    // ============================================================
    // SOURCE INTERFACE
    // ============================================================
    /// <summary>
    /// Defines the contract to retrieve corporate actions and dividend data.
    /// </summary>
    public interface ICorporateActionSource
    {
        // Retrieves CA data for general corporate action processing.
        Task<IEnumerable<ICorporateAction>> GetCorporateActionsAsync(DateTime from, DateTime to, ICollection<string> codes);
        // Retrieves dividend data (PDIs) from the source.
        Task<IEnumerable<ICorporateAction>> GetDividendsAsync(DateTime from, DateTime to, ICollection<string> codes);
    }
    
    // ============================================================
    // FEE CALCULATION HELPER (unchanged from previous refactoring)
    // ============================================================
    public static class FeeCalculator
    {
        public static int GetUpdatedFees(
            IDictionary<int, List<AsiaFactorDataSet>> factorDataSetsByBasket,
            ICollection<int> basketInstrumentIds,
            int feeType)
        {
            var basketChangeIds = basketInstrumentIds
                .Select(id => factorDataSetsByBasket.TryGetValue(id, out var values) ? values.FirstOrDefault()?.ChangeId : null)
                .Where(cid => cid != null)
                .ToList();

            var feeHistory = FbiConnector.GetFactorIndexHistoryFee(basketChangeIds, feeType);
            int updatedFees = feeHistory.Count(fee => fee != null && !IsDefaultValue((double)fee, feeType));
            return updatedFees;
        }

        private static bool IsDefaultValue(double value, int feeType)
        {
            return feeType == 13 ? Math.Abs(value) < 1e-10 : value == 1d;
        }
    }

    // Dummy definitions for external types
    public class AsiaFactorDataSet
    {
        public int ChangeId { get; set; }
    }
    public static class FbiConnector
    {
        public static List<double?> GetFactorIndexHistoryFee(List<object> basketChangeIds, int feeType)
        {
            // Replace with actual fee-retrieval logic.
            return new List<double?> { 0.5, 1.0, null };
        }
    }

    // ============================================================
    // UI OR EXTERNAL LAYER INTERFACES
    // ============================================================
    public interface ISelectedCorporateAction
    {
        string UdlEliotCode { get; }
        DateTime ExerciseDate { get; }
    }
    public class SelectedCa : ISelectedCorporateAction
    {
        public string UdlEliotCode { get; set; }
        public DateTime ExerciseDate { get; set; }
    }
    public class SelectedRfactor : ISelectedCorporateAction
    {
        public string UdlEliotCode { get; set; }
        public DateTime ExerciseDate { get; set; }
    }

    // ============================================================
    // THE REFACTORED CORPORATE ACTION SERVICE
    // ============================================================
    /// <summary>
    /// This service retrieves and processes corporate actions and dividend data from three sources:
    /// Alcor, QData, and FBI.
    /// </summary>
    public class CorporateActionService
    {
        private static readonly int divFeeType = 13;
        private static readonly int eFactorFeeType = 19;

        // Collections for processed dividend data (PDIs) and corporate actions (CAs).
        private readonly ObservableCollection<ICorporateAction> _pdis;
        private readonly ObservableCollection<ICorporateAction> _cas;

        // Data source instances (injected via the constructor).
        private readonly ICorporateActionSource _alcorSource;
        private readonly ICorporateActionSource _qDataSource;
        private readonly ICorporateActionSource _fbiSource;

        // Lookup for fee updating.
        private Dictionary<int, List<AsiaFactorDataSet>> _factorDataSetsByBasket;

        public CorporateActionService(
            ObservableCollection<ICorporateAction> pdis,
            ObservableCollection<ICorporateAction> cas,
            ICorporateActionSource alcorSource,
            ICorporateActionSource qDataSource,
            ICorporateActionSource fbiSource)
        {
            _pdis = pdis;
            _cas = cas;
            _alcorSource = alcorSource;
            _qDataSource = qDataSource;
            _fbiSource = fbiSource;
        }

        /// <summary>
        /// Initializes CA and dividend (PDI) data by concurrently retrieving objects from all sources.
        /// </summary>
        public async Task InitAsync(
            ICollection<string> prIndexes,
            ICollection<string> prStocks,
            DateTime from,
            DateTime to,
            Dictionary<int, List<AsiaFactorDataSet>> factorDataSetsByBasket)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            _factorDataSetsByBasket = factorDataSetsByBasket;

            var caSetupTask = SetupCasAsync(from, to, prStocks);
            var pdiSetupTask = SetupPdisAsync(from, to, prIndexes);
            await Task.WhenAll(caSetupTask, pdiSetupTask);

            watch.Stop();
            // (Log the elapsed time if needed)
        }

        /// <summary>
        /// Updates the upload status for the selected corporate actions.
        /// </summary>
        public void UpdateCorporateActionUploadStatus<T>(ICollection<T> corporateActions, int feeType)
            where T : ISelectedCorporateAction
        {
            System.Threading.Tasks.Parallel.ForEach(corporateActions, corporateAction =>
            {
                var basketInstrumentIds = MainWindow.GetInstrumentByUdlEliot(corporateAction.UdlEliotCode)
                                                     .Select(instr => instr.BasketInstrumentID)
                                                     .ToList();

                int updatedFees = FeeCalculator.GetUpdatedFees(_factorDataSetsByBasket, basketInstrumentIds, feeType);
                var actionToUpdate = GetCorporateActionToUpdate(corporateAction);
                UpdateCorporateActionStatus(basketInstrumentIds.Count, corporateAction, actionToUpdate, updatedFees);
            });
        }

        private ICorporateAction GetCorporateActionToUpdate(ISelectedCorporateAction corporateAction)
        {
            if (corporateAction is SelectedCa)
                return FindInPdisForCas(corporateAction) ?? FindInCas(corporateAction);
            else if (corporateAction is SelectedRfactor)
                return FindInCas(corporateAction);
            else
                return null;
        }

        private ICorporateAction FindInPdisForCas(ISelectedCorporateAction corporateAction)
        {
            return _pdis.FirstOrDefault(pdi =>
                pdi.UdlEliotCode.Equals(corporateAction.UdlEliotCode, StringComparison.OrdinalIgnoreCase) &&
                pdi.ExerciseDate.Equals(corporateAction.ExerciseDate))
                ?? FindInCas(corporateAction);
        }

        private ICorporateAction FindInCas(ISelectedCorporateAction corporateAction)
        {
            return _cas.FirstOrDefault(ca =>
                ca.UdlEliotCode.Equals(corporateAction.UdlEliotCode, StringComparison.OrdinalIgnoreCase) &&
                ca.ExerciseDate.Equals(corporateAction.ExerciseDate));
        }

        private void UpdateCorporateActionStatus(int basketInstrumentCount, ISelectedCorporateAction selectedCorporateAction,
            ICorporateAction corporateActionToUpdate, int updatedFees)
        {
            string status = $"{updatedFees}/{basketInstrumentCount}";
            if (selectedCorporateAction is SelectedCa)
                corporateActionToUpdate.DivStatus = status;
            else if (selectedCorporateAction is SelectedRfactor)
                corporateActionToUpdate.EFactorStatus = status;
        }

        // ============================================================
        // SETUP METHODS FOR DATA RETRIEVAL FROM SOURCES
        // ============================================================
        /// <summary>
        /// Retrieves dividend (PDI) data from all three sources concurrently and adds them to _pdis.
        /// </summary>
        private async Task SetupPdisAsync(DateTime from, DateTime to, ICollection<string> prIndexes)
        {
            var alcorDivsTask = _alcorSource.GetDividendsAsync(from, to, prIndexes);
            var qDataDivsTask = _qDataSource.GetDividendsAsync(from, to, prIndexes);
            var fbiDivsTask = _fbiSource.GetDividendsAsync(from, to, prIndexes);
            await Task.WhenAll(alcorDivsTask, qDataDivsTask, fbiDivsTask);

            var combinedPDIs = alcorDivsTask.Result
                                    .Concat(qDataDivsTask.Result)
                                    .Concat(fbiDivsTask.Result);
            foreach (var pdi in combinedPDIs)
            {
                _pdis.Add(pdi);
            }
        }

        /// <summary>
        /// Retrieves corporate actions from sources that provide CA data.
        /// (FBI is assumed to supply only dividend data.)
        /// </summary>
        private async Task SetupCasAsync(DateTime from, DateTime to, ICollection<string> prStocks)
        {
            var alcorCAsTask = _alcorSource.GetCorporateActionsAsync(from, to, prStocks);
            var qDataCAsTask = _qDataSource.GetCorporateActionsAsync(from, to, prStocks);
            await Task.WhenAll(alcorCAsTask, qDataCAsTask);

            var combinedCAs = alcorCAsTask.Result.Concat(qDataCAsTask.Result);
            foreach (var ca in combinedCAs)
            {
                _cas.Add(ca);
            }
        }

        // ============================================================
        // AGGREGATION & DUPLICATE HANDLING
        // ============================================================
        /// <summary>
        /// Groups and aggregates corporate actions by UdlEliotCode and ExerciseDate.
        /// </summary>
        public Dictionary<Tuple<string, DateTime>, ICorporateAction> ProcessCorporateActions(IEnumerable<ICorporateAction> actions)
        {
            var duplicateGroups = actions.GroupBy(ca => Tuple.Create(ca.UdlEliotCode, ca.ExerciseDate))
                                           .Where(g => g.Count() > 1)
                                           .ToDictionary(g => g.Key, g => g.ToList());

            if (duplicateGroups.Any())
            {
                var message = "Duplicate Corporate Actions detected for:\n" +
                              string.Join("\n", duplicateGroups.Select(g => $"{g.Key.Item1} on {g.Key.Item2:yyyy-MM-dd}"));
                System.Windows.MessageBox.Show(message, "Duplicate Corporate Actions");
            }

            var aggregatedActions = actions.GroupBy(ca => Tuple.Create(ca.UdlEliotCode, ca.ExerciseDate))
                                             .ToDictionary(
                g => g.Key,
                g => AggregateCorporateActions(g.ToList())
            );

            return aggregatedActions;
        }

        private ICorporateAction AggregateCorporateActions(List<ICorporateAction> actions)
        {
            if (actions.Count == 1)
                return actions.First();

            return ((CorporateActionBase)actions.First()).Aggregate(actions);
        }
    }

    // ============================================================
    // A SIMPLE DATA ACCESS INTERFACE & DIVIDEND DATA CLASS
    // ============================================================
    public interface IDataAccess
    {
        Task<List<Dividend>> GetDividendsAsync(DateTime from, DateTime to);
    }

    public class Dividend
    {
        public string UniqueName { get; set; }
        public DateTime CorporateActionExDate { get; set; }
        public decimal DividendGrossAmount { get; set; }
        public decimal DividendNetAmount { get; set; }
        public string DividendCurrency { get; set; }
    }

    // ============================================================
    // SAMPLE IMPLEMENTATION OF ICorporateActionSource FOR ALCOR
    // ============================================================
    /// <summary>
    /// This class implements the dividend retrieval for the Alcor source.
    /// It transforms raw dividend data (retrieved via IDataAccess) into CompositeCorporateAction instances.
    /// </summary>
    public class AlcorCorporateActionSource : ICorporateActionSource
    {
        private readonly IDataAccess _dataAccess;
        
        public AlcorCorporateActionSource(IDataAccess dataAccess)
        {
            _dataAccess = dataAccess;
        }

        public async Task<IEnumerable<ICorporateAction>> GetCorporateActionsAsync(DateTime from, DateTime to, ICollection<string> codes)
        {
            // (Implement corporate action retrieval for Alcor if applicable.)
            // For demonstration purposes, we return an empty list.
            return await Task.FromResult(new List<ICorporateAction>());
        }

        public async Task<IEnumerable<ICorporateAction>> GetDividendsAsync(DateTime from, DateTime to, ICollection<string> codes)
        {
            // Retrieve raw dividend data using the provided data access layer.
            List<Dividend> dividends = await _dataAccess.GetDividendsAsync(from, to);

            // The transformation logic (from your Python-to-C# translation) begins here.
            // We group dividends by their CorporateActionExDate.
            var groupedDividends = dividends.GroupBy(d => d.CorporateActionExDate);

            List<ICorporateAction> results = new List<ICorporateAction>();
            foreach (var group in groupedDividends)
            {
                // Create a composite corporate action for this dividend date.
                var composite = new CompositeCorporateAction();
                composite.UdlEliotCode = "ALCOR_DIV_" + group.Key.ToString("yyyyMMdd");
                composite.ExerciseDate = group.Key;

                // For each dividend record, you might wish to compute:
                // - If a Total Return (TR) index, use:
                //       shortDip = (dividendAmount / fxConversionFactor) * (1 - receive) * componentUnit;
                //       longDip  = (dividendAmount / fxConversionFactor) * (receive - reinvest) * componentUnit;
                // - If a Price index, a simpler adjustment.
                //
                // In this example, we simply sum the gross and net amounts.
                // In your production code, you would apply the FX conversion,
                // receive/reinvest factors, and adjust by the appropriate divisor.
                composite.DivGross = group.Sum(d => (double)d.DividendGrossAmount);
                composite.DivNet = group.Sum(d => (double)d.DividendNetAmount);

                results.Add(composite);
            }

            return results;
        }
    }

    // ============================================================
    // STUBS FOR EXTERNAL DEPENDENCIES (FOR COMPILATION)
    // ============================================================
    public static class MainWindow
    {
        /// <summary>
        /// Stub for retrieving instruments by UdlEliot code.
        /// </summary>
        public static IEnumerable<Instrument> GetInstrumentByUdlEliot(string udlEliotCode)
        {
            return new List<Instrument> 
            { 
                new Instrument { BasketInstrumentID = 1, fixingDateNext = DateTime.Today } 
            };
        }
    }

    public class Instrument
    {
        public int BasketInstrumentID { get; set; }
        public DateTime fixingDateNext { get; set; }
    }
}
```

---

### Explanation of Key Points

1. **Polymorphism Instead of String Comparisons:**  
   • Each source (Alcor, QData, FBI) implements its own version of GetDividendsAsync through the ICorporateActionSource interface.  
   • There is no need for if/else statements comparing strings like `"Alcor"` because the correct behavior is encapsulated in each class.

2. **GetDividendsAsync in AlcorCorporateActionSource:**  
   • This method uses a data access layer (IDataAccess) to asynchronously obtain raw dividend records.  
   • It groups dividends by their ex‑date and then—inspired by your Python transformation logic—creates a CompositeCorporateAction for each group.  
   • In a complete implementation you would include the detailed arithmetic (including FX conversion, the receive/reinvest factors, and basket unit adjustments) rather than simply summing gross and net values.

3. **Modern C# Features Used:****Async/Await & Task.WhenAll:** Allow concurrent retrieval of data from different sources.  
   • **LINQ & Lambda Expressions:** Group and transform the raw dividend data in a readable, concise manner.  
   • **Interfaces and Abstract Classes:** Enable a clean, easily extensible design where new sources require minimal changes elsewhere.

4. **Extensibility:**  
   Adding a new source (such as QData or FBI) requires simply creating another class that implements ICorporateActionSource (with its own GetDividendsAsync and GetCorporateActionsAsync methods). No switch-case or if/else based on strings is necessary.

This approach is cleaner, more extensible, and adheres to object-oriented and clean code principles compared to relying on string comparisons. Happy coding!

Below is one complete rewrite of your entire CorporateAction service code. The goal is to make the code cleaner, more modular, and extensible so that adding a new dividend source (such as FBI) no longer forces you to duplicate code or perform string‐based casting. In this rewrite, we:

• Separate data retrieval from processing.  
• Define interfaces and base classes so that each dividend source (for example, Alcor and FBI) can implement its own retrieval logic.  
• Aggregate and update corporate actions and related fees via helper classes and polymorphic methods rather than by re‑casting objects.  
• Make heavy use of LINQ, async/await, and dependency injection ideas (the code is written in one file for clarity, but in a real application you would break this into separate files).

The code below is a “final” version that consolidates all the functionality you described (CA/PDI setup, duplicate detection/aggregation, fee update, upload status update, etc.) while using clean code and OOP principles. (Note that some external dependencies such as MainWindow, FbiConnector, AsiaFactorDataSet, etc. are represented as stubs or placeholders so you can integrate with your existing code.) 

---

```csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;

namespace FactorMonitor.DataAcquisition.CorporateAction
{
    // ============================================================
    // DOMAIN MODELS & ABSTRACTS
    // ============================================================

    /// <summary>
    /// The common interface for all corporate action objects.
    /// </summary>
    public interface ICorporateAction : INotifyPropertyChanged
    {
        string UdlEliotCode { get; }
        DateTime ExerciseDate { get; }
        double DivGross { get; set; }
        double DivNet { get; set; }
        double? REactor { get; set; }
        string DivStatus { get; set; }
        string EFactorStatus { get; set; }
    }

    /// <summary>
    /// Base class implementing ICorporateAction with INotifyPropertyChanged.
    /// </summary>
    public abstract class CorporateActionBase : ICorporateAction
    {
        private string _udlEliotCode;
        private DateTime _exerciseDate;
        private double _divGross;
        private double _divNet;
        private double? _rEactor;
        private string _divStatus;
        private string _eFactorStatus;

        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged(string propName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        }

        public string UdlEliotCode
        {
            get => _udlEliotCode;
            protected set { _udlEliotCode = value; NotifyPropertyChanged(nameof(UdlEliotCode)); }
        }

        public DateTime ExerciseDate
        {
            get => _exerciseDate;
            protected set { _exerciseDate = value; NotifyPropertyChanged(nameof(ExerciseDate)); }
        }

        public double DivGross
        {
            get => _divGross;
            set { _divGross = value; NotifyPropertyChanged(nameof(DivGross)); }
        }
        public double DivNet
        {
            get => _divNet;
            set { _divNet = value; NotifyPropertyChanged(nameof(DivNet)); }
        }
        public double? REactor
        {
            get => _rEactor;
            set { _rEactor = value; NotifyPropertyChanged(nameof(REactor)); }
        }
        public string DivStatus
        {
            get => _divStatus;
            set { _divStatus = value; NotifyPropertyChanged(nameof(DivStatus)); }
        }
        public string EFactorStatus
        {
            get => _eFactorStatus;
            set { _eFactorStatus = value; NotifyPropertyChanged(nameof(EFactorStatus)); }
        }
    }

    // This is the “composite” corporate action used in many parts of your system.
    // We now derive from CorporateActionBase so that we have a common base.
    public class CompositeCorporateAction : CorporateActionBase
    {
        // Additional properties required for upload statistics may be added here.
        // For example, product count, currency codes, etc.
    }

    // Two concrete implementations for different dividend sources.
    public class AlcorCorporateAction : CorporateActionBase
    {
        // Properties specific to Alcor if needed.
    }
    public class FbiCorporateAction : CorporateActionBase
    {
        // Properties or behavior specific to the FBI dividend source.
    }

    // ============================================================
    // SOURCE INTERFACE & FACTORY
    // ============================================================

    /// <summary>
    /// Interface to abstract how corporate actions or dividend data is fetched.
    /// A source can be Alcor, FBI, etc.
    /// </summary>
    public interface ICorporateActionSource
    {
        // Fetches CA data used in the composite CA section.
        Task<IEnumerable<ICorporateAction>> GetCorporateActionsAsync(DateTime from, DateTime to, ICollection<string> codes);
        // Fetches dividend information used for PDIs (dividend source).
        Task<IEnumerable<ICorporateAction>> GetDividendsAsync(DateTime from, DateTime to, ICollection<string> codes);
    }
    
    // A simple factory that creates a CA object from raw data and a source name.
    public static class CorporateActionFactory
    {
        public static ICorporateAction Create(string source, dynamic rawData)
        {
            if (string.Equals(source, "Alcor", StringComparison.OrdinalIgnoreCase))
            {
                return new AlcorCorporateAction
                {
                    UdlEliotCode = rawData.UdlEliotCode,
                    ExerciseDate = rawData.ExerciseDate,
                    DivGross = rawData.DivGross,
                    DivNet = rawData.DivNet,
                    REactor = rawData.Reactor
                };
            }
            else if (string.Equals(source, "FBI", StringComparison.OrdinalIgnoreCase))
            {
                return new FbiCorporateAction
                {
                    UdlEliotCode = rawData.UdlEliotCode,
                    ExerciseDate = rawData.ExerciseDate,
                    DivGross = rawData.DivGross,
                    DivNet = rawData.DivNet
                };
            }
            throw new NotSupportedException($"Source '{source}' is not supported.");
        }
    }

    // ============================================================
    // FEE CALCULATION HELPER
    // ============================================================

    public static class FeeCalculator
    {
        public static int GetUpdatedFees(
            IDictionary<int, List<AsiaFactorDataSet>> factorDataSetsByBasketInstrument,
            ICollection<int> basketInstrumentIds,
            int feeType)
        {
            // Retrieve the basket change IDs from the factor data sets.
            var basketChangeIds = basketInstrumentIds
                .Select(id => factorDataSetsByBasketInstrument.TryGetValue(id, out var values) ? values.FirstOrDefault()?.ChangeId : null)
                .Where(cid => cid != null)
                .ToList();

            // Retrieve fee history from an external connector.
            var feeHistory = FbiConnector.GetFactorIndexHistoryFee(basketChangeIds, feeType);
            int updatedFees = feeHistory.Count(fee => fee != null && !IsDefaultValue((double)fee, feeType));
            return updatedFees;
        }

        private static bool IsDefaultValue(double value, int feeType)
        {
            // For dividend fee (divFeeType == 13), we treat near-zero as default.
            return feeType == 13 ? Math.Abs(value) < 1e-10 : value == 1d;
        }
    }

    // Dummy definitions for external types.
    public class AsiaFactorDataSet
    {
        public int ChangeId { get; set; }
        // Other properties…
    }
    public static class FbiConnector
    {
        public static List<double?> GetFactorIndexHistoryFee(List<object> basketChangeIds, int feeType)
        {
            // Replace with your real connection
            return new List<double?> { 0.5, 1.0, null };
        }
    }

    // ============================================================
    // INTERFACES USED BY THE UI OR OTHER LAYERS
    // ============================================================
    public interface ISelectedCorporateAction
    {
        string UdlEliotCode { get; }
        DateTime ExerciseDate { get; }
    }
    public class SelectedCa : ISelectedCorporateAction
    {
        public string UdlEliotCode { get; set; }
        public DateTime ExerciseDate { get; set; }
    }
    public class SelectedRfactor : ISelectedCorporateAction
    {
        public string UdlEliotCode { get; set; }
        public DateTime ExerciseDate { get; set; }
    }

    // ============================================================
    // THE REWRITTEN CORPORATE ACTION SERVICE
    // ============================================================

    /// <summary>
    /// Rewritten CorporateActionService that uses dependency injection of sources,
    /// polymorphism (no messy casting), and clean separation of concerns.
    /// </summary>
    public class CorporateActionService
    {
        private static readonly int divFeeType = 13;
        private static readonly int eFactorFeeType = 19;

        // Collections holding processed corporate actions.
        private readonly ObservableCollection<ICorporateAction> _pdis;
        private readonly ObservableCollection<ICorporateAction> _cas;

        // Sources for retrieving data from different vendors.
        private readonly ICorporateActionSource _alcorSource;
        private readonly ICorporateActionSource _fbiSource;

        // Factor data sets (lookup for fee updates).
        private Dictionary<int, List<AsiaFactorDataSet>> _factorDataSetsByBasketInstrument;

        public CorporateActionService(
            ObservableCollection<ICorporateAction> pdis,
            ObservableCollection<ICorporateAction> cas,
            ICorporateActionSource alcorSource,
            ICorporateActionSource fbiSource)
        {
            _pdis = pdis;
            _cas = cas;
            _alcorSource = alcorSource;
            _fbiSource = fbiSource;
        }

        /// <summary>
        /// Initializes corporate action data by concurrently setting up PDIs (dividends)
        /// and corporate actions from available sources.
        /// </summary>
        public async Task InitAsync(
            ICollection<string> prIndexes,
            ICollection<string> prStocks,
            DateTime from,
            DateTime to,
            Dictionary<int, List<AsiaFactorDataSet>> factorDataSetsByBasketInstrument)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            // Log: "Start requesting CA" (using your logging framework)
            _factorDataSetsByBasketInstrument = factorDataSetsByBasketInstrument;

            // Start CA and PDI setups concurrently.
            var caSetupTask = SetupCasAsync(from, to, prStocks);
            var pdiSetupTask = SetupPdisAsync(from, to, prIndexes);
            await Task.WhenAll(caSetupTask, pdiSetupTask);

            watch.Stop();
            // Log: "Finished requesting CA in {elapsedSeconds} seconds"
        }

        /// <summary>
        /// Updates the upload status for the provided corporate actions. This method uses
        /// parallel processing and defers to helper methods so that no explicit casting is needed.
        /// </summary>
        public void UpdateCorporateActionUploadStatus<T>(ICollection<T> corporateActions, int feeType)
            where T : ISelectedCorporateAction
        {
            System.Threading.Tasks.Parallel.ForEach(corporateActions, corporateAction =>
            {
                // Retrieve a list of basket instrument IDs based on the corporate action's UdlEliotCode.
                var basketInstrumentIds = MainWindow.GetInstrumentByUdlEliot(corporateAction.UdlEliotCode)
                                                     .Select(instr => instr.BasketInstrumentID)
                                                     .ToList();

                int updatedFees = FeeCalculator.GetUpdatedFees(_factorDataSetsByBasketInstrument, basketInstrumentIds, feeType);
                var actionToUpdate = GetCorporateActionToUpdate(corporateAction);
                UpdateCorporateActionStatus(basketInstrumentIds.Count, corporateAction, actionToUpdate, updatedFees);
            });
        }

        /// <summary>
        /// Determines which underlying corporate action should be updated given the selected CA.
        /// </summary>
        private ICorporateAction GetCorporateActionToUpdate(ISelectedCorporateAction corporateAction)
        {
            if (corporateAction is SelectedCa)
                return FindInPdisForCas(corporateAction) ?? FindInCas(corporateAction);
            else if (corporateAction is SelectedRfactor)
                return FindInCas(corporateAction);
            else
                return null;
        }

        private ICorporateAction FindInPdisForCas(ISelectedCorporateAction corporateAction)
        {
            // Searches the PDIs for a match based on UdlEliotCode and ExerciseDate.
            return _pdis.FirstOrDefault(pdi =>
                pdi.UdlEliotCode.Equals(corporateAction.UdlEliotCode, StringComparison.OrdinalIgnoreCase) &&
                pdi.ExerciseDate.Equals(corporateAction.ExerciseDate))
                ?? FindInCas(corporateAction);
        }

        private ICorporateAction FindInCas(ISelectedCorporateAction corporateAction)
        {
            // Searches the CA collection.
            return _cas.FirstOrDefault(ca =>
                ca.UdlEliotCode.Equals(corporateAction.UdlEliotCode, StringComparison.OrdinalIgnoreCase) &&
                ca.ExerciseDate.Equals(corporateAction.ExerciseDate));
        }

        private void UpdateCorporateActionStatus(int basketInstrumentCount, ISelectedCorporateAction selectedCorporateAction,
            ICorporateAction corporateActionToUpdate, int updatedFees)
        {
            // Format the status string based on updated fees and total basket instrument count.
            string status = $"{updatedFees}/{basketInstrumentCount}";
            if (selectedCorporateAction is SelectedCa)
                corporateActionToUpdate.DivStatus = status;
            else if (selectedCorporateAction is SelectedRfactor)
                corporateActionToUpdate.EFactorStatus = status;
        }

        // ============================================================
        // SETUP METHODS FOR PDIs AND CAS
        // ============================================================

        private async Task SetupPdisAsync(DateTime from, DateTime to, ICollection<string> prIndexes)
        {
            // Retrieve dividend data (PDIs) from both sources concurrently.
            var alcorDividendsTask = _alcorSource.GetDividendsAsync(from, to, prIndexes);
            var fbiDividendsTask = _fbiSource.GetDividendsAsync(from, to, prIndexes);
            await Task.WhenAll(alcorDividendsTask, fbiDividendsTask);

            var combinedPDIs = alcorDividendsTask.Result.Concat(fbiDividendsTask.Result);
            foreach (var pdi in combinedPDIs)
            {
                _pdis.Add(pdi);
            }
        }

        private async Task SetupCasAsync(DateTime from, DateTime to, ICollection<string> prStocks)
        {
            // For corporate actions, we only use the Alcor source if FBI does not apply.
            var alcorCAs = await _alcorSource.GetCorporateActionsAsync(from, to, prStocks);
            foreach (var ca in alcorCAs)
            {
                _cas.Add(ca);
            }
        }

        // ============================================================
        // AGGREGATION & DUPLICATE PROCESSING FOR CORPORATE ACTIONS
        // ============================================================

        /// <summary>
        /// Processes a collection of corporate actions by grouping duplicates by UdlEliotCode and ExerciseDate.
        /// If duplicate dividend values are detected, a warning is raised.
        /// </summary>
        public Dictionary<Tuple<string, DateTime>, ICorporateAction> ProcessCorporateActions(IEnumerable<ICorporateAction> actions, string caSource)
        {
            // Group actions by unique key.
            var duplicateGroups = actions.GroupBy(ca => Tuple.Create(ca.UdlEliotCode, ca.ExerciseDate))
                                          .Where(g => g.Count() > 1)
                                          .ToDictionary(g => g.Key, g => g.ToList());

            if (duplicateGroups.Any())
            {
                InformDuplicateCA(duplicateGroups, caSource);
            }

            // Aggregate values for each group.
            var aggregatedActions = actions.GroupBy(ca => Tuple.Create(ca.UdlEliotCode, ca.ExerciseDate))
                                             .ToDictionary(
                g => g.Key,
                g => AggregateCorporateActions(g.ToList(), caSource));

            return aggregatedActions;
        }

        private void InformDuplicateCA(Dictionary<Tuple<string, DateTime>, List<ICorporateAction>> duplicates, string caSource)
        {
            var message = $"[{caSource}] Duplicate Corporate Actions detected for:\n";
            foreach (var pair in duplicates)
            {
                message += $"{pair.Key.Item1} on {pair.Key.Item2:yyyy-MM-dd}:\n";
                foreach (var ca in pair.Value)
                {
                    message += $"  Gross: {ca.DivGross}, Net: {ca.DivNet}\n";
                }
            }
            // In production, log or handle the message appropriately (avoid MessageBox in core logic).
            System.Windows.MessageBox.Show(message, "Duplicate Corporate Actions");
        }

        private ICorporateAction AggregateCorporateActions(List<ICorporateAction> actions, string caSource)
        {
            if (actions.Count == 1)
                return actions.First();

            var first = actions.First();
            // If all dividend values are the same, return the first.
            bool allSame = actions.All(ca => ca.DivGross == first.DivGross && ca.DivNet == first.DivNet);
            if (allSame)
            {
                System.Windows.MessageBox.Show(
                    $"Duplicate dividend values detected for {first.UdlEliotCode} on {first.ExerciseDate:yyyy-MM-dd}",
                    "Duplicate Warning");
                return first;
            }
            else
            {
                // Otherwise, sum up the values.
                double totalGross = actions.Sum(ca => ca.DivGross);
                double totalNet = actions.Sum(ca => ca.DivNet);

                // Create a new composite action by copying the first one and updating the dividend values.
                if (string.Equals(caSource, "Alcor", StringComparison.OrdinalIgnoreCase))
                {
                    var alcorAction = first as AlcorCorporateAction ?? new AlcorCorporateAction();
                    alcorAction.DivGross = totalGross;
                    alcorAction.DivNet = totalNet;
                    return alcorAction;
                }
                else
                {
                    var fbiAction = first as FbiCorporateAction ?? new FbiCorporateAction();
                    fbiAction.DivGross = totalGross;
                    fbiAction.DivNet = totalNet;
                    return fbiAction;
                }
            }
        }
    }

    // ============================================================
    // STUBS FOR EXTERNAL DEPENDENCIES (for compilation only)
    // ============================================================

    public static class MainWindow
    {
        /// <summary>
        /// Stub for retrieving instrument info by UdlEliot code.
        /// The real implementation should return a collection with a BasketInstrumentID property.
        /// </summary>
        public static IEnumerable<Instrument> GetInstrumentByUdlEliot(string udlEliotCode)
        {
            // Return sample data.
            return new List<Instrument> { new Instrument { BasketInstrumentID = 1 } };
        }
    }

    public class Instrument
    {
        public int BasketInstrumentID { get; set; }
        // Assume additionally: a fixingDateNext property for CA processing.
        public DateTime fixingDateNext { get; set; } = DateTime.Today;
    }
}
```

---

### Explanation of the Rewrite

1. **Clean Separation with Interfaces and Base Classes:**  
   All corporate action types implement the common interface `ICorporateAction` (with common properties) through a base class (`CorporateActionBase`). This eliminates messy casts and string comparisons.

2. **Data Source Abstraction:**  
   The interface `ICorporateActionSource` lets you plug in different sources (Alcor, FBI, etc.) so that each implements its own methods for retrieving corporate actions or dividends. The service uses two injected sources (_alcorSource and _fbiSource).

3. **Asynchronous Setup and Parallel Processing:**  
   The `InitAsync` method downloads PDIs and CAS concurrently using async/await and Task.WhenAll, keeping the UI responsive and the code efficient. Upload status updates are processed in parallel.

4. **Aggregation and Duplicate Handling:**  
   The `ProcessCorporateActions` method groups actions by unique key and aggregates the dividend values. A helper method informs when duplicates are found and raises a warning.

5. **Fee Update Logic Isolated in a Helper:**  
   The fee calculator is separated into its own static class so that any changes to fee logic do not affect the main service.

6. **Extensibility & Clean Code Principles:**  
   In this design, adding new sources or changing the way corporate actions are processed is a matter of adding or modifying a small class rather than rewriting large monolithic methods. This code follows clean code and OOP principles by keeping responsibilities separate, using dependency injection (or constructor parameters), and minimizing duplicate code.

You can further split this code into separate files and integrate a dependency injection framework if desired. This rewrite should serve as a robust, scalable foundation for incorporating your additional dividend source without introducing unnecessary complexity.

Below is the same complete C# code as before—but now with many extra inline comments and additional explanations. In the comments I explain how the logic for calculating dividend points works and what the "receive" and "reinvest" amounts mean. I have also added comments to explain some common C# language features that appear in the code.

> **Background on Dividend Points Calculation:**  
> When calculating dividend points for an index, you are essentially adjusting the index level based on the dividend events. Dividends can be computed on a **gross** (total dividend declared) or **net** (dividend after any tax/fees) basis.  
>  
> In many indices, a portion of the dividend may be **received as cash** (the "receive" factor) while another portion may be **reinvested** (the "reinvest" factor). For instance, if an index is a total return index, it assumes dividends are reinvested—thus the effective yield contribution is the difference between the receive and reinvest factors.  
>  
> The code below groups dividends by their ex–date (the day when the dividend is credited), matches each dividend to the underlying index components (using a unique instrument identifier), and then applies formulas that differ by basket type:  
>  
> - **TR (Total Return) Index:**  
>   - **Short side (cash adjustment):**  
>     Formula: `(dividendAmount / fxConversion) * (1 - receive) * componentUnit`  
>   - **Long side (reinvestment gain):**  
>     Formula: `(dividendAmount / fxConversion) * (receive - reinvest) * componentUnit`
>  
> - **Price Index:**  
>   - The dividend might be incorporated with a different weight, often less adjusted than the TR index.
>  
> In addition, if the basket (index) is not in EUR, the dividend amount is converted from EUR to the basket currency using an FX conversion factor (obtained from a reference service).

Below is the complete code with additional inline comments. You can study the comments to learn both about how the dividend points are calculated and how various C# language features are used.

---

```csharp
// Full translation of the provided Python code into C# (.NET Framework 4.8)
// This version contains more comments explaining the dividend calculations and C# features.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace DipsReportProcessor
{
    // *******************************
    // Model Classes
    // *******************************

    // Represents a dividend record with both gross and net dividend values.
    public class Dividend
    {
        public decimal DividendGrossAmount { get; set; }
        public decimal DividendNetAmount { get; set; }
        // The ex-date is the date on which the dividend is applied/credited.
        public DateTime CorporateActionExDate { get; set; }
        // The unique identifier for an underlying instrument.
        public string UniqueName { get; set; }
        public string DividendCurrency { get; set; }
        public string MICCode { get; set; }
        public string Segment { get; set; }
        public string DividendType { get; set; }
    }

    // Represents a currency pair used for FX conversion.
    public class CurrencyPair
    {
        // Example: 'EUR' + 'USD' yields "EURUSD"
        public string SourceTarget { get; set; }
        public string ListingId { get; set; }
        public string CurrencyISOCode { get; set; }
    }

    // Represents an FX quote retrieved via REST using ICOM.
    public class FXSnap
    {
        public string InstrumentId { get; set; }
        public decimal Value { get; set; }
    }
    
    // Represents one component (an underlying instrument) within an index basket.
    public class Component
    {
        public string UniqueName { get; set; }
        public decimal Unit { get; set; }
        // ISIN: a unique security identifier—which here is used, among other things,
        // to determine the country (first two letters) and hence the dividend factors.
        public string ISIN { get; set; }
    }

    // Holds the details of the basket validity window and a divisor to adjust the dividend point.
    public class Composition
    {
        public DateTime ValidFrom { get; set; }
        public DateTime ValidTo { get; set; }
        // The divisor is used to adjust the computed points (for example, to scale the raw dividend contribution to index points).
        public decimal Divisor { get; set; }
    }

    // Represents an index basket that contains a list of underlying instruments.
    public class Basket
    {
        public string BasketUniqueName { get; set; }
        // Basket type might be "TR" (Total Return) or "Price". Different formulas apply.
        public string Type { get; set; }
        // The basket’s base currency (e.g., EUR) is needed for FX conversion.
        public string BasketCurrency { get; set; }
        // Composition details are represented using three possible composition windows (pcf, ecf, acf).
        public Composition Pcf { get; set; }
        public Composition Ecf { get; set; }
        public Composition Acf { get; set; }
        // List of underlying components (instruments) that compose the index.
        public List<Component> Composition { get; set; } = new List<Component>();
        // The computed dividend "points" are stored per date. Each day holds a short and long value.
        public Dictionary<DateTime, BasketDayDip> DaysDips { get; set; } = new Dictionary<DateTime, BasketDayDip>();
    }

    // Represents the aggregated dividend adjustment for an index for a specific day.
    public class BasketDayDip
    {
        public decimal ShortDip { get; set; }
        public decimal LongDip { get; set; }
    }

    // Represents the country–specific configuration for dividend processing.
    public class CountrySetting
    {
        // "Receive" is the factor representing the portion of a dividend that is paid as cash.
        public decimal Receive { get; set; }
        // "Reinvest" is the factor representing how much is assumed to be reinvested and thus not paid out as cash.
        public decimal Reinvest { get; set; }
    }

    // *******************************
    // Data Access Layer
    // *******************************

    // A simple helper class to execute SQL queries using ADO.NET.
    public class DataAccess
    {
        private readonly string _connectionString;
        public DataAccess()
        {
            // The connection string is read from App.config/Web.config.
            _connectionString = ConfigurationManager.AppSettings["MyDbConnection"];
        }
        // Generic async method using async/await to run a query.
        // The lambda parameter "map" converts each row (IDataRecord) into an object of type T.
        public async Task<List<T>> RunQueryAsync<T>(string sql, Func<IDataRecord, T> map)
        {
            var results = new List<T>();
            // "using" ensures resources like the SqlConnection are properly disposed.
            using (SqlConnection conn = new SqlConnection(_connectionString))
            using (SqlCommand cmd = new SqlCommand(sql, conn))
            {
                await conn.OpenAsync();
                using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        results.Add(map(reader));
                    }
                }
            }
            return results;
        }
    }

    // *******************************
    // Services
    // *******************************

    // Service to retrieve dividend records from the database.
    public class DividendService
    {
        private readonly DataAccess _dataAccess;
        public DividendService()
        {
            _dataAccess = new DataAccess();
        }
        // This method retrieves dividends between two dates.
        public async Task<List<Dividend>> GetDividendsAsync(DateTime reportFrom, DateTime reportTo)
        {
            // SQL query is built using date parameters.
            // Notice the use of string.Format for clarity (this could also be parameterized to protect against SQL injection).
            string sql = string.Format(@"
                SELECT DISTINCT
                    li.UniqueName,
                    cdd.ExDate AS CorporateActionExDate,
                    cdd.CurrencyISOCode AS DividendCurrency,
                    cad.GrossAmount,
                    cdd.NetAmount,
                    m.MIC AS MICCode,
                    m.Segment,
                    cdd.CorporateActionType,
                    cdd.DividendType
                FROM CashDividendDetail cdd
                INNER JOIN Listing li ON li.Id = cdd.ListingId
                INNER JOIN Market m ON m.Id = li.MarketId
                WHERE cdd.ExDate >= '{0}' 
                  AND cdd.ExDate < '{1}' 
                  AND LOWER(cdd.DividendType) NOT IN ('omitted', 'discontinued', 'return capital', 'cancelled')
                  AND cdd.IsPaidInPercentage = 0",
                reportFrom.ToString("yyyy-MM-dd"), reportTo.ToString("yyyy-MM-dd"));

            // The lambda in RunQueryAsync maps each row to a Dividend object.
            List<Dividend> dividends = await _dataAccess.RunQueryAsync(sql, record => new Dividend
            {
                UniqueName = record["UniqueName"].ToString(),
                CorporateActionExDate = Convert.ToDateTime(record["CorporateActionExDate"]),
                DividendCurrency = record["DividendCurrency"].ToString(),
                DividendGrossAmount = Convert.ToDecimal(record["GrossAmount"]),
                DividendNetAmount = Convert.ToDecimal(record["NetAmount"]),
                MICCode = record["MICCode"].ToString(),
                Segment = record["Segment"].ToString(),
                DividendType = record["DividendType"].ToString()
            });
            return dividends;
        }
    }

    // Service to retrieve basket data (index compositions and metadata) from the database.
    public class BasketService
    {
        private readonly DataAccess _dataAccess;
        public BasketService()
        {
            _dataAccess = new DataAccess();
        }
        // Mimics the Python function getBasketData.
        // Accepts a list of basket unique names and returns basket details keyed by their unique name.
        public async Task<Dictionary<string, Basket>> GetBasketDataAsync(List<string> basketUniqueNames)
        {
            // Build an SQL IN clause from the list.
            string inClause = string.Join(",", basketUniqueNames.Select(name => $"'{name}'"));
            string sql = $@"
                SELECT 
                    rec.BasketUniqueName,
                    rec.Restriction AS Description,
                    rec.ValidFrom,
                    rec.ValidTo,
                    rec.Divisor,
                    rec.Cash,
                    rec.CashCurrency,
                    bft.Name AS BasketFlavourType,
                    bli.CurrencyISOCode AS BasketCurrency,
                    rfi.Page AS IndexReutersPage,
                    bfi.Page AS IndexBloombergPage,
                    bbilld AS IndexListingId,
                    wli.UniqueName AS ComponentUniqueName,
                    wsi.ISIN AS ComponentISIN,
                    wli.CurrencyISOCode AS ComponentCurrency,
                    w.Unit AS ComponentUnit,
                    rec.BasketChangeId
                FROM RecentBasketChange rec
                INNER JOIN Listing bli ON bli.InstrumentId = rec.BasketInstrumentId
                INNER JOIN Weight w ON w.BasketChangeId = rec.BasketChangeId
                INNER JOIN Listing wli ON wli.Id = w.ListingId
                INNER JOIN SecurityInstrument wsi ON wsi.Id = w.InstrumentId
                INNER JOIN BasketFlavourType bft ON bft.Id = rec.BasketFlavourTypeId
                -- Additional joins as needed...
                WHERE rec.BasketUniqueName IN ({inClause})
                ORDER BY rec.BasketUniqueName, bbilld";

            // Execute the query and capture raw data using an anonymous type.
            List<dynamic> rawData = await _dataAccess.RunQueryAsync(sql, record => new
            {
                BasketUniqueName = record["BasketUniqueName"].ToString(),
                ValidFrom = Convert.ToDateTime(record["ValidFrom"]),
                ValidTo = Convert.ToDateTime(record["ValidTo"]),
                Divisor = Convert.ToDecimal(record["Divisor"]),
                BasketCurrency = record["BasketCurrency"].ToString(),
                ComponentUniqueName = record["ComponentUniqueName"].ToString(),
                ComponentISIN = record["ComponentISIN"].ToString(),
                ComponentUnit = Convert.ToDecimal(record["ComponentUnit"])
            });

            // Create Basket objects from raw data, grouping by BasketUniqueName.
            Dictionary<string, Basket> baskets = new Dictionary<string, Basket>();
            foreach (var row in rawData)
            {
                if (!baskets.ContainsKey(row.BasketUniqueName))
                {
                    baskets[row.BasketUniqueName] = new Basket
                    {
                        BasketUniqueName = row.BasketUniqueName,
                        // Here we assume "Price" type; in practice this might come from config.
                        Type = "Price",
                        BasketCurrency = row.BasketCurrency,
                        // Using the same validity and divisor for both compositions for simplicity.
                        Pcf = new Composition { ValidFrom = row.ValidFrom, ValidTo = row.ValidTo, Divisor = row.Divisor },
                        Ecf = new Composition { ValidFrom = row.ValidFrom, ValidTo = row.ValidTo, Divisor = row.Divisor },
                        Acf = new Composition { ValidFrom = row.ValidFrom, ValidTo = row.ValidTo, Divisor = row.Divisor }
                    };
                }
                // Add each component (underlying instrument) to the basket.
                baskets[row.BasketUniqueName].Composition.Add(new Component
                {
                    UniqueName = row.ComponentUniqueName,
                    ISIN = row.ComponentISIN,
                    Unit = row.ComponentUnit
                });
            }
            return baskets;
        }
    }

    // Adapter for calling the ICOM REST service to get FX snapshots (used for FX conversion).
    public class ICOMAdapter
    {
        private readonly string _baseUrl;
        private readonly string _clientId;
        private readonly string _clientSecret;
        private readonly HttpClient _httpClient;
        public ICOMAdapter()
        {
            // Read the URL and credentials from configuration.
            _baseUrl = ConfigurationManager.AppSettings["ICOMRestUrl"] ?? "https://abc-rest.de.world.abc:10443";
            _clientId = ConfigurationManager.AppSettings["ICOMClientId"] ?? "";
            _clientSecret = ConfigurationManager.AppSettings["ICOMClientSecret"] ?? "";
            _httpClient = new HttpClient { BaseAddress = new Uri(_baseUrl) };
        }
        // Makes an asynchronous POST call to get FX reference data.
        public async Task<List<FXSnap>> GetRefDataByListAsync(List<string> instrumentIds, List<string> snapTypes)
        {
            // For example, if the identifier type is "uniqueName", pick an appropriate API path.
            string path = "/reference-data/fbi-unique-name";
            // Build a payload combining instrument IDs and their associated snap types.
            var payload = instrumentIds.Select((id, index) => new { instrumentId = id, type = snapTypes.ElementAtOrDefault(index) }).ToList();
            string jsonPayload = JsonConvert.SerializeObject(payload);
            HttpResponseMessage response = await _httpClient.PostAsync(path,
                new StringContent(jsonPayload, Encoding.UTF8, "application/json"));
            response.EnsureSuccessStatusCode();
            string responseContent = await response.Content.ReadAsStringAsync();
            List<FXSnap> fxSnaps = JsonConvert.DeserializeObject<List<FXSnap>>(responseContent);
            return fxSnaps;
        }
    }

    // *******************************
    // Main Processing Engine
    // *******************************

    public class DipsReportProcessorEngine
    {
        // This method is the main engine that orchestrates the data retrieval and dividend point calculation.
        // It mimics the overall flow of the original Python code.
        public async Task ProcessReportsAsync()
        {
            // === Step 1: Retrieve Dividends ===
            DividendService dividendService = new DividendService();
            DateTime reportFrom = DateTime.Parse("2025-05-23");
            DateTime reportTo = DateTime.Parse("2025-05-25");
            List<Dividend> dividends = await dividendService.GetDividendsAsync(reportFrom, reportTo);

            // Group dividends by ex–date then by unique instrument.
            Dictionary<DateTime, Dictionary<string, List<Dividend>>> dividendDays = new Dictionary<DateTime, Dictionary<string, List<Dividend>>>();
            foreach (var div in dividends)
            {
                DateTime day = div.CorporateActionExDate;
                if (!dividendDays.ContainsKey(day))
                    dividendDays[day] = new Dictionary<string, List<Dividend>>();
                if (!dividendDays[day].ContainsKey(div.UniqueName))
                    dividendDays[day][div.UniqueName] = new List<Dividend>();
                dividendDays[day][div.UniqueName].Add(div);
            }

            // === Step 2: Retrieve Basket Data ===
            // Read basket configuration (here assumed to be a comma–separated list in the config).
            string basketsConfig = ConfigHelper.GetSetting("reporter.reports.DipsReport.baskets");
            List<string> basketUniqueNames = basketsConfig
                .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => s.Trim())
                .ToList();
            BasketService basketService = new BasketService();
            Dictionary<string, Basket> baskets = await basketService.GetBasketDataAsync(basketUniqueNames);

            // === Step 3: Get Country Settings ===
            Dictionary<string, CountrySetting> countrySettings = ConfigHelper.GetCountrySettings("reporter.reports.DipsReport.countries");

            // === Step 4: Retrieve FX and Currency Pair Data ===
            string currencyPairSql = @"
                SELECT 
                    cp.SourceCurrencyISOCode + cp.TargetCurrencyISOCode AS SourceTarget,
                    'FBILST' + RIGHT('0000000000' + CAST(li.Id AS VARCHAR), 10) AS ListingId,
                    li.CurrencyISOCode
                FROM CurrencyPair cp
                INNER JOIN Listing li ON li.InstrumentId = cp.Id
                WHERE cp.SourceCurrencyISOCode = 'EUR'
                  AND li.StatusId = 3";
            DataAccess dataAccess = new DataAccess();
            List<CurrencyPair> currencyPairs = await dataAccess.RunQueryAsync(currencyPairSql, record => new CurrencyPair
            {
                SourceTarget = record["SourceTarget"].ToString(),
                ListingId = record["ListingId"].ToString(),
                CurrencyISOCode = record["CurrencyISOCode"].ToString()
            });
            // Collect listing IDs from currency pairs.
            List<string> listingIds = currencyPairs.Select(cp => cp.ListingId).Distinct().ToList();
            // Assume snapTypes list is provided or empty.
            List<string> snapTypes = new List<string>();
            ICOMAdapter icomAdapter = new ICOMAdapter();
            List<FXSnap> fxSnaps = await icomAdapter.GetRefDataByListAsync(listingIds, snapTypes);
            // Build a dictionary for easy lookup: key = instrumentId, value = FX conversion value.
            Dictionary<string, decimal> fxSnapsDict = fxSnaps.ToDictionary(snap => snap.InstrumentId, snap => snap.Value);

            // === Step 5: Process Each Basket to Calculate Dividend Points ===
            bool force = false; // This flag can force processing even if the basket's validity dates don't match today.
            List<string> basketsNotValid = new List<string>();

            foreach (var basketEntry in baskets)
            {
                Basket basket = basketEntry.Value;
                // Check if today's date falls within the basket's valid period.
                if (basket.Pcf == null || basket.Ecf == null ||
                    !(basket.Pcf.ValidFrom < DateTime.Today && DateTime.Today <= basket.Ecf.ValidTo) && !force)
                {
                    basketsNotValid.Add(basket.BasketUniqueName);
                    continue;
                }

                Console.WriteLine("Working on basket: " + basket.BasketUniqueName);
                Console.WriteLine("Index type is: " + basket.Type);

                // Initialize a dip container for each dividend day.
                foreach (var day in dividendDays.Keys)
                {
                    if (!basket.DaysDips.ContainsKey(day))
                        basket.DaysDips[day] = new BasketDayDip { ShortDip = 0, LongDip = 0 };
                }

                // Determine the conversion factor. If the basket currency is not EUR, look up the FX rate.
                decimal euroToBasketCurrency = 1.0m;
                if (!basket.BasketCurrency.Equals("EUR", StringComparison.OrdinalIgnoreCase))
                {
                    string pairKey = "EUR" + basket.BasketCurrency;
                    CurrencyPair cp = currencyPairs.FirstOrDefault(c => c.SourceTarget == pairKey);
                    if (cp == null)
                        throw new KeyNotFoundException($"Currency pair {pairKey} does not exist.");
                    if (!fxSnapsDict.ContainsKey(cp.ListingId))
                        throw new KeyNotFoundException($"FX snap for listing id {cp.ListingId} not found.");
                    euroToBasketCurrency = fxSnapsDict[cp.ListingId];
                }

                // Process each component (instrument) in the basket.
                foreach (var comp in basket.Composition)
                {
                    // For each dividend day...
                    foreach (var day in dividendDays.Keys)
                    {
                        // If there are dividend records for the current instrument/component on that day.
                        if (dividendDays[day].ContainsKey(comp.UniqueName))
                        {
                            foreach (var div in dividendDays[day][comp.UniqueName])
                            {
                                decimal dividendAmount = 0.0m;
                                // Check if dividend type is not "specialcash".
                                // In some cases, you may have a configuration flag determining whether special dividends are considered.
                                bool considerSpecialDivs = false; // (Could be read from config.)
                                if (div.DividendType != "specialcash" || considerSpecialDivs)
                                {
                                    // Decide whether to use net dividend or gross dividend based on config.
                                    bool useNetDividend = false; // (Could be set per basket or MICCode.)
                                    if (useNetDividend)
                                        dividendAmount = div.DividendNetAmount != 0 ? div.DividendNetAmount : div.DividendGrossAmount;
                                    else
                                        dividendAmount = div.DividendGrossAmount != 0 ? div.DividendGrossAmount : div.DividendGrossAmount;
                                }

                                // Determine country–specific reinvest/receive factors.
                                // For example, if the instrument ISIN starts with "US", use the US factors.
                                string countryCode = comp.ISIN.Substring(0, 2).ToUpper();
                                CountrySetting cs;
                                if (!countrySettings.TryGetValue(countryCode, out cs))
                                {
                                    Console.Error.WriteLine($"Country '{countryCode}' wasn't found in config. Using default factors.");
                                    cs = new CountrySetting { Receive = 0.85m, Reinvest = 0.85m };
                                }
                                decimal receive = cs.Receive;   // e.g., 0.85 means 85% is received as cash.
                                decimal reinvest = cs.Reinvest;   // e.g., 0.70 means 70% is reinvested.

                                // --- Dividend Point Calculation ---
                                // For a Total Return (TR) index:
                                //   ShortDip calculation: Adjusts for cash dividend
                                //     Formula: (dividendAmount / fxConversion) * (1 - receive) * componentUnit
                                //   LongDip calculation: Adjusts for total return (reinvestment benefit)
                                //     Formula: (dividendAmount / fxConversion) * (receive - reinvest) * componentUnit
                                //
                                // For a Price index:
                                //     A simpler formula is used, here shortDip is dividendAmount * unit, while longDip scales with (receive - reinvest).
                                if (basket.Type.Equals("TR", StringComparison.OrdinalIgnoreCase))
                                {
                                    decimal shortDip = (dividendAmount / 1.0m) * (1 - receive) * comp.Unit;
                                    decimal longDip = (dividendAmount / 1.0m) * (receive - reinvest) * comp.Unit;
                                    basket.DaysDips[day].ShortDip += shortDip;
                                    basket.DaysDips[day].LongDip += longDip;
                                    Console.WriteLine($"TR Index: Component {comp.UniqueName} on {day:yyyy-MM-dd} short: {shortDip:F4}, long: {longDip:F4}");
                                }
                                else if (basket.Type.Equals("Price", StringComparison.OrdinalIgnoreCase))
                                {
                                    decimal shortDip = (dividendAmount / 1.0m) * comp.Unit;
                                    decimal longDip = (dividendAmount / 1.0m) * (receive - reinvest) * comp.Unit;
                                    basket.DaysDips[day].ShortDip += shortDip;
                                    basket.DaysDips[day].LongDip += longDip;
                                    Console.WriteLine($"Price Index: Component {comp.UniqueName} on {day:yyyy-MM-dd} short: {shortDip:F4}, long: {longDip:F4}");
                                }
                            }
                        }
                    }
                }

                // --- Final Adjustment for Each Basket Day ---
                // Once all computations are done, adjust the dips for each day by the FX conversion factor and the basket's divisor.
                foreach (var day in basket.DaysDips.Keys.ToList())
                {
                    basket.DaysDips[day].ShortDip = basket.DaysDips[day].ShortDip * euroToBasketCurrency / basket.Pcf.Divisor;
                    basket.DaysDips[day].LongDip = basket.DaysDips[day].LongDip * euroToBasketCurrency / basket.Ecf.Divisor;
                    Console.WriteLine($"{basket.BasketUniqueName}: {day:yyyy-MM-dd} dips [short]: {basket.DaysDips[day].ShortDip:F4}");
                    Console.WriteLine($"{basket.BasketUniqueName}: {day:yyyy-MM-dd} dips [long]: {basket.DaysDips[day].LongDip:F4}");
                }
            }
        }
    }

    // *******************************
    // Helper for Configuration
    // *******************************

    public static class ConfigHelper
    {
        // Retrieves a single configuration setting from App.config / Web.config.
        public static string GetSetting(string key)
        {
            return ConfigurationManager.AppSettings[key] ?? string.Empty;
        }
        // Retrieves the country settings configuration.
        // In this example, if the configuration key isn't found, we default to hardcoded values.
        public static Dictionary<string, CountrySetting> GetCountrySettings(string key)
        {
            string json = GetSetting(key);
            if (!string.IsNullOrWhiteSpace(json))
            {
                return JsonConvert.DeserializeObject<Dictionary<string, CountrySetting>>(json);
            }
            return new Dictionary<string, CountrySetting>
            {
                { "US", new CountrySetting { Receive = 0.85m, Reinvest = 0.70m } },
                { "HK", new CountrySetting { Receive = 0.90m, Reinvest = 1.00m } },
                { "SG", new CountrySetting { Receive = 1.00m, Reinvest = 1.00m } }
            };
        }
    }

    // *******************************
    // Program Entry Point
    // *******************************

    class Program
    {
        // Main method is asynchronous; notice the use of async/await.
        static async Task Main(string[] args)
        {
            try
            {
                DipsReportProcessorEngine engine = new DipsReportProcessorEngine();
                await engine.ProcessReportsAsync();
                Console.WriteLine("Report processing complete.");
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine("An error occurred: " + ex.Message);
            }
            // Pause the console for review.
            Console.ReadLine();
        }
    }
}
```

---

## Explanation of the Dividend Point Calculation and Key C# Features

1. **Dividend Point Calculation:**  
   - **Grouping by Ex-Date and Instrument:**  
     The dividends are first grouped by their **ex-date** (the date dividends are credited) and then by the unique instrument identifier. This grouping allows the code to later process each dividend event on a per–day basis.
   - **Component Loop:**  
     For each basket (index), and for each underlying component (instrument), the code checks if that component has any dividend records for that day.
   - **Choosing Dividend Value:**  
     The code chooses between the **gross** and **net** dividend amounts based on a configuration flag (`useNetDividend`).  
   - **Receive / Reinvest Factors:**  
     Each country (determined from the instrument’s ISIN) is assigned:
     - A **receive** factor, indicating the percentage of the dividend paid out as cash.
     - A **reinvest** factor, indicating the portion that is assumed to be reinvested in the index.
     - For a **Total Return (TR) index**, the short side of the dip is calculated using `(1 - receive)`, while the long side is calculated using `(receive - reinvest)`.  
     - For a **Price index**, a similar but simplified formula is applied.
   - **FX Conversion:**  
     If the basket’s base currency is different from EUR, an FX conversion factor is applied to convert the dividend amounts. This factor is retrieved from the ICOM REST service.
   - **Final Adjustment:**  
     The adjusted dividend amounts (short and long dips) are then scaled by a basket-specific **divisor**, which can be thought of as a normalizing factor to translate raw dividend amounts into index point adjustments.

2. **Key C# Language Features Demonstrated:**  
   - **Async/Await:**  
     The code uses the `async` keyword and `await` to handle asynchronous database queries and HTTP calls. This makes the code non–blocking and easier to read than traditional callback–based code.
   - **Using Statement:**  
     Used with `SqlConnection`, `SqlCommand`, and `HttpClient` to ensure that unmanaged resources are disposed of automatically.
   - **Lambda Expressions:**  
     In methods like `RunQueryAsync<T>`, lambda functions are used to map each data record to a model object (e.g., a `Dividend`).
   - **LINQ:**  
     The code uses LINQ (Language Integrated Query) for operations such as grouping, filtering, and selecting values from collections.
   - **String Interpolation and Formatting:**  
     Methods like `string.Format` and `$"..."` (string interpolation) are used for readability in constructing SQL queries and messages.
   - **Exception Handling:**  
     The `try/catch` block in `Main` shows basic error handling.
   - **Dictionary & Collection Usage:**  
     Dictionaries are used to group data (such as dividends by date) and for fast lookup (such as FX snap values by instrument ID).

By studying the inline comments and explanations, you should gain a deeper understanding of how the dividend point calculations are performed and how various C# features can be used to write clear, maintainable code for financial applications. Happy coding!


⚠️ **GitHub.com Fallback** ⚠️