Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
Some checks failed
Build and Test / build (push) Has been cancelled
Some checks failed
Build and Test / build (push) Has been cancelled
This commit is contained in:
864
PHASE_A_SPECIFICATION.md
Normal file
864
PHASE_A_SPECIFICATION.md
Normal file
@@ -0,0 +1,864 @@
|
||||
# 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for NT8DataConverter
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapter for executing orders through NinjaTrader 8 platform.
|
||||
/// Bridges SDK order requests to NT8 order submission and handles callbacks.
|
||||
/// Thread-safe for concurrent NT8 callbacks.
|
||||
/// </summary>
|
||||
public class NT8ExecutionAdapter
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<string, OrderTrackingInfo> _orderTracking;
|
||||
private readonly Dictionary<string, string> _nt8ToSdkOrderMap;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new NT8 execution adapter.
|
||||
/// </summary>
|
||||
public NT8ExecutionAdapter()
|
||||
{
|
||||
_orderTracking = new Dictionary<string, OrderTrackingInfo>();
|
||||
_nt8ToSdkOrderMap = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
// Methods defined below...
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal class for tracking order state
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="request">SDK order request</param>
|
||||
/// <param name="sdkOrderId">Unique SDK order ID</param>
|
||||
/// <returns>Tracking info for the submitted order</returns>
|
||||
/// <exception cref="ArgumentNullException">If request or orderId is null</exception>
|
||||
/// <exception cref="InvalidOperationException">If order already exists</exception>
|
||||
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
|
||||
/// <summary>
|
||||
/// Process order update callback from NinjaTrader 8.
|
||||
/// Called by NT8StrategyBase.OnOrderUpdate().
|
||||
/// </summary>
|
||||
/// <param name="nt8OrderId">NT8's order ID</param>
|
||||
/// <param name="sdkOrderId">SDK's order ID (from order name/tag)</param>
|
||||
/// <param name="orderState">NT8 order state</param>
|
||||
/// <param name="filled">Filled quantity</param>
|
||||
/// <param name="averageFillPrice">Average fill price</param>
|
||||
/// <param name="errorCode">Error code if rejected</param>
|
||||
/// <param name="errorMessage">Error message if rejected</param>
|
||||
public void ProcessOrderUpdate(
|
||||
string nt8OrderId,
|
||||
string sdkOrderId,
|
||||
string orderState,
|
||||
int filled,
|
||||
double averageFillPrice,
|
||||
int errorCode,
|
||||
string errorMessage)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sdkOrderId))
|
||||
return; // 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
|
||||
/// <summary>
|
||||
/// Process execution (fill) callback from NinjaTrader 8.
|
||||
/// Called by NT8StrategyBase.OnExecutionUpdate().
|
||||
/// </summary>
|
||||
/// <param name="nt8OrderId">NT8 order ID</param>
|
||||
/// <param name="executionId">NT8 execution ID</param>
|
||||
/// <param name="price">Fill price</param>
|
||||
/// <param name="quantity">Fill quantity</param>
|
||||
/// <param name="time">Execution time</param>
|
||||
public void ProcessExecution(
|
||||
string nt8OrderId,
|
||||
string executionId,
|
||||
double price,
|
||||
int quantity,
|
||||
DateTime time)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nt8OrderId))
|
||||
return;
|
||||
|
||||
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
|
||||
/// <summary>
|
||||
/// Request to cancel an order.
|
||||
/// NOTE: Actual cancellation happens in NT8StrategyBase via CancelOrder().
|
||||
/// This method validates and marks order for cancellation.
|
||||
/// </summary>
|
||||
/// <param name="sdkOrderId">SDK order ID to cancel</param>
|
||||
/// <returns>True if cancel request accepted, false if order can't be cancelled</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Get current status of an order.
|
||||
/// </summary>
|
||||
/// <param name="sdkOrderId">SDK order ID</param>
|
||||
/// <returns>Order status or null if not found</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Maps NinjaTrader 8 order state strings to SDK OrderState enum.
|
||||
/// </summary>
|
||||
/// <param name="nt8State">NT8 order state as string</param>
|
||||
/// <returns>Mapped SDK OrderState</returns>
|
||||
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** ✅
|
||||
Reference in New Issue
Block a user