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:
@@ -10,4 +10,9 @@
|
||||
<ProjectReference Include="..\NT8.Core\NT8.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Strategies\**\*.cs" />
|
||||
<None Include="Strategies\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
59
src/NT8.Adapters/Strategies/MinimalTestStrategy.cs
Normal file
59
src/NT8.Adapters/Strategies/MinimalTestStrategy.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// File: MinimalTestStrategy.cs
|
||||
using System;
|
||||
using NinjaTrader.Cbi;
|
||||
using NinjaTrader.Data;
|
||||
using NinjaTrader.NinjaScript;
|
||||
using NinjaTrader.NinjaScript.Strategies;
|
||||
|
||||
namespace NinjaTrader.NinjaScript.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimal test strategy to validate NT8 integration and compilation.
|
||||
/// </summary>
|
||||
public class MinimalTestStrategy : Strategy
|
||||
{
|
||||
private int _barCount;
|
||||
|
||||
protected override void OnStateChange()
|
||||
{
|
||||
if (State == State.SetDefaults)
|
||||
{
|
||||
Name = "Minimal Test";
|
||||
Description = "Simple test strategy - logs bars only";
|
||||
Calculate = Calculate.OnBarClose;
|
||||
BarsRequiredToTrade = 1;
|
||||
}
|
||||
else if (State == State.DataLoaded)
|
||||
{
|
||||
_barCount = 0;
|
||||
Print("[MinimalTest] Strategy initialized");
|
||||
}
|
||||
else if (State == State.Terminated)
|
||||
{
|
||||
Print(string.Format("[MinimalTest] Strategy terminated. Processed {0} bars", _barCount));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBarUpdate()
|
||||
{
|
||||
if (CurrentBar < BarsRequiredToTrade)
|
||||
return;
|
||||
|
||||
_barCount++;
|
||||
|
||||
if (_barCount % 10 == 0)
|
||||
{
|
||||
Print(string.Format(
|
||||
"[MinimalTest] Bar {0}: {1} O={2:F2} H={3:F2} L={4:F2} C={5:F2} V={6}",
|
||||
CurrentBar,
|
||||
Time[0].ToString("HH:mm:ss"),
|
||||
Open[0],
|
||||
High[0],
|
||||
Low[0],
|
||||
Close[0],
|
||||
Volume[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
545
src/NT8.Adapters/Strategies/NT8StrategyBase.cs
Normal file
545
src/NT8.Adapters/Strategies/NT8StrategyBase.cs
Normal file
@@ -0,0 +1,545 @@
|
||||
// File: NT8StrategyBase.cs
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NinjaTrader.Cbi;
|
||||
using NinjaTrader.Data;
|
||||
using NinjaTrader.Gui;
|
||||
using NinjaTrader.Gui.Chart;
|
||||
using NinjaTrader.Gui.Tools;
|
||||
using NinjaTrader.NinjaScript;
|
||||
using NinjaTrader.NinjaScript.Indicators;
|
||||
using NinjaTrader.NinjaScript.Strategies;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NT8.Adapters.NinjaTrader;
|
||||
using NT8.Core.Common.Interfaces;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Execution;
|
||||
using NT8.Core.Logging;
|
||||
using NT8.Core.Risk;
|
||||
using NT8.Core.Sizing;
|
||||
using SdkPosition = NT8.Core.Common.Models.Position;
|
||||
using SdkOrderSide = NT8.Core.Common.Models.OrderSide;
|
||||
using SdkOrderType = NT8.Core.Common.Models.OrderType;
|
||||
using OmsOrderRequest = NT8.Core.OMS.OrderRequest;
|
||||
using OmsOrderSide = NT8.Core.OMS.OrderSide;
|
||||
using OmsOrderType = NT8.Core.OMS.OrderType;
|
||||
using OmsOrderState = NT8.Core.OMS.OrderState;
|
||||
using OmsOrderStatus = NT8.Core.OMS.OrderStatus;
|
||||
|
||||
namespace NinjaTrader.NinjaScript.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for strategies that integrate NT8 SDK components.
|
||||
/// </summary>
|
||||
public abstract class NT8StrategyBase : Strategy
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
|
||||
protected IStrategy _sdkStrategy;
|
||||
protected IRiskManager _riskManager;
|
||||
protected IPositionSizer _positionSizer;
|
||||
protected NT8ExecutionAdapter _executionAdapter;
|
||||
protected ILogger _logger;
|
||||
|
||||
protected StrategyConfig _strategyConfig;
|
||||
protected RiskConfig _riskConfig;
|
||||
protected SizingConfig _sizingConfig;
|
||||
|
||||
private bool _sdkInitialized;
|
||||
private AccountInfo _lastAccountInfo;
|
||||
private SdkPosition _lastPosition;
|
||||
private MarketSession _currentSession;
|
||||
private int _ordersSubmittedToday;
|
||||
private DateTime _lastBarTime;
|
||||
private bool _killSwitchTriggered;
|
||||
private ExecutionCircuitBreaker _circuitBreaker;
|
||||
|
||||
#region User-Configurable Properties
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Enable SDK", GroupName = "SDK", Order = 1)]
|
||||
public bool EnableSDK { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Daily Loss Limit", GroupName = "Risk", Order = 1)]
|
||||
public double DailyLossLimit { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Max Trade Risk", GroupName = "Risk", Order = 2)]
|
||||
public double MaxTradeRisk { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Max Positions", GroupName = "Risk", Order = 3)]
|
||||
public int MaxOpenPositions { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Risk Per Trade", GroupName = "Sizing", Order = 1)]
|
||||
public double RiskPerTrade { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Min Contracts", GroupName = "Sizing", Order = 2)]
|
||||
public int MinContracts { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Max Contracts", GroupName = "Sizing", Order = 3)]
|
||||
public int MaxContracts { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Kill Switch (Flatten + Stop)", GroupName = "Emergency Controls", Order = 1)]
|
||||
public bool EnableKillSwitch { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
|
||||
public bool EnableVerboseLogging { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Create the SDK strategy instance.
|
||||
/// </summary>
|
||||
protected abstract IStrategy CreateSdkStrategy();
|
||||
|
||||
/// <summary>
|
||||
/// Configure strategy-specific values after initialization.
|
||||
/// </summary>
|
||||
protected abstract void ConfigureStrategyParameters();
|
||||
|
||||
protected override void OnStateChange()
|
||||
{
|
||||
if (State == State.SetDefaults)
|
||||
{
|
||||
Description = "SDK-integrated strategy base";
|
||||
// Name intentionally not set - this is an abstract base class
|
||||
Calculate = Calculate.OnBarClose;
|
||||
EntriesPerDirection = 1;
|
||||
EntryHandling = EntryHandling.AllEntries;
|
||||
IsExitOnSessionCloseStrategy = true;
|
||||
ExitOnSessionCloseSeconds = 30;
|
||||
IsFillLimitOnTouch = false;
|
||||
MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;
|
||||
OrderFillResolution = OrderFillResolution.Standard;
|
||||
Slippage = 0;
|
||||
StartBehavior = StartBehavior.WaitUntilFlat;
|
||||
TimeInForce = TimeInForce.Gtc;
|
||||
TraceOrders = false;
|
||||
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
|
||||
StopTargetHandling = StopTargetHandling.PerEntryExecution;
|
||||
BarsRequiredToTrade = 20;
|
||||
|
||||
EnableSDK = true;
|
||||
DailyLossLimit = 1000.0;
|
||||
MaxTradeRisk = 200.0;
|
||||
MaxOpenPositions = 3;
|
||||
RiskPerTrade = 100.0;
|
||||
MinContracts = 1;
|
||||
MaxContracts = 10;
|
||||
EnableKillSwitch = false;
|
||||
EnableVerboseLogging = false;
|
||||
_killSwitchTriggered = false;
|
||||
}
|
||||
else if (State == State.DataLoaded)
|
||||
{
|
||||
if (EnableSDK)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeSdkComponents();
|
||||
_sdkInitialized = true;
|
||||
Print(string.Format("[SDK] {0} initialized successfully", Name));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Print(string.Format("[SDK ERROR] Initialization failed: {0}", ex.Message));
|
||||
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error);
|
||||
_sdkInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBarUpdate()
|
||||
{
|
||||
// Kill switch check — must be first
|
||||
if (EnableKillSwitch)
|
||||
{
|
||||
if (!_killSwitchTriggered)
|
||||
{
|
||||
_killSwitchTriggered = true;
|
||||
Print(string.Format("[SDK] KILL SWITCH ACTIVATED at {0} — flattening all positions.", Time[0]));
|
||||
try
|
||||
{
|
||||
ExitLong("KillSwitch");
|
||||
ExitShort("KillSwitch");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_sdkInitialized || _sdkStrategy == null)
|
||||
{
|
||||
if (CurrentBar == 0)
|
||||
Print(string.Format("[SDK] Not initialized: sdkInit={0}, strategy={1}", _sdkInitialized, _sdkStrategy != null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentBar < BarsRequiredToTrade)
|
||||
{
|
||||
if (CurrentBar == 0)
|
||||
Print(string.Format("[SDK] Waiting for bars: current={0}, required={1}", CurrentBar, BarsRequiredToTrade));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time[0] == _lastBarTime)
|
||||
return;
|
||||
|
||||
_lastBarTime = Time[0];
|
||||
|
||||
// Log first processable bar and every 100th bar.
|
||||
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
|
||||
{
|
||||
Print(string.Format("[SDK] Processing bar {0}: {1} O={2:F2} H={3:F2} L={4:F2} C={5:F2}",
|
||||
CurrentBar,
|
||||
Time[0].ToString("yyyy-MM-dd HH:mm"),
|
||||
Open[0],
|
||||
High[0],
|
||||
Low[0],
|
||||
Close[0]));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var barData = ConvertCurrentBar();
|
||||
var context = BuildStrategyContext();
|
||||
|
||||
StrategyIntent intent;
|
||||
lock (_lock)
|
||||
{
|
||||
intent = _sdkStrategy.OnBar(barData, context);
|
||||
}
|
||||
|
||||
if (intent != null)
|
||||
{
|
||||
Print(string.Format("[SDK] Intent generated: {0} {1} @ {2}", intent.Side, intent.Symbol, intent.EntryType));
|
||||
ProcessStrategyIntent(intent, context);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger != null)
|
||||
_logger.LogError("OnBarUpdate failed: {0}", ex.Message);
|
||||
|
||||
Print(string.Format("[SDK ERROR] OnBarUpdate: {0}", ex.Message));
|
||||
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnOrderUpdate(
|
||||
Order order,
|
||||
double limitPrice,
|
||||
double stopPrice,
|
||||
int quantity,
|
||||
int filled,
|
||||
double averageFillPrice,
|
||||
NinjaTrader.Cbi.OrderState orderState,
|
||||
DateTime time,
|
||||
ErrorCode errorCode,
|
||||
string nativeError)
|
||||
{
|
||||
if (!_sdkInitialized || _executionAdapter == null || order == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(order.Name) || !order.Name.StartsWith("SDK_"))
|
||||
return;
|
||||
|
||||
// Record NT8 rejections in circuit breaker
|
||||
if (orderState == NinjaTrader.Cbi.OrderState.Rejected && _circuitBreaker != null)
|
||||
{
|
||||
var reason = string.Format("{0} {1}", errorCode, nativeError ?? string.Empty);
|
||||
_circuitBreaker.RecordOrderRejection(reason);
|
||||
Print(string.Format("[SDK] Order rejected by NT8: {0}", reason));
|
||||
}
|
||||
|
||||
_executionAdapter.ProcessOrderUpdate(
|
||||
order.OrderId,
|
||||
order.Name,
|
||||
orderState.ToString(),
|
||||
filled,
|
||||
averageFillPrice,
|
||||
(int)errorCode,
|
||||
nativeError);
|
||||
}
|
||||
|
||||
protected override void OnExecutionUpdate(
|
||||
Execution execution,
|
||||
string executionId,
|
||||
double price,
|
||||
int quantity,
|
||||
MarketPosition marketPosition,
|
||||
string orderId,
|
||||
DateTime time)
|
||||
{
|
||||
if (!_sdkInitialized || _executionAdapter == null || execution == null || execution.Order == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
|
||||
return;
|
||||
|
||||
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
|
||||
}
|
||||
|
||||
private void InitializeSdkComponents()
|
||||
{
|
||||
_logger = new BasicLogger(Name);
|
||||
|
||||
Print(string.Format("[SDK] Initializing with: DailyLoss={0:C}, TradeRisk={1:C}, MaxPos={2}",
|
||||
DailyLossLimit,
|
||||
MaxTradeRisk,
|
||||
MaxOpenPositions));
|
||||
|
||||
_riskConfig = new RiskConfig(DailyLossLimit, MaxTradeRisk, MaxOpenPositions, true);
|
||||
_sizingConfig = new SizingConfig(
|
||||
SizingMethod.FixedDollarRisk,
|
||||
MinContracts,
|
||||
MaxContracts,
|
||||
RiskPerTrade,
|
||||
new Dictionary<string, object>());
|
||||
|
||||
_strategyConfig = new StrategyConfig(
|
||||
Name,
|
||||
Instrument.MasterInstrument.Name,
|
||||
new Dictionary<string, object>(),
|
||||
_riskConfig,
|
||||
_sizingConfig);
|
||||
|
||||
_riskManager = new BasicRiskManager(_logger);
|
||||
_positionSizer = new BasicPositionSizer(_logger);
|
||||
_circuitBreaker = new ExecutionCircuitBreaker(
|
||||
NullLogger<ExecutionCircuitBreaker>.Instance,
|
||||
failureThreshold: 3,
|
||||
timeout: TimeSpan.FromSeconds(30));
|
||||
_executionAdapter = new NT8ExecutionAdapter();
|
||||
|
||||
_sdkStrategy = CreateSdkStrategy();
|
||||
if (_sdkStrategy == null)
|
||||
throw new InvalidOperationException("CreateSdkStrategy returned null");
|
||||
|
||||
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
|
||||
ConfigureStrategyParameters();
|
||||
|
||||
_ordersSubmittedToday = 0;
|
||||
_lastBarTime = DateTime.MinValue;
|
||||
_lastAccountInfo = null;
|
||||
_lastPosition = null;
|
||||
_currentSession = null;
|
||||
}
|
||||
|
||||
private BarData ConvertCurrentBar()
|
||||
{
|
||||
return NT8DataConverter.ConvertBar(
|
||||
Instrument.MasterInstrument.Name,
|
||||
Time[0],
|
||||
Open[0],
|
||||
High[0],
|
||||
Low[0],
|
||||
Close[0],
|
||||
(long)Volume[0],
|
||||
(int)BarsPeriod.Value);
|
||||
}
|
||||
|
||||
private StrategyContext BuildStrategyContext()
|
||||
{
|
||||
var customData = new Dictionary<string, object>();
|
||||
customData.Add("CurrentBar", CurrentBar);
|
||||
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
|
||||
customData.Add("OrdersToday", _ordersSubmittedToday);
|
||||
|
||||
return NT8DataConverter.ConvertContext(
|
||||
Instrument.MasterInstrument.Name,
|
||||
Time[0],
|
||||
BuildPositionInfo(),
|
||||
BuildAccountInfo(),
|
||||
BuildSessionInfo(),
|
||||
customData);
|
||||
}
|
||||
|
||||
private AccountInfo BuildAccountInfo()
|
||||
{
|
||||
var accountInfo = NT8DataConverter.ConvertAccount(100000.0, 250000.0, 0.0, 0.0, DateTime.UtcNow);
|
||||
_lastAccountInfo = accountInfo;
|
||||
return accountInfo;
|
||||
}
|
||||
|
||||
private SdkPosition BuildPositionInfo()
|
||||
{
|
||||
var p = NT8DataConverter.ConvertPosition(
|
||||
Instrument.MasterInstrument.Name,
|
||||
Position.Quantity,
|
||||
Position.AveragePrice,
|
||||
0.0,
|
||||
0.0,
|
||||
DateTime.UtcNow);
|
||||
|
||||
_lastPosition = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
private MarketSession BuildSessionInfo()
|
||||
{
|
||||
if (_currentSession != null && _currentSession.SessionStart.Date == Time[0].Date)
|
||||
return _currentSession;
|
||||
|
||||
var sessionStart = Time[0].Date.AddHours(9).AddMinutes(30);
|
||||
var sessionEnd = Time[0].Date.AddHours(16);
|
||||
var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16;
|
||||
var sessionName = isRth ? "RTH" : "ETH";
|
||||
|
||||
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, sessionName);
|
||||
return _currentSession;
|
||||
}
|
||||
|
||||
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
|
||||
{
|
||||
if (EnableVerboseLogging)
|
||||
Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol));
|
||||
|
||||
var riskDecision = _riskManager.ValidateOrder(intent, context, _riskConfig);
|
||||
if (!riskDecision.Allow)
|
||||
{
|
||||
if (EnableVerboseLogging)
|
||||
Print(string.Format("[SDK] Risk REJECTED: {0}", riskDecision.RejectReason));
|
||||
if (_logger != null)
|
||||
_logger.LogWarning("Intent rejected by risk manager: {0}", riskDecision.RejectReason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnableVerboseLogging)
|
||||
Print(string.Format("[SDK] Risk approved"));
|
||||
|
||||
var sizingResult = _positionSizer.CalculateSize(intent, context, _sizingConfig);
|
||||
if (EnableVerboseLogging)
|
||||
{
|
||||
Print(string.Format("[SDK] Position size: {0} contracts (min={1}, max={2})",
|
||||
sizingResult.Contracts,
|
||||
MinContracts,
|
||||
MaxContracts));
|
||||
}
|
||||
|
||||
if (sizingResult.Contracts < MinContracts)
|
||||
{
|
||||
if (EnableVerboseLogging)
|
||||
Print(string.Format("[SDK] Size too small: {0} < {1}", sizingResult.Contracts, MinContracts));
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new OmsOrderRequest();
|
||||
request.Symbol = intent.Symbol;
|
||||
request.Side = MapOrderSide(intent.Side);
|
||||
request.Type = MapOrderType(intent.EntryType);
|
||||
request.Quantity = sizingResult.Contracts;
|
||||
request.LimitPrice = intent.LimitPrice.HasValue ? (decimal?)intent.LimitPrice.Value : null;
|
||||
request.StopPrice = null;
|
||||
|
||||
if (EnableVerboseLogging)
|
||||
{
|
||||
Print(string.Format("[SDK] Submitting order: {0} {1} {2} @ {3}",
|
||||
request.Side,
|
||||
request.Quantity,
|
||||
request.Symbol,
|
||||
request.Type));
|
||||
}
|
||||
|
||||
SubmitOrderToNT8(request, intent);
|
||||
_ordersSubmittedToday++;
|
||||
}
|
||||
|
||||
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
|
||||
{
|
||||
// Circuit breaker gate
|
||||
if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
|
||||
{
|
||||
var state = _circuitBreaker.GetState();
|
||||
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
|
||||
if (_logger != null)
|
||||
_logger.LogWarning("Circuit breaker blocked order: {0}", state.Reason);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks);
|
||||
_executionAdapter.SubmitOrder(request, orderName);
|
||||
|
||||
if (request.Side == OmsOrderSide.Buy)
|
||||
{
|
||||
if (request.Type == OmsOrderType.Market)
|
||||
EnterLong(request.Quantity, orderName);
|
||||
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
|
||||
EnterLongLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
|
||||
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
|
||||
EnterLongStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
|
||||
}
|
||||
else if (request.Side == OmsOrderSide.Sell)
|
||||
{
|
||||
if (request.Type == OmsOrderType.Market)
|
||||
EnterShort(request.Quantity, orderName);
|
||||
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
|
||||
EnterShortLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
|
||||
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
|
||||
EnterShortStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
|
||||
}
|
||||
|
||||
if (intent.StopTicks > 0)
|
||||
SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false);
|
||||
|
||||
if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0)
|
||||
SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value);
|
||||
|
||||
if (_circuitBreaker != null)
|
||||
_circuitBreaker.OnSuccess();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_circuitBreaker != null)
|
||||
_circuitBreaker.OnFailure();
|
||||
|
||||
Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
|
||||
if (_logger != null)
|
||||
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static OmsOrderSide MapOrderSide(SdkOrderSide side)
|
||||
{
|
||||
if (side == SdkOrderSide.Buy)
|
||||
return OmsOrderSide.Buy;
|
||||
return OmsOrderSide.Sell;
|
||||
}
|
||||
|
||||
private static OmsOrderType MapOrderType(SdkOrderType type)
|
||||
{
|
||||
if (type == SdkOrderType.Market)
|
||||
return OmsOrderType.Market;
|
||||
if (type == SdkOrderType.Limit)
|
||||
return OmsOrderType.Limit;
|
||||
if (type == SdkOrderType.StopLimit)
|
||||
return OmsOrderType.StopLimit;
|
||||
return OmsOrderType.StopMarket;
|
||||
}
|
||||
|
||||
protected OmsOrderStatus GetSdkOrderStatus(string orderName)
|
||||
{
|
||||
if (_executionAdapter == null)
|
||||
return null;
|
||||
return _executionAdapter.GetOrderStatus(orderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
src/NT8.Adapters/Strategies/SimpleORBNT8.cs
Normal file
112
src/NT8.Adapters/Strategies/SimpleORBNT8.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
// File: SimpleORBNT8.cs
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NinjaTrader.Cbi;
|
||||
using NinjaTrader.Data;
|
||||
using NinjaTrader.Gui;
|
||||
using NinjaTrader.Gui.Chart;
|
||||
using NinjaTrader.Gui.Tools;
|
||||
using NinjaTrader.NinjaScript;
|
||||
using NinjaTrader.NinjaScript.Indicators;
|
||||
using NinjaTrader.NinjaScript.Strategies;
|
||||
using NT8.Core.Common.Interfaces;
|
||||
using NT8.Strategies.Examples;
|
||||
using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy;
|
||||
|
||||
namespace NinjaTrader.NinjaScript.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple Opening Range Breakout strategy integrated with NT8 SDK.
|
||||
/// </summary>
|
||||
public class SimpleORBNT8 : NT8StrategyBase
|
||||
{
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)]
|
||||
[Range(5, 120)]
|
||||
public int OpeningRangeMinutes { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Std Dev Multiplier", GroupName = "ORB Strategy", Order = 2)]
|
||||
[Range(0.5, 3.0)]
|
||||
public double StdDevMultiplier { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Stop Loss Ticks", GroupName = "ORB Risk", Order = 1)]
|
||||
[Range(1, 50)]
|
||||
public int StopTicks { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)]
|
||||
[Range(1, 100)]
|
||||
public int TargetTicks { get; set; }
|
||||
|
||||
protected override void OnStateChange()
|
||||
{
|
||||
if (State == State.SetDefaults)
|
||||
{
|
||||
Name = "Simple ORB NT8";
|
||||
Description = "Opening Range Breakout with NT8 SDK integration";
|
||||
|
||||
OpeningRangeMinutes = 30;
|
||||
StdDevMultiplier = 1.0;
|
||||
StopTicks = 8;
|
||||
TargetTicks = 16;
|
||||
|
||||
DailyLossLimit = 1000.0;
|
||||
MaxTradeRisk = 200.0;
|
||||
MaxOpenPositions = 1;
|
||||
RiskPerTrade = 100.0;
|
||||
MinContracts = 1;
|
||||
MaxContracts = 3;
|
||||
|
||||
Calculate = Calculate.OnBarClose;
|
||||
BarsRequiredToTrade = 50;
|
||||
}
|
||||
|
||||
base.OnStateChange();
|
||||
}
|
||||
|
||||
protected override IStrategy CreateSdkStrategy()
|
||||
{
|
||||
return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier);
|
||||
}
|
||||
|
||||
protected override void ConfigureStrategyParameters()
|
||||
{
|
||||
_strategyConfig.RiskSettings.DailyLossLimit = DailyLossLimit;
|
||||
_strategyConfig.RiskSettings.MaxTradeRisk = MaxTradeRisk;
|
||||
_strategyConfig.RiskSettings.MaxOpenPositions = MaxOpenPositions;
|
||||
|
||||
// Guard: Instrument may be null during strategy list loading
|
||||
if (Instrument != null && Instrument.MasterInstrument != null)
|
||||
{
|
||||
var pointValue = Instrument.MasterInstrument.PointValue;
|
||||
var tickSize = Instrument.MasterInstrument.TickSize;
|
||||
var dollarRisk = StopTicks * tickSize * pointValue;
|
||||
|
||||
if (dollarRisk > _strategyConfig.RiskSettings.MaxTradeRisk)
|
||||
_strategyConfig.RiskSettings.MaxTradeRisk = dollarRisk;
|
||||
}
|
||||
|
||||
_strategyConfig.SizingSettings.RiskPerTrade = RiskPerTrade;
|
||||
_strategyConfig.SizingSettings.MinContracts = MinContracts;
|
||||
_strategyConfig.SizingSettings.MaxContracts = MaxContracts;
|
||||
|
||||
_strategyConfig.Parameters["StopTicks"] = StopTicks;
|
||||
_strategyConfig.Parameters["TargetTicks"] = TargetTicks;
|
||||
_strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
|
||||
|
||||
if (_logger != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks",
|
||||
OpeningRangeMinutes,
|
||||
StopTicks,
|
||||
TargetTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user