Files
nt8-sdk/PHASE_B_SPECIFICATION.md
2026-02-24 15:00:41 -05:00

38 KiB

Phase B: NT8 Strategy Base Class - Detailed Specification

For: Kilocode AI Agent (Autonomous Implementation)
Phase: Phase B - NT8 Strategy Integration
Components: NT8StrategyBase + SimpleORBNT8 + MinimalTestStrategy
Estimated Time: 4-5 hours
Mode: Code Mode
Dependencies: Phase A must be complete


🎯 Objective

Create the NinjaTrader 8 strategy base class that inherits from NT8's Strategy class and bridges the NT8 platform to our SDK. This is the critical integration layer that makes our SDK run inside NinjaTrader 8.

Key Challenge: These files CANNOT be compiled into a DLL. They must be deployed as .cs files directly to NinjaTrader's Strategies folder so NT8 can compile them with access to NinjaTrader.NinjaScript namespaces.


📋 Architecture Overview

Layer Relationships

┌─────────────────────────────────────────────────────────┐
│  NinjaTrader 8 Platform                                  │
│  - Strategy base class                                   │
│  - Order, Execution, Instrument objects                 │
│  - OnBarUpdate, OnOrderUpdate callbacks                 │
└───────────────────┬─────────────────────────────────────┘
                    │ Inherits
                    ↓
┌─────────────────────────────────────────────────────────┐
│  NT8StrategyBase.cs (THIS PHASE)                        │
│  - Inherits: NinjaTrader.NinjaScript.Strategies.Strategy│
│  - Implements: NT8 lifecycle methods                    │
│  - Creates: SDK components (Strategy, Risk, Sizing)     │
│  - Bridges: NT8 events → SDK → NT8 actions             │
└───────────────────┬─────────────────────────────────────┘
                    │ Uses
                    ↓
┌─────────────────────────────────────────────────────────┐
│  NT8.Core.dll + NT8.Adapters.dll                        │
│  - Already built in Phase A                             │
│  - Strategy, Risk, Sizing, Analytics                    │
│  - NT8ExecutionAdapter, NT8DataConverter                │
└─────────────────────────────────────────────────────────┘

Why .cs Files, Not DLLs?

NT8 Requirement:

  • NinjaTrader strategies MUST inherit from NinjaTrader.NinjaScript.Strategies.Strategy
  • This class is in NinjaTrader.Custom.dll which is only available at NT8 compile time
  • We cannot reference NT8 DLLs in our SDK project (circular dependency)
  • Therefore: Deploy as .cs source files to Documents\NinjaTrader 8\bin\Custom\Strategies\

Our Approach:

  • NT8StrategyBase.cs → Deployed to NT8 as source
  • SimpleORBNT8.cs → Deployed to NT8 as source
  • MinimalTestStrategy.cs → Deployed to NT8 as source
  • These files reference NT8.Core.dll (already in Custom\bin)

📦 Component 1: NT8StrategyBase.cs

Overview

Abstract base class that all SDK-integrated strategies inherit from. Handles the NT8 lifecycle and bridges NT8 events to SDK components.

Location

Create: src/NT8.Adapters/Strategies/NT8StrategyBase.cs
Deploy To: Documents\NinjaTrader 8\bin\Custom\Strategies\NT8StrategyBase.cs

File Structure

// Required NT8 namespaces
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Strategies;

// SDK namespaces
using System;
using System.Collections.Generic;
using NT8.Core.Common.Interfaces;
using NT8.Core.Common.Models;
using NT8.Core.Risk;
using NT8.Core.Sizing;
using NT8.Core.Logging;
using NT8.Core.OMS;
using NT8.Adapters.NinjaTrader;

namespace NinjaTrader.NinjaScript.Strategies
{
    /// <summary>
    /// Base class for strategies that integrate NT8 SDK.
    /// Handles NT8 lifecycle and bridges platform events to SDK components.
    /// 
    /// DEPLOYMENT NOTE: This file must be deployed as .cs source to:
    /// Documents\NinjaTrader 8\bin\Custom\Strategies\
    /// 
    /// Do NOT compile into NT8.Adapters.dll - NT8 must compile it.
    /// </summary>
    public abstract class NT8StrategyBase : Strategy
    {
        // Implementation below...
    }
}

Fields & Properties

#region Fields

private readonly object _lock = new object();

