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
}
}