# Phase A: NT8 Data & Execution Adapters - Detailed Specification **For:** Kilocode AI Agent (Autonomous Implementation) **Phase:** Phase A - Foundation **Components:** NT8DataAdapter Tests + NT8ExecutionAdapter **Estimated Time:** 4-5 hours **Mode:** Code Mode --- ## 🎯 Objective Build comprehensive unit tests for existing NT8DataAdapter/NT8DataConverter, then create the NT8ExecutionAdapter that handles real order submission to NinjaTrader 8. --- ## 📋 Task 1: NT8 Data Adapter Unit Tests (2 hours) ### Overview The `NT8DataConverter.cs` already exists but has ZERO unit tests. Create comprehensive test coverage. ### Location **Create:** `tests/NT8.Core.Tests/Adapters/NT8DataConverterTests.cs` ### Requirements **Test Coverage Target:** 95%+ of NT8DataConverter methods **Test Categories:** 1. ConvertBar (8 tests) 2. ConvertAccount (4 tests) 3. ConvertPosition (5 tests) 4. ConvertSession (4 tests) 5. ConvertContext (6 tests) **Total:** 27 unit tests minimum ### Detailed Test Specifications #### 1. ConvertBar Tests (8 tests) ```csharp namespace NT8.Core.Tests.Adapters { public class NT8DataConverterTests { // TEST 1: Happy path with valid ES bar [Fact] public void ConvertBar_WithValidESBar_ShouldCreateBarData() { // Input: symbol="ES", time=2026-02-17 09:30:00, OHLCV=4200/4210/4195/4208/10000, barSize=5 // Expected: BarData with all properties matching, BarSize=TimeSpan.FromMinutes(5) } // TEST 2: Null/empty/whitespace symbol [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void ConvertBar_WithInvalidSymbol_ShouldThrowArgumentException(string symbol) { // Expected: ArgumentException with parameter name "symbol" } // TEST 3: Invalid bar sizes (zero, negative) [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(-60)] public void ConvertBar_WithInvalidBarSize_ShouldThrowArgumentException(int barSize) { // Expected: ArgumentException with parameter name "barSizeMinutes" } // TEST 4: Different timeframes (1min, 5min, 15min, 30min, 60min, 240min, daily) [Fact] public void ConvertBar_WithDifferentTimeframes_ShouldSetCorrectBarSize() { // Test each: 1, 5, 15, 30, 60, 240, 1440 // Verify BarSize property matches TimeSpan.FromMinutes(input) } // TEST 5: High < Low scenario (invalid OHLC) [Fact] public void ConvertBar_WithHighLessThanLow_ShouldStillCreate() { // Note: BarData constructor should validate, but converter just passes through // Expected: May throw from BarData constructor OR create invalid bar // Document actual behavior } // TEST 6: Zero volume [Fact] public void ConvertBar_WithZeroVolume_ShouldCreateBar() { // Expected: Creates bar with Volume=0 (valid for some instruments/sessions) } // TEST 7: Negative prices [Fact] public void ConvertBar_WithNegativePrices_ShouldHandleCorrectly() { // For instruments like ZN that can have negative yields // Expected: Accepts negative prices } // TEST 8: Large volume values [Fact] public void ConvertBar_WithLargeVolume_ShouldHandleCorrectly() { // Volume = 10,000,000 // Expected: Handles long values correctly } } } ``` #### 2. ConvertAccount Tests (4 tests) ```csharp // TEST 9: Valid account with positive values [Fact] public void ConvertAccount_WithPositiveValues_ShouldCreateAccountInfo() { // Input: equity=100000, buyingPower=250000, dailyPnL=1250.50, maxDD=0.05 // Expected: All properties match } // TEST 10: Negative daily P&L (losing day) [Fact] public void ConvertAccount_WithNegativePnL_ShouldHandleCorrectly() { // Input: dailyPnL=-2500.75 // Expected: DailyPnL property is negative } // TEST 11: Zero equity/buying power (margin call scenario) [Fact] public void ConvertAccount_WithZeroValues_ShouldCreateAccount() { // Input: All zeros // Expected: Creates valid AccountInfo with zero values } // TEST 12: Very large equity values [Fact] public void ConvertAccount_WithLargeEquity_ShouldHandleCorrectly() { // Input: equity=10,000,000 // Expected: Handles large doubles correctly } ``` #### 3. ConvertPosition Tests (5 tests) ```csharp // TEST 13: Long position [Fact] public void ConvertPosition_WithLongPosition_ShouldCreatePosition() { // Input: symbol="ES", quantity=2, avgPrice=4200.50, unrealizedPnL=250, realizedPnL=500 // Expected: Quantity > 0 } // TEST 14: Short position (negative quantity) [Fact] public void ConvertPosition_WithShortPosition_ShouldHandleNegativeQuantity() { // Input: quantity=-1 // Expected: Quantity < 0 } // TEST 15: Flat position (zero quantity) [Fact] public void ConvertPosition_WithFlatPosition_ShouldHandleZeroQuantity() { // Input: quantity=0, avgPrice=0 // Expected: Creates valid flat position } // TEST 16: Invalid symbol [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void ConvertPosition_WithInvalidSymbol_ShouldThrowArgumentException(string symbol) { // Expected: ArgumentException with parameter "symbol" } // TEST 17: Negative unrealized P&L (losing position) [Fact] public void ConvertPosition_WithNegativeUnrealizedPnL_ShouldHandleCorrectly() { // Input: unrealizedPnL=-350.25 // Expected: UnrealizedPnL property is negative } ``` #### 4. ConvertSession Tests (4 tests) ```csharp // TEST 18: RTH session [Fact] public void ConvertSession_WithRTHSession_ShouldCreateMarketSession() { // Input: start=09:30, end=16:00, isRth=true, name="RTH" // Expected: IsRth=true } // TEST 19: ETH session [Fact] public void ConvertSession_WithETHSession_ShouldCreateMarketSession() { // Input: start=18:00, end=next day 09:30, isRth=false, name="ETH" // Expected: IsRth=false, handles overnight session } // TEST 20: Invalid session name [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void ConvertSession_WithInvalidName_ShouldThrowArgumentException(string name) { // Expected: ArgumentException with parameter "sessionName" } // TEST 21: End before start (invalid range) [Fact] public void ConvertSession_WithEndBeforeStart_ShouldThrowArgumentException() { // Input: start=16:00, end=09:30 // Expected: ArgumentException with parameter "sessionEnd" } ``` #### 5. ConvertContext Tests (6 tests) ```csharp // TEST 22: Valid context with all components [Fact] public void ConvertContext_WithValidInputs_ShouldCreateStrategyContext() { // Input: All valid Position, Account, Session, CustomData with 2 entries // Expected: All properties populated, CustomData contains both entries } // TEST 23: Null custom data [Fact] public void ConvertContext_WithNullCustomData_ShouldCreateEmptyDictionary() { // Input: customData=null // Expected: CustomData is non-null empty dictionary } // TEST 24: Invalid symbol [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void ConvertContext_WithInvalidSymbol_ShouldThrowArgumentException(string symbol) { // Expected: ArgumentException with parameter "symbol" } // TEST 25: Null position [Fact] public void ConvertContext_WithNullPosition_ShouldThrowArgumentNullException() { // Expected: ArgumentNullException with parameter "currentPosition" } // TEST 26: Null account [Fact] public void ConvertContext_WithNullAccount_ShouldThrowArgumentNullException() { // Expected: ArgumentNullException with parameter "account" } // TEST 27: Null session [Fact] public void ConvertContext_WithNullSession_ShouldThrowArgumentNullException() { // Expected: ArgumentNullException with parameter "session" } ``` ### Implementation Notes **Framework:** - Use xUnit - Use FluentAssertions for readable assertions - Follow existing test patterns in `tests/NT8.Core.Tests` **File Structure:** ```csharp using System; using System.Collections.Generic; using Xunit; using FluentAssertions; using NT8.Adapters.NinjaTrader; using NT8.Core.Common.Models; namespace NT8.Core.Tests.Adapters { /// /// Unit tests for NT8DataConverter /// public class NT8DataConverterTests { // All 27 tests here } } ``` **Success Criteria:** - [ ] All 27 tests implemented - [ ] All tests pass - [ ] Zero warnings - [ ] Code coverage >95% for NT8DataConverter - [ ] Follows existing test patterns --- ## 📋 Task 2: NT8ExecutionAdapter Implementation (2-3 hours) ### Overview Create the adapter that handles REAL order submission to NinjaTrader 8. ### Location **Create:** `src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs` **Create:** `tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs` ### NT8ExecutionAdapter Specification #### Class Structure ```csharp using System; using System.Collections.Generic; using NT8.Core.Common.Models; using NT8.Core.OMS; namespace NT8.Adapters.NinjaTrader { /// /// 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. /// public class NT8ExecutionAdapter { private readonly object _lock = new object(); private readonly Dictionary _orderTracking; private readonly Dictionary _nt8ToSdkOrderMap; /// /// Creates a new NT8 execution adapter. /// public NT8ExecutionAdapter() { _orderTracking = new Dictionary(); _nt8ToSdkOrderMap = new Dictionary(); } // Methods defined below... } /// /// Internal class for tracking order state /// internal class OrderTrackingInfo { public string SdkOrderId { get; set; } public string Nt8OrderId { get; set; } public OrderRequest OriginalRequest { get; set; } public OrderState CurrentState { get; set; } public int FilledQuantity { get; set; } public double AverageFillPrice { get; set; } public DateTime LastUpdate { get; set; } public string ErrorMessage { get; set; } } } ``` #### Method 1: SubmitOrder ```csharp /// /// Submit an order to NinjaTrader 8. /// NOTE: This method accepts primitive parameters instead of NT8 Strategy object /// to maintain testability and avoid NT8 DLL dependencies in core adapter. /// The actual NT8StrategyBase will call NT8 methods and pass results here. /// /// SDK order request /// Unique SDK order ID /// Tracking info for the submitted order /// If request or orderId is null /// If order already exists public OrderTrackingInfo SubmitOrder(OrderRequest request, string sdkOrderId) { if (request == null) throw new ArgumentNullException("request"); if (string.IsNullOrWhiteSpace(sdkOrderId)) throw new ArgumentNullException("sdkOrderId"); lock (_lock) { // Check if order already tracked if (_orderTracking.ContainsKey(sdkOrderId)) { throw new InvalidOperationException( string.Format("Order {0} already exists", sdkOrderId)); } // Create tracking info var trackingInfo = new OrderTrackingInfo { SdkOrderId = sdkOrderId, Nt8OrderId = null, // Will be set by NT8 callback OriginalRequest = request, CurrentState = OrderState.Pending, FilledQuantity = 0, AverageFillPrice = 0.0, LastUpdate = DateTime.UtcNow, ErrorMessage = null }; _orderTracking[sdkOrderId] = trackingInfo; // NOTE: Actual NT8 submission happens in NT8StrategyBase // This adapter only tracks state return trackingInfo; } } ``` #### Method 2: ProcessOrderUpdate ```csharp /// /// Process order update callback from NinjaTrader 8. /// Called by NT8StrategyBase.OnOrderUpdate(). /// /// NT8's order ID /// SDK's order ID (from order name/tag) /// NT8 order state /// Filled quantity /// Average fill price /// Error code if rejected /// Error message if rejected public void ProcessOrderUpdate( string nt8OrderId, string sdkOrderId, string orderState, int filled, double averageFillPrice, int errorCode, string errorMessage) { if (string.IsNullOrWhiteSpace(sdkOrderId)) return; // Ignore orders not from SDK lock (_lock) { if (!_orderTracking.ContainsKey(sdkOrderId)) { // Order not tracked, ignore return; } var info = _orderTracking[sdkOrderId]; // Map NT8 order ID if (!string.IsNullOrWhiteSpace(nt8OrderId) && info.Nt8OrderId == null) { info.Nt8OrderId = nt8OrderId; _nt8ToSdkOrderMap[nt8OrderId] = sdkOrderId; } // Update state info.CurrentState = MapNT8OrderState(orderState); info.FilledQuantity = filled; info.AverageFillPrice = averageFillPrice; info.LastUpdate = DateTime.UtcNow; // Handle errors if (errorCode != 0 && !string.IsNullOrWhiteSpace(errorMessage)) { info.ErrorMessage = string.Format("[{0}] {1}", errorCode, errorMessage); info.CurrentState = OrderState.Rejected; } } } ``` #### Method 3: ProcessExecution ```csharp /// /// Process execution (fill) callback from NinjaTrader 8. /// Called by NT8StrategyBase.OnExecutionUpdate(). /// /// NT8 order ID /// NT8 execution ID /// Fill price /// Fill quantity /// Execution time public void ProcessExecution( string nt8OrderId, string executionId, double price, int quantity, DateTime time) { if (string.IsNullOrWhiteSpace(nt8OrderId)) return; lock (_lock) { // Map NT8 order ID to SDK order ID if (!_nt8ToSdkOrderMap.ContainsKey(nt8OrderId)) return; // Not our order var sdkOrderId = _nt8ToSdkOrderMap[nt8OrderId]; if (!_orderTracking.ContainsKey(sdkOrderId)) return; var info = _orderTracking[sdkOrderId]; // Update fill info // Note: NT8 may send multiple execution callbacks for partial fills // We track cumulative filled quantity via ProcessOrderUpdate info.LastUpdate = time; // Update state based on filled quantity if (info.FilledQuantity >= info.OriginalRequest.Quantity) { info.CurrentState = OrderState.Filled; } else if (info.FilledQuantity > 0) { info.CurrentState = OrderState.PartiallyFilled; } } } ``` #### Method 4: CancelOrder ```csharp /// /// Request to cancel an order. /// NOTE: Actual cancellation happens in NT8StrategyBase via CancelOrder(). /// This method validates and marks order for cancellation. /// /// SDK order ID to cancel /// True if cancel request accepted, false if order can't be cancelled public bool CancelOrder(string sdkOrderId) { if (string.IsNullOrWhiteSpace(sdkOrderId)) throw new ArgumentNullException("sdkOrderId"); lock (_lock) { if (!_orderTracking.ContainsKey(sdkOrderId)) return false; // Order not found var info = _orderTracking[sdkOrderId]; // Check if order is in cancellable state if (info.CurrentState == OrderState.Filled || info.CurrentState == OrderState.Cancelled || info.CurrentState == OrderState.Rejected) { return false; // Already in terminal state } // Mark as pending cancellation // Actual state change happens in ProcessOrderUpdate callback info.LastUpdate = DateTime.UtcNow; return true; } } ``` #### Method 5: GetOrderStatus ```csharp /// /// Get current status of an order. /// /// SDK order ID /// Order status or null if not found public OrderStatus GetOrderStatus(string sdkOrderId) { if (string.IsNullOrWhiteSpace(sdkOrderId)) return null; lock (_lock) { if (!_orderTracking.ContainsKey(sdkOrderId)) return null; var info = _orderTracking[sdkOrderId]; return new OrderStatus( orderId: info.SdkOrderId, state: info.CurrentState, symbol: info.OriginalRequest.Symbol, side: info.OriginalRequest.Side, quantity: info.OriginalRequest.Quantity, type: info.OriginalRequest.Type, filled: info.FilledQuantity, averageFillPrice: info.FilledQuantity > 0 ? (double?)info.AverageFillPrice : null, message: info.ErrorMessage, timestamp: info.LastUpdate ); } } ``` #### Helper Method: MapNT8OrderState ```csharp /// /// Maps NinjaTrader 8 order state strings to SDK OrderState enum. /// /// NT8 order state as string /// Mapped SDK OrderState private OrderState MapNT8OrderState(string nt8State) { if (string.IsNullOrWhiteSpace(nt8State)) return OrderState.Unknown; // NT8 order states: https://ninjatrader.com/support/helpGuides/nt8/?orderstate.htm 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; // Still working until cancelled case "PENDINGCHANGE": case "PENDINGSUBMIT": return OrderState.Pending; default: return OrderState.Unknown; } } ``` ### Unit Tests for NT8ExecutionAdapter **Create:** `tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs` **Test Count:** 15 tests minimum ```csharp public class NT8ExecutionAdapterTests { // TEST 1: Submit valid order [Fact] public void SubmitOrder_WithValidRequest_ShouldCreateTrackingInfo() // TEST 2: Submit duplicate order [Fact] public void SubmitOrder_WithDuplicateOrderId_ShouldThrowInvalidOperationException() // TEST 3: Submit with null request [Fact] public void SubmitOrder_WithNullRequest_ShouldThrowArgumentNullException() // TEST 4: Process order update - Working state [Fact] public void ProcessOrderUpdate_WithWorkingState_ShouldUpdateState() // TEST 5: Process order update - Filled state [Fact] public void ProcessOrderUpdate_WithFilledState_ShouldMarkFilled() // TEST 6: Process order update - Rejected with error [Fact] public void ProcessOrderUpdate_WithRejection_ShouldSetErrorMessage() // TEST 7: Process execution - Full fill [Fact] public void ProcessExecution_WithFullFill_ShouldMarkFilled() // TEST 8: Process execution - Partial fill [Fact] public void ProcessExecution_WithPartialFill_ShouldMarkPartiallyFilled() // TEST 9: Cancel order - Valid [Fact] public void CancelOrder_WithWorkingOrder_ShouldReturnTrue() // TEST 10: Cancel order - Already filled [Fact] public void CancelOrder_WithFilledOrder_ShouldReturnFalse() // TEST 11: Get order status - Exists [Fact] public void GetOrderStatus_WithExistingOrder_ShouldReturnStatus() // TEST 12: Get order status - Not found [Fact] public void GetOrderStatus_WithNonExistentOrder_ShouldReturnNull() // TEST 13: NT8 order state mapping [Theory] [InlineData("ACCEPTED", OrderState.Working)] [InlineData("FILLED", OrderState.Filled)] [InlineData("CANCELLED", OrderState.Cancelled)] [InlineData("REJECTED", OrderState.Rejected)] public void MapNT8OrderState_WithKnownStates_ShouldMapCorrectly(string nt8State, OrderState expected) // TEST 14: Thread safety - Concurrent submissions [Fact] public void SubmitOrder_WithConcurrentCalls_ShouldBeThreadSafe() // TEST 15: Multiple executions for same order [Fact] public void ProcessExecution_WithMultipleCallsForSameOrder_ShouldAccumulate() } ``` ### Success Criteria **For NT8ExecutionAdapter:** - [ ] All public methods implemented - [ ] Thread-safe with lock protection - [ ] Comprehensive XML documentation - [ ] C# 5.0 compliant (no modern syntax) - [ ] Zero build warnings **For Tests:** - [ ] All 15 tests implemented - [ ] All tests pass - [ ] Code coverage >90% for NT8ExecutionAdapter - [ ] Thread safety validated --- ## 🔄 Implementation Workflow ### Step 1: Create Test File (30 min) 1. Create `tests/NT8.Core.Tests/Adapters/` directory 2. Create `NT8DataConverterTests.cs` 3. Implement all 27 tests 4. Run tests - should all PASS (code already exists) ### Step 2: Verify Test Coverage (15 min) ```bash dotnet test --collect:"XPlat Code Coverage" # Verify >95% coverage for NT8DataConverter ``` ### Step 3: Create NT8ExecutionAdapter (2 hours) 1. Create `src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs` 2. Implement all methods per specification 3. Add XML documentation 4. Verify C# 5.0 compliance ### Step 4: Create Execution Adapter Tests (1 hour) 1. Create `tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs` 2. Implement all 15 tests 3. Run tests - should all PASS ### Step 5: Build & Verify (15 min) ```bash dotnet build --configuration Release dotnet test --configuration Release .\verify-build.bat ``` ### Step 6: Git Commit ```bash git add tests/NT8.Core.Tests/Adapters/ git add src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs git commit -m "feat: Add NT8 adapter tests and execution adapter - Added 27 unit tests for NT8DataConverter (>95% coverage) - Implemented NT8ExecutionAdapter with order tracking - Added 15 unit tests for NT8ExecutionAdapter (>90% coverage) - Thread-safe order state management - NT8 order state mapping - C# 5.0 compliant Phase A complete: Foundation adapters ready for NT8 integration" ``` --- ## 📊 Deliverables Checklist - [ ] `tests/NT8.Core.Tests/Adapters/NT8DataConverterTests.cs` (27 tests) - [ ] `src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs` (full implementation) - [ ] `tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs` (15 tests) - [ ] All 42 new tests passing - [ ] All 240+ existing tests still passing - [ ] Zero build warnings - [ ] Code coverage: >95% for DataConverter, >90% for ExecutionAdapter - [ ] Git commit with clear message --- ## 🚨 Important Constraints 1. **C# 5.0 Only** - No: - `async/await` - String interpolation `$""` - Expression-bodied members `=>` - Pattern matching - Tuples - Use `string.Format()` instead of `$""` 2. **Thread Safety** - All shared state must use `lock (_lock)` 3. **Defensive Programming** - Validate all inputs, null checks 4. **XML Documentation** - All public members must have /// comments 5. **Test Patterns** - Follow existing test conventions in `tests/NT8.Core.Tests` --- ## 🎯 Success Metrics **Definition of Done:** - ✅ All 42 tests passing - ✅ All existing 240+ tests still passing - ✅ Build succeeds with zero warnings - ✅ Code coverage targets met - ✅ Thread safety verified - ✅ C# 5.0 compliant - ✅ Committed to Git **Time Target:** 4-5 hours total --- **READY FOR KILOCODE EXECUTION IN CODE MODE** ✅