366 lines
12 KiB
C#
366 lines
12 KiB
C#
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; }
|
|
}
|
|
}
|