// SDK Components
protected IStrategy _sdkStrategy;
protected IRiskManager _riskManager;
protected IPositionSizer _positionSizer;
protected IOrderManager _orderManager;
protected NT8ExecutionAdapter _executionAdapter;
protected ILogger _logger;

// Configuration
protected StrategyConfig _strategyConfig;
protected RiskConfig _riskConfig;
protected SizingConfig _sizingConfig;

// State tracking
private bool _sdkInitialized;
private AccountInfo _lastAccountInfo;
private Position _lastPosition;
private MarketSession _currentSession;
private int _ordersSubmittedToday;
private DateTime _lastBarTime;

#endregion

#region User-Configurable Properties

/// <summary>
/// Enable SDK integration. Set to false to disable SDK and run NT8-only logic.
/// </summary>
[NinjaScriptProperty]
[Display(Name = "Enable SDK", GroupName = "SDK", Order = 1)]
public bool EnableSDK { get; set; }

/// <summary>
/// Maximum daily loss limit in dollars.
/// </summary>
[NinjaScriptProperty]
[Display(Name = "Daily Loss Limit", GroupName = "Risk", Order = 1)]
public double DailyLossLimit { get; set; }

/// <summary>
/// Maximum risk per trade in dollars.
/// </summary>
[NinjaScriptProperty]
[Display(Name = "Max Trade Risk", GroupName = "Risk", Order = 2)]
public double MaxTradeRisk { get; set; }

/// <summary>
/// Maximum open positions allowed.
/// </summary>
[NinjaScriptProperty]
[Display(Name = "Max Positions", GroupName = "Risk", Order = 3)]
public int MaxOpenPositions { get; set; }

/// <summary>
/// Risk per trade in dollars for position sizing.
/// </summary>
[NinjaScriptProperty]
[Display(Name = "Risk Per Trade", GroupName = "Sizing", Order = 1)]
public double RiskPerTrade { get; set; }

/// <summary>
/// Minimum contracts (position size floor).
/// </summary>
[NinjaScriptProperty]
[Display(Name = "Min Contracts", GroupName = "Sizing", Order = 2)]
public int MinContracts { get; set; }

/// <summary>
/// Maximum contracts (position size ceiling).
/// </summary>
[NinjaScriptProperty]
[Display(Name = "Max Contracts", GroupName = "Sizing", Order = 3)]
public int MaxContracts { get; set; }

#endregion

Abstract Methods

#region Abstract Methods - Derived Strategies Must Implement

/// <summary>
/// Create the SDK strategy instance.
/// Called during State.DataLoaded.
/// </summary>
/// <returns>SDK strategy implementation</returns>
protected abstract IStrategy CreateSdkStrategy();

/// <summary>
/// Configure strategy-specific parameters.
/// Called after SDK components are initialized.
/// Override to set custom risk/sizing parameters.
/// </summary>
protected abstract void ConfigureStrategyParameters();

#endregion

NT8 Lifecycle: OnStateChange

#region NT8 Lifecycle

protected override void OnStateChange()
{
    if (State == State.SetDefaults)
    {
        // Set defaults for all strategies
        Description = "SDK-integrated strategy base";
        Name = "NT8 SDK Strategy Base";
        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;
        
        // SDK defaults
        EnableSDK = true;
        DailyLossLimit = 1000.0;
        MaxTradeRisk = 200.0;
        MaxOpenPositions = 3;
        RiskPerTrade = 100.0;
        MinContracts = 1;
        MaxContracts = 10;
    }
    else if (State == State.Configure)
    {
        // Add any additional data series here if needed
        // Derived strategies can override and add their own
    }
    else if (State == State.DataLoaded)
    {
        // Initialize SDK components
        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;
            }
        }
        else
        {
            Print(string.Format("[SDK] SDK disabled for {0}", Name));
            _sdkInitialized = false;
        }
    }
    else if (State == State.Terminated)
    {
        // Cleanup
        if (_sdkInitialized)
        {
            Print(string.Format("[SDK] {0} terminated", Name));
        }
    }
}

#endregion

SDK Initialization

#region SDK Initialization

