using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; namespace NT8.Core.Execution { /// /// Manages trailing stops for positions with various trailing methods /// public class TrailingStopManager { private readonly ILogger _logger; private readonly object _lock = new object(); // Store trailing stop information for each order private readonly Dictionary _trailingStops; /// /// Constructor for TrailingStopManager /// /// Logger instance public TrailingStopManager(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; _trailingStops = new Dictionary(); } /// /// Starts trailing a stop for an order/position /// /// Order ID /// Position information /// Trailing stop configuration public void StartTrailing(string orderId, OMS.OrderStatus position, TrailingStopConfig config) { if (string.IsNullOrEmpty(orderId)) throw new ArgumentNullException("orderId"); if (position == null) throw new ArgumentNullException("position"); if (config == null) throw new ArgumentNullException("config"); try { lock (_lock) { var trailingStop = new TrailingStopInfo { OrderId = orderId, Position = position, Config = config, IsActive = true, LastTrackedPrice = position.AverageFillPrice, LastCalculatedStop = CalculateInitialStop(position, config), StartTime = DateTime.UtcNow }; _trailingStops[orderId] = trailingStop; _logger.LogDebug("Started trailing stop for {OrderId}, initial stop at {StopPrice}", orderId, trailingStop.LastCalculatedStop); } } catch (Exception ex) { _logger.LogError("Failed to start trailing stop for {OrderId}: {Message}", orderId, ex.Message); throw; } } /// /// Updates the trailing stop based on current market price /// /// Order ID /// Current market price /// New stop price if updated, null if not updated public decimal? UpdateTrailingStop(string orderId, decimal currentPrice) { if (string.IsNullOrEmpty(orderId)) throw new ArgumentNullException("orderId"); try { lock (_lock) { if (!_trailingStops.ContainsKey(orderId)) { _logger.LogWarning("No trailing stop found for {OrderId}", orderId); return null; } var trailingStop = _trailingStops[orderId]; if (!trailingStop.IsActive) { return null; } var newStopPrice = CalculateNewStopPrice(trailingStop.Config.Type, trailingStop.Position, currentPrice, trailingStop.Config); // Only update if the stop has improved (moved in favorable direction) var shouldUpdate = false; decimal updatedStop = trailingStop.LastCalculatedStop; if (trailingStop.Position.Side == OMS.OrderSide.Buy) { // For long positions, update if new stop is higher than previous if (newStopPrice > trailingStop.LastCalculatedStop) { shouldUpdate = true; updatedStop = newStopPrice; } } else // Sell/Short { // For short positions, update if new stop is lower than previous if (newStopPrice < trailingStop.LastCalculatedStop) { shouldUpdate = true; updatedStop = newStopPrice; } } if (shouldUpdate) { trailingStop.LastCalculatedStop = updatedStop; trailingStop.LastTrackedPrice = currentPrice; trailingStop.LastUpdateTime = DateTime.UtcNow; _logger.LogDebug("Updated trailing stop for {OrderId} to {StopPrice}", orderId, updatedStop); return updatedStop; } return null; } } catch (Exception ex) { _logger.LogError("Failed to update trailing stop for {OrderId}: {Message}", orderId, ex.Message); throw; } } /// /// Calculates the new stop price based on trailing stop type /// /// Type of trailing stop /// Position information /// Current market price /// Trailing stop configuration /// Calculated stop price public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice, TrailingStopConfig config = null) { if (position == null) throw new ArgumentNullException("position"); try { if (config == null) { config = new TrailingStopConfig(StopType.FixedTrailing, 8, 2m, true); } switch (type) { case StopType.FixedTrailing: { // Tick size is fixed here as a temporary default (ES/NQ standard). // TODO: provide symbol-specific tick size via configuration. var tickSize = 0.25m; var trailingTicks = config.TrailingAmountTicks > 0 ? config.TrailingAmountTicks : 8; var distance = trailingTicks * tickSize; return position.Side == OMS.OrderSide.Buy ? marketPrice - distance : marketPrice + distance; } case StopType.ATRTrailing: { // ATR is approximated here until live ATR is provided in config/context. var atrMultiplier = config.AtrMultiplier > 0 ? config.AtrMultiplier : 2.0m; var estimatedAtr = position.AverageFillPrice * 0.005m; var distance = atrMultiplier * estimatedAtr; return position.Side == OMS.OrderSide.Buy ? marketPrice - distance : marketPrice + distance; } case StopType.Chandelier: { // Chandelier approximation uses the same ATR proxy until bar history is wired in. var chanMultiplier = config.AtrMultiplier > 0 ? config.AtrMultiplier : 3.0m; var estimatedAtr = position.AverageFillPrice * 0.005m; var distance = chanMultiplier * estimatedAtr; return position.Side == OMS.OrderSide.Buy ? marketPrice - distance : marketPrice + distance; } case StopType.PercentageTrailing: // Percentage trailing: trail by percentage of current price var pctTrail = 0.02m; return position.Side == OMS.OrderSide.Buy ? marketPrice * (1 - pctTrail) : marketPrice * (1 + pctTrail); default: // Fixed trailing as fallback var tickSizeFallback = 0.25m; var ticks = config.TrailingAmountTicks > 0 ? config.TrailingAmountTicks : 8; return position.Side == OMS.OrderSide.Buy ? marketPrice - (ticks * tickSizeFallback) : marketPrice + (ticks * tickSizeFallback); } } catch (Exception ex) { _logger.LogError("Failed to calculate new stop price: {Message}", ex.Message); throw; } } /// /// Checks if a position should be moved to breakeven /// /// Position to check /// Current market price /// Auto-breakeven configuration /// True if should move to breakeven, false otherwise public bool ShouldMoveToBreakeven(OMS.OrderStatus position, decimal currentPrice, AutoBreakevenConfig config) { if (position == null) throw new ArgumentNullException("position"); if (config == null) throw new ArgumentNullException("config"); try { if (!config.Enabled) return false; // Calculate profit in ticks var profitPerContract = position.Side == OMS.OrderSide.Buy ? currentPrice - position.AverageFillPrice : position.AverageFillPrice - currentPrice; var tickSize = 0.25m; // Should be configurable per symbol var profitInTicks = (int)(profitPerContract / tickSize); return profitInTicks >= config.TicksToBreakeven; } catch (Exception ex) { _logger.LogError("Failed to check breakeven condition: {Message}", ex.Message); throw; } } /// /// Gets the current trailing stop price for an order /// /// Order ID to get stop for /// Current trailing stop price public decimal? GetCurrentStopPrice(string orderId) { if (string.IsNullOrEmpty(orderId)) throw new ArgumentNullException("orderId"); try { lock (_lock) { if (_trailingStops.ContainsKey(orderId)) { return _trailingStops[orderId].LastCalculatedStop; } return null; } } catch (Exception ex) { _logger.LogError("Failed to get current stop price for {OrderId}: {Message}", orderId, ex.Message); throw; } } /// /// Deactivates trailing for an order /// /// Order ID to deactivate public void DeactivateTrailing(string orderId) { if (string.IsNullOrEmpty(orderId)) throw new ArgumentNullException("orderId"); try { lock (_lock) { if (_trailingStops.ContainsKey(orderId)) { _trailingStops[orderId].IsActive = false; _logger.LogDebug("Deactivated trailing stop for {OrderId}", orderId); } } } catch (Exception ex) { _logger.LogError("Failed to deactivate trailing for {OrderId}: {Message}", orderId, ex.Message); throw; } } /// /// Removes trailing stop tracking for an order /// /// Order ID to remove public void RemoveTrailing(string orderId) { if (string.IsNullOrEmpty(orderId)) throw new ArgumentNullException("orderId"); try { lock (_lock) { if (_trailingStops.ContainsKey(orderId)) { _trailingStops.Remove(orderId); _logger.LogDebug("Removed trailing stop tracking for {OrderId}", orderId); } } } catch (Exception ex) { _logger.LogError("Failed to remove trailing for {OrderId}: {Message}", orderId, ex.Message); throw; } } /// /// Calculates initial stop price based on configuration /// /// Position information /// Trailing stop configuration /// Initial stop price private decimal CalculateInitialStop(OMS.OrderStatus position, TrailingStopConfig config) { if (position == null || config == null) return 0; var tickSize = 0.25m; // Should be configurable per symbol var initialDistance = config.TrailingAmountTicks * tickSize; return position.Side == OMS.OrderSide.Buy ? position.AverageFillPrice - initialDistance : position.AverageFillPrice + initialDistance; } } /// /// Information about a trailing stop /// internal class TrailingStopInfo { /// /// Order ID this trailing stop is for /// public string OrderId { get; set; } /// /// Position information /// public OMS.OrderStatus Position { get; set; } /// /// Trailing stop configuration /// public TrailingStopConfig Config { get; set; } /// /// Whether trailing is currently active /// public bool IsActive { get; set; } /// /// Last price that was tracked /// public decimal LastTrackedPrice { get; set; } /// /// Last calculated stop price /// public decimal LastCalculatedStop { get; set; } /// /// When trailing was started /// public DateTime StartTime { get; set; } /// /// When stop was last updated /// public DateTime LastUpdateTime { get; set; } } }