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!