/// <summary>
/// Initialize all SDK components.
/// Called during State.DataLoaded.
/// </summary>
private void InitializeSdkComponents()
{
    // 1. Create logger
    _logger = new BasicLogger(Name);
    _logger.LogInformation("Initializing SDK components...");
    
    // 2. Create risk configuration
    _riskConfig = new RiskConfig(
        dailyLossLimit: DailyLossLimit,
        maxTradeRisk: MaxTradeRisk,
        maxOpenPositions: MaxOpenPositions,
        emergencyFlattenEnabled: true
    );
    
    // 3. Create sizing configuration
    _sizingConfig = new SizingConfig(
        method: SizingMethod.FixedDollarRisk,
        minContracts: MinContracts,
        maxContracts: MaxContracts,
        riskPerTrade: RiskPerTrade,
        methodParameters: new Dictionary<string, object>()
    );
    
    // 4. Create strategy configuration
    _strategyConfig = new StrategyConfig(
        name: Name,
        symbol: Instrument.MasterInstrument.Name,
        parameters: new Dictionary<string, object>(),
        riskSettings: _riskConfig,
        sizingSettings: _sizingConfig
    );
    
    // 5. Create SDK components
    _riskManager = new BasicRiskManager(_logger);
    _positionSizer = new BasicPositionSizer(_logger);
    _orderManager = new BasicOrderManager(_logger, null); // NT8 adapter set later
    _executionAdapter = new NT8ExecutionAdapter();
    
    // 6. Create SDK strategy
    _sdkStrategy = CreateSdkStrategy();
    if (_sdkStrategy == null)
    {
        throw new InvalidOperationException("CreateSdkStrategy returned null");
    }
    
    // 7. Initialize SDK strategy
    _sdkStrategy.Initialize(_strategyConfig, null, _logger);
    
    // 8. Let derived strategy configure parameters
    ConfigureStrategyParameters();
    
    // 9. Initialize state tracking
    _ordersSubmittedToday = 0;
    _lastBarTime = DateTime.MinValue;
    _lastAccountInfo = null;
    _lastPosition = null;
    _currentSession = null;
    
    _logger.LogInformation("SDK initialization complete");
}

#endregion

NT8 Bar Update Handler

#region Bar Update Handler

protected override void OnBarUpdate()
{
    // Skip if SDK not initialized
    if (!_sdkInitialized || _sdkStrategy == null)
        return;
    
    // Skip if not enough bars
    if (CurrentBar < BarsRequiredToTrade)
        return;
    
    // Prevent processing same bar twice
    if (Time[0] == _lastBarTime)
        return;
    
    _lastBarTime = Time[0];
    
    try
    {
        // 1. Convert NT8 bar to SDK format
        var barData = ConvertCurrentBar();
        
        // 2. Build strategy context
        var context = BuildStrategyContext();
        
        // 3. Call SDK strategy
        StrategyIntent intent = null;
        lock (_lock)
        {
            intent = _sdkStrategy.OnBar(barData, context);
        }
        
        // 4. Process intent if generated
        if (intent != null)
        {
            ProcessStrategyIntent(intent, context);
        }
    }
    catch (Exception ex)
    {
        _logger.LogError("OnBarUpdate failed: {0}", ex.Message);
        Print(string.Format("[SDK ERROR] OnBarUpdate: {0}", ex.Message));
        
        // Log full stack trace to NT8 log
        Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error);
    }
}

/// <summary>
/// Convert current NT8 bar to SDK BarData.
/// </summary>
private BarData ConvertCurrentBar()
{
    var symbol = Instrument.MasterInstrument.Name;
    var time = Time[0];
    var open = Open[0];
    var high = High[0];
    var low = Low[0];
    var close = Close[0];
    var volume = Volume[0];
    var barSizeMinutes = (int)BarsPeriod.Value;
    
    return NT8DataConverter.ConvertBar(
        symbol, time, open, high, low, close, volume, barSizeMinutes);
}

/// <summary>
/// Build SDK StrategyContext from current NT8 state.
/// </summary>
private StrategyContext BuildStrategyContext()
{
    var symbol = Instrument.MasterInstrument.Name;
    var currentTime = Time[0];
    
    // Build account info
    var account = BuildAccountInfo();
    
    // Build position info
    var position = BuildPositionInfo();
    
    // Build session info
    var session = BuildSessionInfo();
    
    // Custom data (can be extended by derived strategies)
    var customData = new Dictionary<string, object>
    {
        { "CurrentBar", CurrentBar },
        { "BarsRequiredToTrade", BarsRequiredToTrade },
        { "OrdersToday", _ordersSubmittedToday }
    };
    
    return NT8DataConverter.ConvertContext(
        symbol, currentTime, position, account, session, customData);
}

