# 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** ✅