// 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 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
{
///
/// Base class for strategies that integrate NT8 SDK components.
///
public abstract class NT8StrategyBase : Strategy, INT8ExecutionBridge
{
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;
private System.IO.StreamWriter _fileLog;
private readonly object _fileLock = new object();
#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; }
[NinjaScriptProperty]
[Display(Name = "Enable File Logging", GroupName = "Diagnostics", Order = 10)]
public bool EnableFileLogging { get; set; }
[NinjaScriptProperty]
[Display(Name = "Log Directory", GroupName = "Diagnostics", Order = 11)]
public string LogDirectory { get; set; }
[NinjaScriptProperty]
[Display(Name = "Enable Long Trades", GroupName = "Trade Direction", Order = 1)]
public bool EnableLongTrades { get; set; }
[NinjaScriptProperty]
[Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)]
public bool EnableShortTrades { get; set; }
#endregion
// INT8ExecutionBridge implementation
public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
{
if (stopTicks > 0)
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
if (targetTicks > 0)
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
EnterLong(quantity, signalName);
}
public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
{
if (stopTicks > 0)
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
if (targetTicks > 0)
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
EnterShort(quantity, signalName);
}
public void ExitLongManaged(string signalName)
{
ExitLong(signalName);
}
public void ExitShortManaged(string signalName)
{
ExitShort(signalName);
}
public void FlattenAll()
{
ExitLong("EmergencyFlatten");
ExitShort("EmergencyFlatten");
}
///
/// Create the SDK strategy instance.
///
protected abstract IStrategy CreateSdkStrategy();
///
/// Configure strategy-specific values after initialization.
///
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 = 50;
EnableSDK = true;
DailyLossLimit = 1000.0;
MaxTradeRisk = 200.0;
MaxOpenPositions = 3;
RiskPerTrade = 100.0;
MinContracts = 1;
MaxContracts = 10;
EnableKillSwitch = false;
EnableVerboseLogging = false;
EnableFileLogging = true;
LogDirectory = string.Empty;
EnableLongTrades = true;
EnableShortTrades = true;
_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()), NinjaTrader.Cbi.LogLevel.Error);
_sdkInitialized = false;
}
}
}
else if (State == State.Realtime)
{
InitFileLog();
WriteSessionHeader();
}
else if (State == State.Terminated)
{
WriteSessionFooter();
}
}
protected override void OnBarUpdate()
{
if (!_sdkInitialized || _sdkStrategy == null)
{
return;
}
if (CurrentBar < BarsRequiredToTrade)
{
return;
}
if (Time[0] == _lastBarTime)
return;
_lastBarTime = Time[0];
// Kill switch — checked AFTER bar guards so ExitLong/ExitShort are valid
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;
}
// 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()), NinjaTrader.Cbi.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;
FileLog(string.Format("FILL {0} {1} @ {2:F2} | OrderId={3}",
execution.MarketPosition,
execution.Quantity,
execution.Price,
execution.OrderId));
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
}
private void InitFileLog()
{
if (!EnableFileLogging)
return;
try
{
string dir = string.IsNullOrEmpty(LogDirectory)
? System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"NinjaTrader 8", "log", "nt8-sdk")
: LogDirectory;
System.IO.Directory.CreateDirectory(dir);
string path = System.IO.Path.Combine(
dir,
string.Format("session_{0}.log", DateTime.Now.ToString("yyyyMMdd_HHmmss")));
_fileLog = new System.IO.StreamWriter(path, false);
_fileLog.AutoFlush = true;
Print(string.Format("[NT8-SDK] File log started: {0}", path));
}
catch (Exception ex)
{
Print(string.Format("[NT8-SDK] Failed to open file log: {0}", ex.Message));
}
}
private void FileLog(string message)
{
if (_fileLog == null)
return;
lock (_fileLock)
{
try
{
_fileLog.WriteLine(string.Format("[{0:HH:mm:ss.fff}] {1}", DateTime.Now, message));
}
catch
{
}
}
}
private void WriteSessionHeader()
{
FileLog("=== SESSION START " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ===");
FileLog(string.Format("Strategy : {0}", Name));
FileLog(string.Format("Account : {0}", Account != null ? Account.Name : "N/A"));
FileLog(string.Format("Symbol : {0}", Instrument != null ? Instrument.FullName : "N/A"));
FileLog(string.Format("Risk : DailyLimit=${0} MaxTradeRisk=${1} RiskPerTrade=${2}",
DailyLossLimit,
MaxTradeRisk,
RiskPerTrade));
FileLog(string.Format("Sizing : MinContracts={0} MaxContracts={1}", MinContracts, MaxContracts));
FileLog(string.Format("VerboseLog : {0} FileLog: {1}", EnableVerboseLogging, EnableFileLogging));
FileLog("---");
}
private void WriteSessionFooter()
{
FileLog("---");
FileLog("=== SESSION END " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ===");
if (_fileLog != null)
{
lock (_fileLock)
{
try
{
_fileLog.Close();
}
catch
{
}
_fileLog = null;
}
}
}
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());
_strategyConfig = new StrategyConfig(
Name,
Instrument.MasterInstrument.Name,
new Dictionary(),
_riskConfig,
_sizingConfig);
_riskManager = new BasicRiskManager(_logger);
_positionSizer = new BasicPositionSizer(_logger);
_circuitBreaker = new ExecutionCircuitBreaker(
_logger,
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()
{
DateTime etTime;
try
{
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone);
}
catch
{
etTime = Time[0];
}
var customData = new Dictionary();
customData.Add("CurrentBar", CurrentBar);
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
customData.Add("OrdersToday", _ordersSubmittedToday);
return NT8DataConverter.ConvertContext(
Instrument.MasterInstrument.Name,
etTime,
BuildPositionInfo(),
BuildAccountInfo(),
BuildSessionInfo(),
customData);
}
private AccountInfo BuildAccountInfo()
{
double cashValue = 100000.0;
double buyingPower = 250000.0;
try
{
if (Account != null)
{
cashValue = Account.Get(AccountItem.CashValue, Currency.UsDollar);
buyingPower = Account.Get(AccountItem.BuyingPower, Currency.UsDollar);
}
}
catch (Exception ex)
{
Print(string.Format("[NT8-SDK] WARNING: Could not read live account balance, using defaults: {0}", ex.Message));
}
var accountInfo = NT8DataConverter.ConvertAccount(cashValue, buyingPower, 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()
{
DateTime etTime;
try
{
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone);
}
catch
{
etTime = Time[0];
}
var sessionStart = etTime.Date.AddHours(9).AddMinutes(30);
var sessionEnd = etTime.Date.AddHours(16);
var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5)
&& etTime.TimeOfDay < TimeSpan.FromHours(16.0);
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, isRth ? "RTH" : "ETH");
return _currentSession;
}
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
{
// Direction filter — checked before risk to avoid unnecessary processing
if (intent.Side == SdkOrderSide.Buy && !EnableLongTrades)
{
if (EnableVerboseLogging)
Print(string.Format("[SDK] Long trade filtered by direction setting: {0}", intent.Symbol));
return;
}
if (intent.Side == SdkOrderSide.Sell && !EnableShortTrades)
{
if (EnableVerboseLogging)
Print(string.Format("[SDK] Short trade filtered by direction setting: {0}", intent.Symbol));
return;
}
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 (State == State.Historical)
{
// Skip circuit breaker during backtest — wall-clock timeout is meaningless on historical data.
}
else 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, Guid.NewGuid().ToString("N").Substring(0, 12));
if (EnableFileLogging)
{
string grade = "N/A";
string score = "N/A";
string factors = string.Empty;
if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score"))
{
var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore;
if (cs != null)
{
grade = cs.Grade.ToString();
score = cs.WeightedScore.ToString("F3");
var sb = new System.Text.StringBuilder();
foreach (var f in cs.Factors)
sb.Append(string.Format("{0}={1:F2} ", f.Type, f.Score));
factors = sb.ToString().TrimEnd();
}
}
FileLog(string.Format("SIGNAL {0} | Grade={1} | Score={2}", intent.Side, grade, score));
if (!string.IsNullOrEmpty(factors))
FileLog(string.Format(" Factors: {0}", factors));
FileLog(string.Format("SUBMIT {0} {1} @ Market | Stop={2} Target={3}",
intent.Side,
request.Quantity,
intent.StopTicks,
intent.TargetTicks));
}
_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);
}
}
}