Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
2026-02-24 15:00:41 -05:00
parent 0e36fe5d23
commit a87152effb
50 changed files with 12849 additions and 752 deletions

View File

@@ -0,0 +1,365 @@
using System;
using System.Collections.Generic;
using NT8.Core.OMS;
namespace NT8.Adapters.NinjaTrader
{
/// <summary>
/// Adapter for executing orders through NinjaTrader 8 platform.
/// Bridges SDK order requests to NT8 order submission and handles callbacks.
/// Thread-safe for concurrent NT8 callbacks.
/// </summary>
public class NT8ExecutionAdapter
{
private readonly object _lock = new object();
private readonly Dictionary<string, OrderTrackingInfo> _orderTracking;
private readonly Dictionary<string, string> _nt8ToSdkOrderMap;
/// <summary>
/// Creates a new NT8 execution adapter.
/// </summary>
public NT8ExecutionAdapter()
{
_orderTracking = new Dictionary<string, OrderTrackingInfo>();
_nt8ToSdkOrderMap = new Dictionary<string, string>();
}
/// <summary>
/// Submit an order to NinjaTrader 8.
/// NOTE: This method tracks order state only. Actual NT8 submission is performed by strategy wrapper code.
/// </summary>
/// <param name="request">SDK order request.</param>
/// <param name="sdkOrderId">Unique SDK order ID.</param>
/// <returns>Tracking info for the submitted order.</returns>
/// <exception cref="ArgumentNullException">Thrown when request or sdkOrderId is invalid.</exception>
/// <exception cref="InvalidOperationException">Thrown when the same order ID is submitted twice.</exception>
public OrderTrackingInfo SubmitOrder(OrderRequest request, string sdkOrderId)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
throw new ArgumentNullException("sdkOrderId");
}
try
{
lock (_lock)
{
if (_orderTracking.ContainsKey(sdkOrderId))
{
throw new InvalidOperationException(string.Format("Order {0} already exists", sdkOrderId));
}
var trackingInfo = new OrderTrackingInfo();
trackingInfo.SdkOrderId = sdkOrderId;
trackingInfo.Nt8OrderId = null;
trackingInfo.OriginalRequest = request;
trackingInfo.CurrentState = OrderState.Pending;
trackingInfo.FilledQuantity = 0;
trackingInfo.AverageFillPrice = 0.0;
trackingInfo.LastUpdate = DateTime.UtcNow;
trackingInfo.ErrorMessage = null;
_orderTracking.Add(sdkOrderId, trackingInfo);
return trackingInfo;
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Process order update callback from NinjaTrader 8.
/// Called by NT8 strategy wrapper OnOrderUpdate.
/// </summary>
/// <param name="nt8OrderId">NT8 order ID.</param>
/// <param name="sdkOrderId">SDK order ID.</param>
/// <param name="orderState">NT8 order state string.</param>
/// <param name="filled">Filled quantity.</param>
/// <param name="averageFillPrice">Average fill price.</param>
/// <param name="errorCode">Error code if rejected.</param>
/// <param name="errorMessage">Error message if rejected.</param>
public void ProcessOrderUpdate(
string nt8OrderId,
string sdkOrderId,
string orderState,
int filled,
double averageFillPrice,
int errorCode,
string errorMessage)
{
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
return;
}
try
{
lock (_lock)
{
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return;
}
var info = _orderTracking[sdkOrderId];
if (!string.IsNullOrWhiteSpace(nt8OrderId) && info.Nt8OrderId == null)
{
info.Nt8OrderId = nt8OrderId;
_nt8ToSdkOrderMap[nt8OrderId] = sdkOrderId;
}
info.CurrentState = MapNT8OrderState(orderState);
info.FilledQuantity = filled;
info.AverageFillPrice = averageFillPrice;
info.LastUpdate = DateTime.UtcNow;
if (errorCode != 0 && !string.IsNullOrWhiteSpace(errorMessage))
{
info.ErrorMessage = string.Format("[{0}] {1}", errorCode, errorMessage);
info.CurrentState = OrderState.Rejected;
}
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Process execution callback from NinjaTrader 8.
/// Called by NT8 strategy wrapper OnExecutionUpdate.
/// </summary>
/// <param name="nt8OrderId">NT8 order ID.</param>
/// <param name="executionId">Execution identifier.</param>
/// <param name="price">Execution price.</param>
/// <param name="quantity">Execution quantity.</param>
/// <param name="time">Execution time.</param>
public void ProcessExecution(
string nt8OrderId,
string executionId,
double price,
int quantity,
DateTime time)
{
if (string.IsNullOrWhiteSpace(nt8OrderId))
{
return;
}
try
{
lock (_lock)
{
if (!_nt8ToSdkOrderMap.ContainsKey(nt8OrderId))
{
return;
}
var sdkOrderId = _nt8ToSdkOrderMap[nt8OrderId];
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return;
}
var info = _orderTracking[sdkOrderId];
info.LastUpdate = time;
if (info.FilledQuantity >= info.OriginalRequest.Quantity)
{
info.CurrentState = OrderState.Filled;
}
else if (info.FilledQuantity > 0)
{
info.CurrentState = OrderState.PartiallyFilled;
}
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Request to cancel an order.
/// NOTE: Actual cancellation is performed by strategy wrapper code.
/// </summary>
/// <param name="sdkOrderId">SDK order ID to cancel.</param>
/// <returns>True when cancel request is accepted; otherwise false.</returns>
public bool CancelOrder(string sdkOrderId)
{
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
throw new ArgumentNullException("sdkOrderId");
}
try
{
lock (_lock)
{
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return false;
}
var info = _orderTracking[sdkOrderId];
if (info.CurrentState == OrderState.Filled ||
info.CurrentState == OrderState.Cancelled ||
info.CurrentState == OrderState.Rejected)
{
return false;
}
info.LastUpdate = DateTime.UtcNow;
return true;
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Get current status of an order.
/// </summary>
/// <param name="sdkOrderId">SDK order ID.</param>
/// <returns>Order status snapshot; null when not found.</returns>
public OrderStatus GetOrderStatus(string sdkOrderId)
{
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
return null;
}
try
{
lock (_lock)
{
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return null;
}
var info = _orderTracking[sdkOrderId];
var status = new OrderStatus();
status.OrderId = info.SdkOrderId;
status.Symbol = info.OriginalRequest.Symbol;
status.Side = info.OriginalRequest.Side;
status.Quantity = info.OriginalRequest.Quantity;
status.Type = info.OriginalRequest.Type;
status.State = info.CurrentState;
status.FilledQuantity = info.FilledQuantity;
status.AverageFillPrice = info.FilledQuantity > 0 ? (decimal)info.AverageFillPrice : 0m;
status.CreatedTime = info.LastUpdate;
status.FilledTime = info.FilledQuantity > 0 ? (DateTime?)info.LastUpdate : null;
return status;
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Maps NinjaTrader order state string to SDK order state.
/// </summary>
/// <param name="nt8State">NT8 order state string.</param>
/// <returns>Mapped SDK state.</returns>
private OrderState MapNT8OrderState(string nt8State)
{
if (string.IsNullOrWhiteSpace(nt8State))
{
return OrderState.Expired;
}
switch (nt8State.ToUpperInvariant())
{
case "ACCEPTED":
case "WORKING":
return OrderState.Working;
case "FILLED":
return OrderState.Filled;
case "PARTFILLED":
case "PARTIALLYFILLED":
return OrderState.PartiallyFilled;
case "CANCELLED":
case "CANCELED":
return OrderState.Cancelled;
case "REJECTED":
return OrderState.Rejected;
case "PENDINGCANCEL":
return OrderState.Working;
case "PENDINGCHANGE":
case "PENDINGSUBMIT":
return OrderState.Pending;
default:
return OrderState.Expired;
}
}
}
/// <summary>
/// Internal tracking information for orders managed by NT8ExecutionAdapter.
/// </summary>
public class OrderTrackingInfo
{
/// <summary>
/// SDK order identifier.
/// </summary>
public string SdkOrderId { get; set; }
/// <summary>
/// NinjaTrader order identifier.
/// </summary>
public string Nt8OrderId { get; set; }
/// <summary>
/// Original order request.
/// </summary>
public OrderRequest OriginalRequest { get; set; }
/// <summary>
/// Current SDK order state.
/// </summary>
public OrderState CurrentState { get; set; }
/// <summary>
/// Filled quantity.
/// </summary>
public int FilledQuantity { get; set; }
/// <summary>
/// Average fill price.
/// </summary>
public double AverageFillPrice { get; set; }
/// <summary>
/// Last update timestamp.
/// </summary>
public DateTime LastUpdate { get; set; }
/// <summary>
/// Last error message.
/// </summary>
public string ErrorMessage { get; set; }
}
}