using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; using NT8.Core.MarketData; namespace NT8.Core.Execution { /// /// Handles contract roll operations for futures and other expiring instruments /// public class ContractRollHandler { private readonly ILogger _logger; private readonly object _lock = new object(); // Store contract roll information private readonly Dictionary _rollInfo; // Store positions that need to be rolled private readonly Dictionary _positionsToRoll; /// /// Constructor for ContractRollHandler /// /// Logger instance public ContractRollHandler(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; _rollInfo = new Dictionary(); _positionsToRoll = new Dictionary(); } /// /// Checks if it's currently in a contract roll period /// /// Base symbol to check (e.g., ES) /// Date to check /// True if in roll period, false otherwise public bool IsRollPeriod(string symbol, DateTime date) { if (string.IsNullOrEmpty(symbol)) throw new ArgumentNullException("symbol"); try { lock (_lock) { if (_rollInfo.ContainsKey(symbol)) { var rollInfo = _rollInfo[symbol]; var daysUntilRoll = (rollInfo.RollDate - date.Date).Days; // Consider it rolling if within 5 days of roll date return daysUntilRoll <= 5 && daysUntilRoll >= 0; } return false; } } catch (Exception ex) { _logger.LogError("Failed to check roll period for {Symbol}: {Message}", symbol, ex.Message); throw; } } /// /// Gets the active contract for a base symbol on a given date /// /// Base symbol (e.g., ES) /// Date to get contract for /// Active contract symbol public string GetActiveContract(string baseSymbol, DateTime date) { if (string.IsNullOrEmpty(baseSymbol)) throw new ArgumentNullException("baseSymbol"); try { lock (_lock) { if (_rollInfo.ContainsKey(baseSymbol)) { var rollInfo = _rollInfo[baseSymbol]; // If we're past the roll date, return the next contract if (date.Date >= rollInfo.RollDate) { return rollInfo.NextContract; } else { return rollInfo.ActiveContract; } } // Default: just append date to base symbol (this would be configured externally in practice) return baseSymbol + date.ToString("yyMM"); } } catch (Exception ex) { _logger.LogError("Failed to get active contract for {Symbol}: {Message}", baseSymbol, ex.Message); throw; } } /// /// Determines if a position should be rolled /// /// Symbol of the position /// Position details /// Roll decision public RollDecision ShouldRollPosition(string symbol, OMS.OrderStatus position) { if (string.IsNullOrEmpty(symbol)) throw new ArgumentNullException("symbol"); if (position == null) throw new ArgumentNullException("position"); try { lock (_lock) { var baseSymbol = ExtractBaseSymbol(symbol); if (_rollInfo.ContainsKey(baseSymbol)) { var rollInfo = _rollInfo[baseSymbol]; var daysToRoll = rollInfo.DaysToRoll; // Roll if we're within 3 days of roll date and position has quantity if (daysToRoll <= 3 && position.RemainingQuantity > 0) { return new RollDecision( true, String.Format("Roll needed in {0} days", daysToRoll), RollReason.ImminentExpiration ); } else if (daysToRoll <= 7 && position.RemainingQuantity > 0) { return new RollDecision( true, String.Format("Roll recommended in {0} days", daysToRoll), RollReason.ApproachingExpiration ); } } return new RollDecision( false, "No roll needed", RollReason.None ); } } catch (Exception ex) { _logger.LogError("Failed to determine roll decision for {Symbol}: {Message}", symbol, ex.Message); throw; } } /// /// Initiates a contract rollover from one contract to another /// /// Contract to roll from /// Contract to roll to public void InitiateRollover(string fromContract, string toContract) { if (string.IsNullOrEmpty(fromContract)) throw new ArgumentNullException("fromContract"); if (string.IsNullOrEmpty(toContract)) throw new ArgumentNullException("toContract"); try { lock (_lock) { // Find positions in the from contract that need to be rolled var positionsToClose = new List(); foreach (var kvp in _positionsToRoll) { if (kvp.Value.Symbol == fromContract && kvp.Value.State == OMS.OrderState.Working) { positionsToClose.Add(kvp.Value); } } // Close positions in old contract foreach (var position in positionsToClose) { // In a real implementation, this would submit close orders for the old contract _logger.LogInformation("Initiating rollover: closing position in {FromContract}, size {Size}", fromContract, position.RemainingQuantity); } // In a real implementation, this would establish new positions in the toContract _logger.LogInformation("Rollover initiated from {FromContract} to {ToContract}", fromContract, toContract); } } catch (Exception ex) { _logger.LogError("Failed to initiate rollover from {FromContract} to {ToContract}: {Message}", fromContract, toContract, ex.Message); throw; } } /// /// Sets contract roll information for a symbol /// /// Base symbol (e.g., ES) /// Current active contract (e.g., ESZ24) /// Next contract to roll to (e.g., ESH25) /// Date of the roll public void SetRollInfo(string baseSymbol, string activeContract, string nextContract, DateTime rollDate) { if (string.IsNullOrEmpty(baseSymbol)) throw new ArgumentNullException("baseSymbol"); if (string.IsNullOrEmpty(activeContract)) throw new ArgumentNullException("activeContract"); if (string.IsNullOrEmpty(nextContract)) throw new ArgumentNullException("nextContract"); try { lock (_lock) { var daysToRoll = (rollDate.Date - DateTime.UtcNow.Date).Days; var isRollPeriod = daysToRoll <= 5 && daysToRoll >= 0; var rollInfo = new ContractRollInfo( baseSymbol, activeContract, nextContract, rollDate, daysToRoll, isRollPeriod ); _rollInfo[baseSymbol] = rollInfo; _logger.LogDebug("Set roll info for {Symbol}: {ActiveContract} -> {NextContract} on {RollDate}", baseSymbol, activeContract, nextContract, rollDate); } } catch (Exception ex) { _logger.LogError("Failed to set roll info for {Symbol}: {Message}", baseSymbol, ex.Message); throw; } } /// /// Adds a position that should be monitored for rolling /// /// Position to monitor public void MonitorPositionForRoll(OMS.OrderStatus position) { if (position == null) throw new ArgumentNullException("position"); try { lock (_lock) { var key = position.OrderId; _positionsToRoll[key] = position; _logger.LogDebug("Added position {OrderId} for roll monitoring", position.OrderId); } } catch (Exception ex) { _logger.LogError("Failed to monitor position for roll: {Message}", ex.Message); throw; } } /// /// Removes a position from roll monitoring /// /// Order ID of position to remove public void RemovePositionFromRollMonitoring(string orderId) { if (string.IsNullOrEmpty(orderId)) throw new ArgumentNullException("orderId"); try { lock (_lock) { _positionsToRoll.Remove(orderId); _logger.LogDebug("Removed position {OrderId} from roll monitoring", orderId); } } catch (Exception ex) { _logger.LogError("Failed to remove position from roll monitoring: {Message}", ex.Message); throw; } } /// /// Gets the roll information for a symbol /// /// Base symbol to get roll info for /// Contract roll information public ContractRollInfo GetRollInfo(string baseSymbol) { if (string.IsNullOrEmpty(baseSymbol)) throw new ArgumentNullException("baseSymbol"); try { lock (_lock) { ContractRollInfo info; _rollInfo.TryGetValue(baseSymbol, out info); return info; } } catch (Exception ex) { _logger.LogError("Failed to get roll info for {Symbol}: {Message}", baseSymbol, ex.Message); throw; } } /// /// Extracts the base symbol from a contract symbol /// /// Full contract symbol (e.g., ESZ24) /// Base symbol (e.g., ES) private string ExtractBaseSymbol(string contractSymbol) { if (string.IsNullOrEmpty(contractSymbol)) return string.Empty; // For now, extract letters from the beginning // In practice, this would be more sophisticated var baseSymbol = ""; foreach (char c in contractSymbol) { if (char.IsLetter(c)) baseSymbol += c; else break; } return baseSymbol; } } /// /// Decision regarding contract rolling /// public class RollDecision { /// /// Whether the position should be rolled /// public bool ShouldRoll { get; set; } /// /// Reason for the decision /// public string Reason { get; set; } /// /// Reason category /// public RollReason RollReason { get; set; } /// /// Constructor for RollDecision /// /// Whether to roll /// Reason for decision /// Category of reason public RollDecision(bool shouldRoll, string reason, RollReason rollReason) { ShouldRoll = shouldRoll; Reason = reason; RollReason = rollReason; } } /// /// Reason for contract roll /// public enum RollReason { /// /// No roll needed /// None = 0, /// /// Approaching expiration /// ApproachingExpiration = 1, /// /// Imminent expiration /// ImminentExpiration = 2, /// /// Better liquidity in next contract /// BetterLiquidity = 3, /// /// Scheduled roll /// Scheduled = 4 } }