/// <summary>
/// Build AccountInfo from NT8 account data.
/// </summary>
private AccountInfo BuildAccountInfo()
{
    if (Account == null)
    {
        // Return default account if not available
        return new AccountInfo(100000, 250000, 0, 0, DateTime.UtcNow);
    }
    
    var equity = Account.Get(AccountItem.CashValue, Currency.UsDollar);
    var buyingPower = Account.Get(AccountItem.BuyingPower, Currency.UsDollar);
    var dailyPnL = Account.Get(AccountItem.RealizedProfitLoss, Currency.UsDollar);
    
    // Calculate max drawdown from system performance
    var maxDrawdown = 0.0;
    if (SystemPerformance != null && SystemPerformance.AllTrades.Count > 0)
    {
        var perf = SystemPerformance.AllTrades.TradesPerformance;
        if (perf != null)
        {
            maxDrawdown = perf.Currency.DrawDown.Maximum;
        }
    }
    
    var accountInfo = NT8DataConverter.ConvertAccount(
        equity, buyingPower, dailyPnL, maxDrawdown, DateTime.UtcNow);
    
    _lastAccountInfo = accountInfo;
    return accountInfo;
}

/// <summary>
/// Build Position from NT8 position data.
/// </summary>
private Position BuildPositionInfo()
{
    var symbol = Instrument.MasterInstrument.Name;
    
    // Get current position from NT8
    var quantity = Position.Quantity;
    var avgPrice = Position.AveragePrice;
    var unrealizedPnL = Position.GetUnrealizedProfitLoss(PerformanceUnit.Currency);
    
    // Get realized P&L from performance
    var realizedPnL = 0.0;
    if (SystemPerformance != null && SystemPerformance.AllTrades.Count > 0)
    {
        realizedPnL = SystemPerformance.AllTrades.TradesPerformance.Currency.CumProfit;
    }
    
    var position = NT8DataConverter.ConvertPosition(
        symbol, quantity, avgPrice, unrealizedPnL, realizedPnL, DateTime.UtcNow);
    
    _lastPosition = position;
    return position;
}

/// <summary>
/// Build MarketSession from NT8 session data.
/// </summary>
private MarketSession BuildSessionInfo()
{
    // Use cached session if same day
    if (_currentSession != null && 
        _currentSession.SessionStart.Date == Time[0].Date)
    {
        return _currentSession;
    }
    
    // Build new session
    // Note: NT8 doesn't provide direct session access, so we estimate
    var sessionStart = Time[0].Date.AddHours(9).AddMinutes(30); // 9:30 AM
    var sessionEnd = Time[0].Date.AddHours(16); // 4:00 PM
    var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16;
    var sessionName = isRth ? "RTH" : "ETH";
    
    _currentSession = NT8DataConverter.ConvertSession(
        sessionStart, sessionEnd, isRth, sessionName);
    
    return _currentSession;
}

#endregion

Intent Processing

#region Intent Processing

/// <summary>
/// Process strategy intent through risk, sizing, and execution.
/// </summary>
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
{
    try
    {
        // 1. Risk validation
        var riskDecision = _riskManager.ValidateOrder(intent, context, _riskConfig);
        
        if (!riskDecision.Approved)
        {
            _logger.LogWarning("Intent rejected by risk manager: {0}", riskDecision.Reason);
            Print(string.Format("[SDK] Trade rejected: {0}", riskDecision.Reason));
            return;
        }
        
        // 2. Position sizing
        var sizingResult = _positionSizer.CalculateSize(intent, context, _sizingConfig);
        
        if (sizingResult.Contracts < MinContracts)
        {
            _logger.LogWarning("Position size too small: {0} contracts", sizingResult.Contracts);
            Print(string.Format("[SDK] Position size too small: {0}", sizingResult.Contracts));
            return;
        }
        
        // 3. Create order request
        var orderRequest = new OrderRequest(
            symbol: intent.Symbol,
            side: intent.Side,
            quantity: sizingResult.Contracts,
            type: intent.EntryType,
            limitPrice: intent.LimitPrice,
            stopPrice: intent.StopPrice
        );
        
        // 4. Submit to NT8
        SubmitOrderToNT8(orderRequest, intent);
        
        _ordersSubmittedToday++;
        
    }
    catch (Exception ex)
    {
        _logger.LogError("Failed to process intent: {0}", ex.Message);
        Print(string.Format("[SDK ERROR] Intent processing: {0}", ex.Message));
        Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error);
    }
}

