Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
Some checks failed
Build and Test / build (push) Has been cancelled
Some checks failed
Build and Test / build (push) Has been cancelled
This commit is contained in:
365
src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs
Normal file
365
src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user