Files
nt8-sdk/src/NT8.Core/Execution/TrailingStopManager.cs
2026-02-24 15:00:41 -05:00

412 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace NT8.Core.Execution
{
/// <summary>
/// Manages trailing stops for positions with various trailing methods
/// </summary>
public class TrailingStopManager
{
private readonly ILogger _logger;
private readonly object _lock = new object();
// Store trailing stop information for each order
private readonly Dictionary<string, TrailingStopInfo> _trailingStops;
/// <summary>
/// Constructor for TrailingStopManager
/// </summary>
/// <param name="logger">Logger instance</param>
public TrailingStopManager(ILogger<TrailingStopManager> logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
_trailingStops = new Dictionary<string, TrailingStopInfo>();
}
/// <summary>
/// Starts trailing a stop for an order/position
/// </summary>
/// <param name="orderId">Order ID</param>
/// <param name="position">Position information</param>
/// <param name="config">Trailing stop configuration</param>
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;
}
}
/// <summary>
/// Updates the trailing stop based on current market price
/// </summary>
/// <param name="orderId">Order ID</param>
/// <param name="currentPrice">Current market price</param>
/// <returns>New stop price if updated, null if not updated</returns>
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;
}
}
/// <summary>
/// Calculates the new stop price based on trailing stop type
/// </summary>
/// <param name="type">Type of trailing stop</param>
/// <param name="position">Position information</param>
/// <param name="marketPrice">Current market price</param>
/// <param name="config">Trailing stop configuration</param>
/// <returns>Calculated stop price</returns>
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;
}
}
/// <summary>
/// Checks if a position should be moved to breakeven
/// </summary>
/// <param name="position">Position to check</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="config">Auto-breakeven configuration</param>
/// <returns>True if should move to breakeven, false otherwise</returns>
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;
}
}
/// <summary>
/// Gets the current trailing stop price for an order
/// </summary>
/// <param name="orderId">Order ID to get stop for</param>
/// <returns>Current trailing stop price</returns>
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;
}
}
/// <summary>
/// Deactivates trailing for an order
/// </summary>
/// <param name="orderId">Order ID to deactivate</param>
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;
}
}
/// <summary>
/// Removes trailing stop tracking for an order
/// </summary>
/// <param name="orderId">Order ID to remove</param>
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;
}
}
/// <summary>
/// Calculates initial stop price based on configuration
/// </summary>
/// <param name="position">Position information</param>
/// <param name="config">Trailing stop configuration</param>
/// <returns>Initial stop price</returns>
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;
}
}
/// <summary>
/// Information about a trailing stop
/// </summary>
internal class TrailingStopInfo
{
/// <summary>
/// Order ID this trailing stop is for
/// </summary>
public string OrderId { get; set; }
/// <summary>
/// Position information
/// </summary>
public OMS.OrderStatus Position { get; set; }
/// <summary>
/// Trailing stop configuration
/// </summary>
public TrailingStopConfig Config { get; set; }
/// <summary>
/// Whether trailing is currently active
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// Last price that was tracked
/// </summary>
public decimal LastTrackedPrice { get; set; }
/// <summary>
/// Last calculated stop price
/// </summary>
public decimal LastCalculatedStop { get; set; }
/// <summary>
/// When trailing was started
/// </summary>
public DateTime StartTime { get; set; }
/// <summary>
/// When stop was last updated
/// </summary>
public DateTime LastUpdateTime { get; set; }
}
}