/// <summary>
/// Submit order to NinjaTrader 8.
/// </summary>
private void SubmitOrderToNT8(OrderRequest request, StrategyIntent intent)
{
    var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks);
    
    _logger.LogInformation("Submitting order: {0} {1} {2} @ {3}",
        request.Side, request.Quantity, request.Symbol, request.Type);
    
    Print(string.Format("[SDK] Submitting: {0} {1} {2}",
        request.Side, request.Quantity, request.Symbol));
    
    // Track order in execution adapter
    var trackingInfo = _executionAdapter.SubmitOrder(request, orderName);
    
    // Submit to NT8 based on side
    if (request.Side == OrderSide.Buy)
    {
        if (request.Type == OrderType.Market)
        {
            EnterLong(request.Quantity, orderName);
        }
        else if (request.Type == OrderType.Limit && request.LimitPrice.HasValue)
        {
            EnterLongLimit(request.LimitPrice.Value, request.Quantity, orderName);
        }
        else if (request.Type == OrderType.Stop && request.StopPrice.HasValue)
        {
            EnterLongStopMarket(request.StopPrice.Value, request.Quantity, orderName);
        }
    }
    else if (request.Side == OrderSide.Sell)
    {
        if (request.Type == OrderType.Market)
        {
            EnterShort(request.Quantity, orderName);
        }
        else if (request.Type == OrderType.Limit && request.LimitPrice.HasValue)
        {
            EnterShortLimit(request.LimitPrice.Value, request.Quantity, orderName);
        }
        else if (request.Type == OrderType.Stop && request.StopPrice.HasValue)
        {
            EnterShortStopMarket(request.StopPrice.Value, request.Quantity, orderName);
        }
    }
    
    // Set stops and targets if specified
    if (intent.StopTicks > 0)
    {
        SetStopLoss(orderName, CalculationMode.Ticks, intent.StopTicks, false);
    }
    
    if (intent.TargetTicks > 0)
    {
        SetProfitTarget(orderName, CalculationMode.Ticks, intent.TargetTicks);
    }
}

#endregion

NT8 Callback Handlers

#region NT8 Callbacks

protected override void OnOrderUpdate(
    Order order, double limitPrice, double stopPrice,
    int quantity, int filled, double averageFillPrice,
    OrderState orderState, DateTime time,
    ErrorCode errorCode, string nativeError)
{
    if (!_sdkInitialized || _executionAdapter == null)
        return;
    
    // Extract SDK order ID from order name
    var sdkOrderId = order.Name;
    if (string.IsNullOrEmpty(sdkOrderId) || !sdkOrderId.StartsWith("SDK_"))
        return; // Not our order
    
    try
    {
        // Map NT8 order state to string
        var nt8StateString = orderState.ToString();
        var errorCodeInt = (int)errorCode;
        
        // Process in execution adapter
        _executionAdapter.ProcessOrderUpdate(
            order.OrderId,
            sdkOrderId,
            nt8StateString,
            filled,
            averageFillPrice,
            errorCodeInt,
            nativeError
        );
        
        // Log significant state changes
        if (orderState == OrderState.Filled)
        {
            _logger.LogInformation("Order filled: {0} @ {1:F2}", sdkOrderId, averageFillPrice);
            Print(string.Format("[SDK] Filled: {0} @ {1:F2}", sdkOrderId, averageFillPrice));
        }
        else if (orderState == OrderState.Rejected)
        {
            _logger.LogError("Order rejected: {0} - {1}", sdkOrderId, nativeError);
            Print(string.Format("[SDK] Rejected: {0} - {1}", sdkOrderId, nativeError));
        }
        else if (orderState == OrderState.Cancelled)
        {
            _logger.LogInformation("Order cancelled: {0}", sdkOrderId);
            Print(string.Format("[SDK] Cancelled: {0}", sdkOrderId));
        }
    }
    catch (Exception ex)
    {
        _logger.LogError("OnOrderUpdate failed: {0}", ex.Message);
        Print(string.Format("[SDK ERROR] OnOrderUpdate: {0}", ex.Message));
    }
}

