412 lines
16 KiB
C#
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; }
|
|
}
|
|
}
|