protected override void OnExecutionUpdate(
    Execution execution, string executionId,
    double price, int quantity,
    MarketPosition marketPosition, string orderId,
    DateTime time)
{
    if (!_sdkInitialized || _executionAdapter == null)
        return;
    
    // Check if this is our order
    if (execution.Order == null || !execution.Order.Name.StartsWith("SDK_"))
        return;
    
    try
    {
        // Process execution in adapter
        _executionAdapter.ProcessExecution(
            orderId,
            executionId,
            price,
            quantity,
            time
        );
        
        _logger.LogInformation("Execution: {0} {1} @ {2:F2}",
            quantity, execution.Order.Name, price);
            
        Print(string.Format("[SDK] Exec: {0} @ {1:F2}", quantity, price));
    }
    catch (Exception ex)
    {
        _logger.LogError("OnExecutionUpdate failed: {0}", ex.Message);
        Print(string.Format("[SDK ERROR] OnExecutionUpdate: {0}", ex.Message));
    }
}

#endregion

Helper Methods

#region Helper Methods

/// <summary>
/// Get current SDK order status.
/// </summary>
protected OrderStatus GetSdkOrderStatus(string orderName)
{
    if (_executionAdapter == null)
        return null;
    
    return _executionAdapter.GetOrderStatus(orderName);
}

/// <summary>
/// Cancel SDK order by name.
/// </summary>
protected bool CancelSdkOrder(string orderName)
{
    if (_executionAdapter == null)
        return false;
    
    var canCancel = _executionAdapter.CancelOrder(orderName);
    
    if (canCancel)
    {
        // Find NT8 order and cancel it
        foreach (var order in Account.Orders)
        {
            if (order.Name == orderName && order.OrderState == OrderState.Working)
            {
                CancelOrder(order);
                return true;
            }
        }
    }
    
    return false;
}

#endregion

📦 Component 2: SimpleORBNT8.cs

Overview

Concrete implementation of SimpleORB strategy for NinjaTrader 8.

Location

Create: src/NT8.Adapters/Strategies/SimpleORBNT8.cs
Deploy To: Documents\NinjaTrader 8\bin\Custom\Strategies\SimpleORBNT8.cs

Full Implementation

using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Strategies;
using System;
using System.Collections.Generic;
using NT8.Core.Common.Interfaces;
using NT8.Core.Common.Models;
using NT8.Strategies.Examples;

namespace NinjaTrader.NinjaScript.Strategies
{
    /// <summary>
    /// Simple Opening Range Breakout strategy integrated with NT8 SDK.
    /// 
    /// Strategy Logic:
    /// 1. Calculate opening range in first N minutes
    /// 2. Trade breakouts of this range
    /// 3. Use SDK risk management and position sizing
    /// 4. Track performance with SDK analytics
    /// 
    /// DEPLOYMENT: Copy to Documents\NinjaTrader 8\bin\Custom\Strategies\
    /// </summary>
    public class SimpleORBNT8 : NT8StrategyBase
    {
        #region User Parameters
        
        [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; }
        
        #endregion
        
        #region NT8 Lifecycle
        
        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                // Strategy identification
                Name = "Simple ORB NT8";
                Description = "Opening Range Breakout with NT8 SDK integration";
                
                // ORB parameters
                OpeningRangeMinutes = 30;
                StdDevMultiplier = 1.0;
                StopTicks = 8;
                TargetTicks = 16;
                
                // Risk parameters (from base class)
                DailyLossLimit = 1000.0;
                MaxTradeRisk = 200.0;
                MaxOpenPositions = 1; // ORB is single position strategy
                RiskPerTrade = 100.0;
                MinContracts = 1;
                MaxContracts = 3;
                
                // NT8 settings
                Calculate = Calculate.OnBarClose;
                BarsRequiredToTrade = 50; // Need enough bars for ORB calculation
            }
            
            // Call base implementation
            base.OnStateChange();
        }
        
        #endregion
        
        #region SDK Integration
        
        protected override IStrategy CreateSdkStrategy()
        {
            // Create SimpleORBStrategy from SDK
            return new SimpleORBStrategy(OpeningRangeMinutes, StdDevMultiplier);
        }
        
        protected override void ConfigureStrategyParameters()
        {
            // Configure risk settings specific to ORB
            _strategyConfig.RiskSettings.DailyLossLimit = DailyLossLimit;
            _strategyConfig.RiskSettings.MaxTradeRisk = MaxTradeRisk;
            _strategyConfig.RiskSettings.MaxOpenPositions = MaxOpenPositions;
            
            // Calculate max trade risk from stop ticks
            var tickValue = Instrument.MasterInstrument.PointValue;
            var tickSize = Instrument.MasterInstrument.TickSize;
            var dollarRisk = StopTicks * tickSize * tickValue;
            
            _strategyConfig.RiskSettings.MaxTradeRisk = Math.Max(dollarRisk, MaxTradeRisk);
            
            // Position sizing
            _strategyConfig.SizingSettings.RiskPerTrade = RiskPerTrade;
            _strategyConfig.SizingSettings.MinContracts = MinContracts;
            _strategyConfig.SizingSettings.MaxContracts = MaxContracts;
            
            // Add ORB-specific parameters to custom data
            _strategyConfig.Parameters["StopTicks"] = StopTicks;
            _strategyConfig.Parameters["TargetTicks"] = TargetTicks;
            _strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
            
            _logger.LogInformation("Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks",
                OpeningRangeMinutes, StopTicks, TargetTicks);
        }
        
        #endregion
    }
}

📦 Component 3: MinimalTestStrategy.cs

Overview

Simplest possible NT8 strategy to validate compilation and deployment. No SDK dependencies.

Location

Create: src/NT8.Adapters/Strategies/MinimalTestStrategy.cs
Deploy To: Documents\NinjaTrader 8\bin\Custom\Strategies\MinimalTestStrategy.cs

Full Implementation

using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Strategies;
using System;

namespace NinjaTrader.NinjaScript.Strategies
{
    /// <summary>
    /// Minimal test strategy to validate NT8 integration.
    /// Does NOT use SDK - just logs bar data.
    /// 
    /// Use this to verify:
    /// 1. NT8 can compile strategies
    /// 2. Strategies can be enabled on charts
    /// 3. Basic NT8 lifecycle works
    /// 
    /// DEPLOYMENT: Copy to Documents\NinjaTrader 8\bin\Custom\Strategies\
    /// </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++;
            
            // Log every 10th bar to avoid spam
            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]));
            }
        }
    }
}

Validation & Testing

Compilation Test Plan

Step 1: Deploy to NT8

# Copy files to NT8
$strategies = "C:\dev\nt8-sdk\src\NT8.Adapters\Strategies"
$nt8Strat = "$env:USERPROFILE\Documents\NinjaTrader 8\bin\Custom\Strategies"

Copy-Item "$strategies\NT8StrategyBase.cs" $nt8Strat -Force
Copy-Item "$strategies\SimpleORBNT8.cs" $nt8Strat -Force
Copy-Item "$strategies\MinimalTestStrategy.cs" $nt8Strat -Force

Step 2: Compile in NT8

  1. Open NinjaTrader 8
  2. Tools → NinjaScript Editor (F5)
  3. Compile → Compile All (F5)
  4. Verify: "Compilation succeeded"

Step 3: Test MinimalTestStrategy

  1. New → Strategy
  2. Select "Minimal Test"
  3. Enable on ES 5-minute chart
  4. Verify: Bars logged in Output window
  5. Run for 30 minutes
  6. Disable strategy
  7. Check for errors in Log

Step 4: Test SimpleORBNT8

  1. New → Strategy
  2. Select "Simple ORB NT8"
  3. Configure parameters:
    • OpeningRangeMinutes = 30
    • StopTicks = 8
    • TargetTicks = 16
    • DailyLossLimit = 1000
  4. Enable on ES 5-minute chart (simulation account)
  5. Verify: SDK initialization message in Print output
  6. Let run until opening range complete
  7. Verify: Strategy generates intents
  8. Verify: Orders submit to simulation
  9. Check for errors

📋 Success Criteria

Must Have (Release Blockers)

  • All 3 files compile in NT8 with zero errors
  • Zero compilation warnings
  • MinimalTestStrategy runs and logs bars
  • SimpleORBNT8 initializes SDK components
  • SimpleORBNT8 processes bars without errors
  • SimpleORBNT8 generates trading intents
  • SimpleORBNT8 submits orders to NT8
  • Risk validation works (rejects over-risk trades)
  • Position sizing calculates correctly
  • Order callbacks update execution adapter
  • Stops and targets place correctly
  • No crashes or exceptions for 1+ hours

Should Have (Quality Targets)

  • OnBarUpdate executes in <200ms
  • SDK initialization in <1 second
  • Clear logging of all major events
  • Graceful error handling
  • Memory stable over time

Nice to Have (Future)

  • Parameter optimization interface
  • Real-time analytics display
  • Multi-timeframe support

🚨 Critical Constraints

NT8-Specific Requirements

  1. Namespace MUST be: NinjaTrader.NinjaScript.Strategies
  2. Class MUST be: public (not internal)
  3. Inherit from: NinjaTrader.NinjaScript.Strategies.Strategy
  4. Properties MUST use: [NinjaScriptProperty] for NT8 UI
  5. NO async/await (NT8 doesn't support it well)
  6. NO LINQ in OnBarUpdate (performance)
  7. Print() for output (not Console.WriteLine)
  8. Log() for errors (not throw exceptions)

C# 5.0 Constraints

  • No string interpolation $""
  • No expression-bodied members =>
  • No null-conditional operators ?.
  • No pattern matching
  • Use string.Format()
  • Use traditional methods
  • Explicit null checks

Performance Requirements

OnBarUpdate must complete in <200ms:

  • Minimize allocations
  • No complex calculations
  • Cache frequently-used values
  • Use lock scopes minimally

SDK calls must be fast:

  • Risk validation: <5ms
  • Position sizing: <3ms
  • Intent generation: <50ms

🔄 Implementation Workflow

Step 1: Create NT8StrategyBase.cs (3 hours)

  1. Create file in src/NT8.Adapters/Strategies/
  2. Implement all sections per specification
  3. Add comprehensive XML documentation
  4. Verify C# 5.0 compliance
  5. Self-review for NT8 requirements

Step 2: Create SimpleORBNT8.cs (1 hour)

  1. Create file in src/NT8.Adapters/Strategies/
  2. Implement per specification
  3. Add XML documentation
  4. Link to SDK SimpleORBStrategy

Step 3: Create MinimalTestStrategy.cs (15 min)

  1. Create file in src/NT8.Adapters/Strategies/
  2. Implement simple logging
  3. No SDK dependencies

Step 4: Local Build Verification (15 min)

# These files won't compile in our SDK project (no NT8 refs)
# But verify syntax is valid C# 5.0
# Check with code analysis tools if available

Step 5: Deploy to NT8 (15 min)

# Run deployment commands
# Open NT8 NinjaScript Editor
# Compile All
# Fix any compilation errors

Step 6: Test in NT8 (1-2 hours)

  1. Test MinimalTestStrategy (30 min)
  2. Test SimpleORBNT8 on historical data (30 min)
  3. Test SimpleORBNT8 on simulation (30 min)
  4. Document any issues

Step 7: Git Commit

git add src/NT8.Adapters/Strategies/
git commit -m "feat: Add NT8 strategy base class and implementations

- NT8StrategyBase: Full NT8 lifecycle integration
- SimpleORBNT8: ORB strategy with SDK integration
- MinimalTestStrategy: Simple test strategy

Implements:
- NT8 bar update handling
- SDK component initialization
- Risk/sizing integration
- Order submission to NT8
- Order/execution callbacks
- Error handling and logging

Tested in NT8:
- Compilation successful
- MinimalTest runs correctly
- SimpleORB initializes and trades

Phase B complete: NT8 integration layer ready"

📊 Deliverables Checklist

  • src/NT8.Adapters/Strategies/NT8StrategyBase.cs (~800-1000 lines)
  • src/NT8.Adapters/Strategies/SimpleORBNT8.cs (~150-200 lines)
  • src/NT8.Adapters/Strategies/MinimalTestStrategy.cs (~50 lines)
  • All files compile in NT8 with zero errors/warnings
  • MinimalTest validated (runs and logs)
  • SimpleORB validated (initializes, trades)
  • Documentation complete (XML comments)
  • Git committed with clear message

📚 Reference Materials

NT8 Documentation

SDK References

  • src/NT8.Strategies/Examples/SimpleORBStrategy.cs
  • src/NT8.Core/Common/Interfaces/IStrategy.cs
  • src/NT8.Core/Risk/BasicRiskManager.cs
  • src/NT8.Core/Sizing/BasicPositionSizer.cs

🎯 Success Definition

Phase B is complete when:

  1. All 3 strategy files created
  2. All files compile in NT8 with zero errors
  3. MinimalTestStrategy runs successfully
  4. SimpleORBNT8 initializes SDK correctly
  5. SimpleORBNT8 generates trading intents
  6. SimpleORBNT8 submits orders to NT8
  7. Risk/sizing validation works
  8. Order callbacks process correctly
  9. Strategy runs 1+ hours without errors
  10. Code committed to Git

Time Target: 4-5 hours total


READY FOR KILOCODE EXECUTION IN CODE MODE

Dependencies: Phase A must be complete before starting Phase B