Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
2026-02-24 15:00:41 -05:00
parent 0e36fe5d23
commit a87152effb
50 changed files with 12849 additions and 752 deletions

View File

@@ -0,0 +1,354 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NT8.Adapters.NinjaTrader;
using NT8.Core.Common.Models;
namespace NT8.Core.Tests.Adapters
{
/// <summary>
/// Unit tests for NT8DataConverter.
/// </summary>
[TestClass]
public class NT8DataConverterTests
{
[TestMethod]
public void ConvertBar_WithValidESBar_ShouldCreateBarData()
{
var time = new DateTime(2026, 2, 17, 9, 30, 0, DateTimeKind.Utc);
var result = NT8DataConverter.ConvertBar("ES", time, 4200.0, 4210.0, 4195.0, 4208.0, 10000, 5);
Assert.AreEqual("ES", result.Symbol);
Assert.AreEqual(time, result.Time);
Assert.AreEqual(4200.0, result.Open);
Assert.AreEqual(4210.0, result.High);
Assert.AreEqual(4195.0, result.Low);
Assert.AreEqual(4208.0, result.Close);
Assert.AreEqual(10000L, result.Volume);
Assert.AreEqual(TimeSpan.FromMinutes(5), result.BarSize);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ConvertBar_WithInvalidSymbol_ShouldThrowArgumentException(string symbol)
{
var ex = Assert.ThrowsException<ArgumentException>(
delegate
{
NT8DataConverter.ConvertBar(symbol, DateTime.UtcNow, 1, 2, 0.5, 1.5, 10, 5);
});
Assert.AreEqual("symbol", ex.Message);
}
[DataTestMethod]
[DataRow(0)]
[DataRow(-1)]
[DataRow(-60)]
public void ConvertBar_WithInvalidBarSize_ShouldThrowArgumentException(int barSize)
{
var ex = Assert.ThrowsException<ArgumentException>(
delegate
{
NT8DataConverter.ConvertBar("ES", DateTime.UtcNow, 1, 2, 0.5, 1.5, 10, barSize);
});
Assert.AreEqual("barSizeMinutes", ex.Message);
}
[TestMethod]
public void ConvertBar_WithDifferentTimeframes_ShouldSetCorrectBarSize()
{
var sizes = new[] { 1, 5, 15, 30, 60, 240, 1440 };
for (var i = 0; i < sizes.Length; i++)
{
var value = sizes[i];
var result = NT8DataConverter.ConvertBar("ES", DateTime.UtcNow, 1, 2, 0.5, 1.5, 10, value);
Assert.AreEqual(TimeSpan.FromMinutes(value), result.BarSize);
}
}
[TestMethod]
public void ConvertBar_WithHighLessThanLow_ShouldStillCreate()
{
var result = NT8DataConverter.ConvertBar("ES", DateTime.UtcNow, 100, 95, 105, 99, 1000, 5);
Assert.AreEqual(95.0, result.High);
Assert.AreEqual(105.0, result.Low);
}
[TestMethod]
public void ConvertBar_WithZeroVolume_ShouldCreateBar()
{
var result = NT8DataConverter.ConvertBar("MES", DateTime.UtcNow, 5000, 5005, 4995, 5001, 0, 1);
Assert.AreEqual(0L, result.Volume);
}
[TestMethod]
public void ConvertBar_WithNegativePrices_ShouldHandleCorrectly()
{
var result = NT8DataConverter.ConvertBar("ZN", DateTime.UtcNow, -1.2, -0.9, -1.4, -1.0, 2500, 5);
Assert.AreEqual(-1.2, result.Open);
Assert.AreEqual(-0.9, result.High);
Assert.AreEqual(-1.4, result.Low);
Assert.AreEqual(-1.0, result.Close);
}
[TestMethod]
public void ConvertBar_WithLargeVolume_ShouldHandleCorrectly()
{
var result = NT8DataConverter.ConvertBar("NQ", DateTime.UtcNow, 100, 110, 95, 108, 10000000, 5);
Assert.AreEqual(10000000L, result.Volume);
}
[TestMethod]
public void ConvertAccount_WithPositiveValues_ShouldCreateAccountInfo()
{
var now = DateTime.UtcNow;
var result = NT8DataConverter.ConvertAccount(100000, 250000, 1250.50, 0.05, now);
Assert.AreEqual(100000.0, result.Equity);
Assert.AreEqual(250000.0, result.BuyingPower);
Assert.AreEqual(1250.50, result.DailyPnL);
Assert.AreEqual(0.05, result.MaxDrawdown);
Assert.AreEqual(now, result.LastUpdate);
}
[TestMethod]
public void ConvertAccount_WithNegativePnL_ShouldHandleCorrectly()
{
var result = NT8DataConverter.ConvertAccount(100000, 250000, -2500.75, 0.05, DateTime.UtcNow);
Assert.AreEqual(-2500.75, result.DailyPnL);
}
[TestMethod]
public void ConvertAccount_WithZeroValues_ShouldCreateAccount()
{
var result = NT8DataConverter.ConvertAccount(0, 0, 0, 0, DateTime.UtcNow);
Assert.AreEqual(0.0, result.Equity);
Assert.AreEqual(0.0, result.BuyingPower);
Assert.AreEqual(0.0, result.DailyPnL);
Assert.AreEqual(0.0, result.MaxDrawdown);
}
[TestMethod]
public void ConvertAccount_WithLargeEquity_ShouldHandleCorrectly()
{
var result = NT8DataConverter.ConvertAccount(10000000, 25000000, 5000, 100000, DateTime.UtcNow);
Assert.AreEqual(10000000.0, result.Equity);
Assert.AreEqual(25000000.0, result.BuyingPower);
}
[TestMethod]
public void ConvertPosition_WithLongPosition_ShouldCreatePosition()
{
var result = NT8DataConverter.ConvertPosition("ES", 2, 4200.50, 250, 500, DateTime.UtcNow);
Assert.AreEqual("ES", result.Symbol);
Assert.IsTrue(result.Quantity > 0);
Assert.AreEqual(2, result.Quantity);
}
[TestMethod]
public void ConvertPosition_WithShortPosition_ShouldHandleNegativeQuantity()
{
var result = NT8DataConverter.ConvertPosition("ES", -1, 4200.50, -150, 200, DateTime.UtcNow);
Assert.IsTrue(result.Quantity < 0);
Assert.AreEqual(-1, result.Quantity);
}
[TestMethod]
public void ConvertPosition_WithFlatPosition_ShouldHandleZeroQuantity()
{
var result = NT8DataConverter.ConvertPosition("ES", 0, 0, 0, 0, DateTime.UtcNow);
Assert.AreEqual(0, result.Quantity);
Assert.AreEqual(0.0, result.AveragePrice);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ConvertPosition_WithInvalidSymbol_ShouldThrowArgumentException(string symbol)
{
var ex = Assert.ThrowsException<ArgumentException>(
delegate
{
NT8DataConverter.ConvertPosition(symbol, 1, 1, 1, 1, DateTime.UtcNow);
});
Assert.AreEqual("symbol", ex.Message);
}
[TestMethod]
public void ConvertPosition_WithNegativeUnrealizedPnL_ShouldHandleCorrectly()
{
var result = NT8DataConverter.ConvertPosition("ES", 1, 4200.50, -350.25, 20, DateTime.UtcNow);
Assert.AreEqual(-350.25, result.UnrealizedPnL);
}
[TestMethod]
public void ConvertSession_WithRTHSession_ShouldCreateMarketSession()
{
var start = new DateTime(2026, 2, 17, 9, 30, 0, DateTimeKind.Utc);
var end = new DateTime(2026, 2, 17, 16, 0, 0, DateTimeKind.Utc);
var result = NT8DataConverter.ConvertSession(start, end, true, "RTH");
Assert.IsTrue(result.IsRth);
Assert.AreEqual("RTH", result.SessionName);
}
[TestMethod]
public void ConvertSession_WithETHSession_ShouldCreateMarketSession()
{
var start = new DateTime(2026, 2, 17, 18, 0, 0, DateTimeKind.Utc);
var end = new DateTime(2026, 2, 18, 9, 30, 0, DateTimeKind.Utc);
var result = NT8DataConverter.ConvertSession(start, end, false, "ETH");
Assert.IsFalse(result.IsRth);
Assert.AreEqual(start, result.SessionStart);
Assert.AreEqual(end, result.SessionEnd);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ConvertSession_WithInvalidName_ShouldThrowArgumentException(string name)
{
var ex = Assert.ThrowsException<ArgumentException>(
delegate
{
NT8DataConverter.ConvertSession(DateTime.UtcNow, DateTime.UtcNow.AddHours(1), true, name);
});
Assert.AreEqual("sessionName", ex.Message);
}
[TestMethod]
public void ConvertSession_WithEndBeforeStart_ShouldThrowArgumentException()
{
var start = new DateTime(2026, 2, 17, 16, 0, 0, DateTimeKind.Utc);
var end = new DateTime(2026, 2, 17, 9, 30, 0, DateTimeKind.Utc);
var ex = Assert.ThrowsException<ArgumentException>(
delegate
{
NT8DataConverter.ConvertSession(start, end, true, "RTH");
});
Assert.AreEqual("sessionEnd", ex.Message);
}
[TestMethod]
public void ConvertContext_WithValidInputs_ShouldCreateStrategyContext()
{
var position = CreatePosition();
var account = CreateAccount();
var session = CreateSession();
var customData = new Dictionary<string, object>();
customData.Add("a", 1);
customData.Add("b", "value");
var result = NT8DataConverter.ConvertContext("ES", DateTime.UtcNow, position, account, session, customData);
Assert.AreEqual("ES", result.Symbol);
Assert.AreEqual(position, result.CurrentPosition);
Assert.AreEqual(account, result.Account);
Assert.AreEqual(session, result.Session);
Assert.AreEqual(2, result.CustomData.Count);
Assert.AreEqual(1, (int)result.CustomData["a"]);
Assert.AreEqual("value", (string)result.CustomData["b"]);
}
[TestMethod]
public void ConvertContext_WithNullCustomData_ShouldCreateEmptyDictionary()
{
var result = NT8DataConverter.ConvertContext("ES", DateTime.UtcNow, CreatePosition(), CreateAccount(), CreateSession(), null);
Assert.IsNotNull(result.CustomData);
Assert.AreEqual(0, result.CustomData.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ConvertContext_WithInvalidSymbol_ShouldThrowArgumentException(string symbol)
{
var ex = Assert.ThrowsException<ArgumentException>(
delegate
{
NT8DataConverter.ConvertContext(symbol, DateTime.UtcNow, CreatePosition(), CreateAccount(), CreateSession(), null);
});
Assert.AreEqual("symbol", ex.Message);
}
[TestMethod]
public void ConvertContext_WithNullPosition_ShouldThrowArgumentNullException()
{
var ex = Assert.ThrowsException<ArgumentNullException>(
delegate
{
NT8DataConverter.ConvertContext("ES", DateTime.UtcNow, null, CreateAccount(), CreateSession(), null);
});
Assert.AreEqual("currentPosition", ex.ParamName);
}
[TestMethod]
public void ConvertContext_WithNullAccount_ShouldThrowArgumentNullException()
{
var ex = Assert.ThrowsException<ArgumentNullException>(
delegate
{
NT8DataConverter.ConvertContext("ES", DateTime.UtcNow, CreatePosition(), null, CreateSession(), null);
});
Assert.AreEqual("account", ex.ParamName);
}
[TestMethod]
public void ConvertContext_WithNullSession_ShouldThrowArgumentNullException()
{
var ex = Assert.ThrowsException<ArgumentNullException>(
delegate
{
NT8DataConverter.ConvertContext("ES", DateTime.UtcNow, CreatePosition(), CreateAccount(), null, null);
});
Assert.AreEqual("session", ex.ParamName);
}
private static Position CreatePosition()
{
return new Position("ES", 1, 4200.0, 10.0, 5.0, DateTime.UtcNow);
}
private static AccountInfo CreateAccount()
{
return new AccountInfo(100000.0, 250000.0, 500.0, 2500.0, DateTime.UtcNow);
}
private static MarketSession CreateSession()
{
return new MarketSession(DateTime.UtcNow.Date.AddHours(9.5), DateTime.UtcNow.Date.AddHours(16), true, "RTH");
}
}
}

View File

@@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NT8.Adapters.NinjaTrader;
using NT8.Core.OMS;
namespace NT8.Core.Tests.Adapters
{
/// <summary>
/// Unit tests for NT8ExecutionAdapter.
/// </summary>
[TestClass]
public class NT8ExecutionAdapterTests
{
[TestMethod]
public void SubmitOrder_WithValidRequest_ShouldCreateTrackingInfo()
{
var adapter = new NT8ExecutionAdapter();
var request = CreateRequest();
var info = adapter.SubmitOrder(request, "SDK-1");
Assert.IsNotNull(info);
Assert.AreEqual("SDK-1", info.SdkOrderId);
Assert.AreEqual(OrderState.Pending, info.CurrentState);
Assert.AreEqual(0, info.FilledQuantity);
}
[TestMethod]
public void SubmitOrder_WithDuplicateOrderId_ShouldThrowInvalidOperationException()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(), "SDK-1");
Assert.ThrowsException<InvalidOperationException>(
delegate
{
adapter.SubmitOrder(CreateRequest(), "SDK-1");
});
}
[TestMethod]
public void SubmitOrder_WithNullRequest_ShouldThrowArgumentNullException()
{
var adapter = new NT8ExecutionAdapter();
Assert.ThrowsException<ArgumentNullException>(
delegate
{
adapter.SubmitOrder(null, "SDK-1");
});
}
[TestMethod]
public void ProcessOrderUpdate_WithWorkingState_ShouldUpdateState()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "WORKING", 0, 0.0, 0, null);
var status = adapter.GetOrderStatus("SDK-1");
Assert.IsNotNull(status);
Assert.AreEqual(OrderState.Working, status.State);
}
[TestMethod]
public void ProcessOrderUpdate_WithFilledState_ShouldMarkFilled()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(2), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "FILLED", 2, 4205.25, 0, null);
var status = adapter.GetOrderStatus("SDK-1");
Assert.AreEqual(OrderState.Filled, status.State);
Assert.AreEqual(2, status.FilledQuantity);
}
[TestMethod]
public void ProcessOrderUpdate_WithRejection_ShouldSetRejectedState()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "WORKING", 0, 0.0, 123, "Rejected by broker");
var status = adapter.GetOrderStatus("SDK-1");
Assert.AreEqual(OrderState.Rejected, status.State);
}
[TestMethod]
public void ProcessExecution_WithFullFill_ShouldMarkFilled()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(3), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "PARTFILLED", 3, 4202.0, 0, null);
adapter.ProcessExecution("NT8-1", "EX-1", 4202.0, 3, DateTime.UtcNow);
var status = adapter.GetOrderStatus("SDK-1");
Assert.AreEqual(OrderState.Filled, status.State);
}
[TestMethod]
public void ProcessExecution_WithPartialFill_ShouldMarkPartiallyFilled()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(3), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "PARTFILLED", 1, 4202.0, 0, null);
adapter.ProcessExecution("NT8-1", "EX-1", 4202.0, 1, DateTime.UtcNow);
var status = adapter.GetOrderStatus("SDK-1");
Assert.AreEqual(OrderState.PartiallyFilled, status.State);
}
[TestMethod]
public void CancelOrder_WithWorkingOrder_ShouldReturnTrue()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "WORKING", 0, 0.0, 0, null);
var result = adapter.CancelOrder("SDK-1");
Assert.IsTrue(result);
}
[TestMethod]
public void CancelOrder_WithFilledOrder_ShouldReturnFalse()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "FILLED", 1, 4202.0, 0, null);
var result = adapter.CancelOrder("SDK-1");
Assert.IsFalse(result);
}
[TestMethod]
public void GetOrderStatus_WithExistingOrder_ShouldReturnStatus()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(), "SDK-1");
var status = adapter.GetOrderStatus("SDK-1");
Assert.IsNotNull(status);
Assert.AreEqual("SDK-1", status.OrderId);
Assert.AreEqual("ES", status.Symbol);
}
[TestMethod]
public void GetOrderStatus_WithNonExistentOrder_ShouldReturnNull()
{
var adapter = new NT8ExecutionAdapter();
var status = adapter.GetOrderStatus("MISSING");
Assert.IsNull(status);
}
[DataTestMethod]
[DataRow("ACCEPTED", OrderState.Working)]
[DataRow("FILLED", OrderState.Filled)]
[DataRow("CANCELLED", OrderState.Cancelled)]
[DataRow("REJECTED", OrderState.Rejected)]
public void MapNT8OrderState_WithKnownStates_ShouldMapCorrectly(string nt8State, OrderState expected)
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", nt8State, 0, 0.0, 0, null);
var status = adapter.GetOrderStatus("SDK-1");
Assert.AreEqual(expected, status.State);
}
[TestMethod]
public void SubmitOrder_WithConcurrentCalls_ShouldBeThreadSafe()
{
var adapter = new NT8ExecutionAdapter();
var tasks = new List<Task>();
var count = 50;
for (var i = 0; i < count; i++)
{
var index = i;
tasks.Add(Task.Run(
delegate
{
adapter.SubmitOrder(CreateRequest(), string.Format("SDK-{0}", index));
}));
}
Task.WaitAll(tasks.ToArray());
for (var i = 0; i < count; i++)
{
var status = adapter.GetOrderStatus(string.Format("SDK-{0}", i));
Assert.IsNotNull(status);
}
}
[TestMethod]
public void ProcessExecution_WithMultipleCallsForSameOrder_ShouldAccumulate()
{
var adapter = new NT8ExecutionAdapter();
adapter.SubmitOrder(CreateRequest(3), "SDK-1");
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "PARTFILLED", 1, 4201.0, 0, null);
adapter.ProcessExecution("NT8-1", "EX-1", 4201.0, 1, DateTime.UtcNow);
var statusAfterFirst = adapter.GetOrderStatus("SDK-1");
Assert.AreEqual(OrderState.PartiallyFilled, statusAfterFirst.State);
adapter.ProcessOrderUpdate("NT8-1", "SDK-1", "FILLED", 3, 4202.0, 0, null);
adapter.ProcessExecution("NT8-1", "EX-2", 4202.0, 2, DateTime.UtcNow);
var statusAfterSecond = adapter.GetOrderStatus("SDK-1");
Assert.AreEqual(OrderState.Filled, statusAfterSecond.State);
Assert.AreEqual(3, statusAfterSecond.FilledQuantity);
}
private static OrderRequest CreateRequest()
{
return CreateRequest(1);
}
private static OrderRequest CreateRequest(int quantity)
{
var request = new OrderRequest();
request.Symbol = "ES";
request.Side = OrderSide.Buy;
request.Type = OrderType.Market;
request.Quantity = quantity;
request.TimeInForce = TimeInForce.Day;
request.ClientOrderId = Guid.NewGuid().ToString();
return request;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NT8.Core.Execution;
using NT8.Core.OMS;
using NT8.Core.Tests.Mocks;
using ExecutionTrailingStopConfig = NT8.Core.Execution.TrailingStopConfig;
namespace NT8.Core.Tests.Execution
{
[TestClass]
public class TrailingStopManagerFixedTests
{
private TrailingStopManager _manager;
[TestInitialize]
public void TestInitialize()
{
_manager = new TrailingStopManager(new MockLogger<TrailingStopManager>());
}
[TestMethod]
public void CalculateNewStopPrice_FixedTrailing_LongAt5100With8Ticks_Returns5098()
{
var position = CreatePosition(OrderSide.Buy, 5000m);
var config = new ExecutionTrailingStopConfig(StopType.FixedTrailing, 8, 2m, true);
var stop = _manager.CalculateNewStopPrice(StopType.FixedTrailing, position, 5100m, config);
Assert.AreEqual(5098.0m, stop);
}
[TestMethod]
public void CalculateNewStopPrice_FixedTrailing_ShortAt5100With8Ticks_Returns5102()
{
var position = CreatePosition(OrderSide.Sell, 5000m);
var config = new ExecutionTrailingStopConfig(StopType.FixedTrailing, 8, 2m, true);
var stop = _manager.CalculateNewStopPrice(StopType.FixedTrailing, position, 5100m, config);
Assert.AreEqual(5102.0m, stop);
}
private static OrderStatus CreatePosition(OrderSide side, decimal averageFillPrice)
{
var position = new OrderStatus();
position.OrderId = Guid.NewGuid().ToString();
position.Symbol = "ES";
position.Side = side;
position.Quantity = 1;
position.AverageFillPrice = averageFillPrice;
position.State = OrderState.Working;
position.FilledQuantity = 1;
position.CreatedTime = DateTime.UtcNow;
return position;
}
}
}

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\NT8.Core\NT8.Core.csproj" />
<ProjectReference Include="..\..\src\NT8.Adapters\NT8.Adapters.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -0,0 +1,344 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NT8.Adapters.NinjaTrader;
using NT8.Adapters.Wrappers;
using NT8.Core.Common.Models;
using NT8.Core.Logging;
using NT8.Core.Risk;
using NT8.Core.Sizing;
namespace NT8.Integration.Tests
{
/// <summary>
/// Integration tests for end-to-end SDK workflow coverage.
/// </summary>
[TestClass]
public class NT8IntegrationTests
{
private StrategyContext CreateTestContext(string symbol, int qty, double equity, double dailyPnl)
{
var now = new DateTime(2026, 2, 17, 10, 30, 0, DateTimeKind.Utc);
var position = new Position(symbol, qty, 4200.0, 0.0, dailyPnl, now);
var account = new AccountInfo(equity, equity * 2.5, dailyPnl, 0.0, now);
var session = new MarketSession(now.Date.AddHours(9).AddMinutes(30), now.Date.AddHours(16), true, "RTH");
return new StrategyContext(symbol, now, position, account, session, new Dictionary<string, object>());
}
private BarData CreateTestBar(string symbol)
{
return new BarData(
symbol,
new DateTime(2026, 2, 17, 10, 30, 0, DateTimeKind.Utc),
4200.0,
4210.0,
4195.0,
4208.0,
10000,
TimeSpan.FromMinutes(5));
}
[TestMethod]
public void CompleteWorkflow_StrategyToExecution_ShouldProcessIntent()
{
var wrapper = new SimpleORBNT8Wrapper();
var symbol = "ES";
var sessionStart = new DateTime(2026, 2, 17, 9, 30, 0, DateTimeKind.Utc);
var openingBar1 = new BarData(symbol, sessionStart.AddMinutes(5), 100, 101, 99, 100.5, 1000, TimeSpan.FromMinutes(5));
var openingBar2 = new BarData(symbol, sessionStart.AddMinutes(10), 100.5, 102, 100, 101.5, 1000, TimeSpan.FromMinutes(5));
var breakoutBar = new BarData(symbol, sessionStart.AddMinutes(35), 102, 104.5, 101.5, 104.2, 1200, TimeSpan.FromMinutes(5));
wrapper.ProcessBarUpdate(openingBar1, CreateTestContext(symbol, 0, 100000.0, 0.0));
wrapper.ProcessBarUpdate(openingBar2, CreateTestContext(symbol, 0, 100000.0, 0.0));
wrapper.ProcessBarUpdate(breakoutBar, CreateTestContext(symbol, 0, 100000.0, 0.0));
var records = wrapper.GetAdapterForTesting().GetExecutionHistory();
Assert.IsNotNull(records);
Assert.IsTrue(records.Count >= 1);
}
[TestMethod]
public void DataConversion_NT8ToSDK_ShouldPreserveData()
{
var time = new DateTime(2026, 2, 17, 10, 0, 0, DateTimeKind.Utc);
var bar = NT8DataConverter.ConvertBar("ES", time, 4200.0, 4215.0, 4192.0, 4210.0, 15000, 5);
Assert.AreEqual("ES", bar.Symbol);
Assert.AreEqual(time, bar.Time);
Assert.AreEqual(4200.0, bar.Open);
Assert.AreEqual(4215.0, bar.High);
Assert.AreEqual(4192.0, bar.Low);
Assert.AreEqual(4210.0, bar.Close);
Assert.AreEqual(15000L, bar.Volume);
Assert.AreEqual(TimeSpan.FromMinutes(5), bar.BarSize);
}
[TestMethod]
public void ExecutionAdapter_OrderLifecycle_ShouldTrackCorrectly()
{
var adapter = new NT8ExecutionAdapter();
var req = new NT8.Core.OMS.OrderRequest();
req.Symbol = "ES";
req.Side = NT8.Core.OMS.OrderSide.Buy;
req.Type = NT8.Core.OMS.OrderType.Market;
req.Quantity = 2;
var tracking = adapter.SubmitOrder(req, "TEST_001");
Assert.AreEqual(NT8.Core.OMS.OrderState.Pending, tracking.CurrentState);
adapter.ProcessOrderUpdate("NT8_1", "TEST_001", "WORKING", 0, 0.0, 0, null);
Assert.AreEqual(NT8.Core.OMS.OrderState.Working, adapter.GetOrderStatus("TEST_001").State);
adapter.ProcessOrderUpdate("NT8_1", "TEST_001", "PARTFILLED", 1, 4200.50, 0, null);
adapter.ProcessExecution("NT8_1", "EXEC_1", 4200.50, 1, DateTime.UtcNow);
Assert.AreEqual(NT8.Core.OMS.OrderState.PartiallyFilled, adapter.GetOrderStatus("TEST_001").State);
adapter.ProcessOrderUpdate("NT8_1", "TEST_001", "FILLED", 2, 4201.00, 0, null);
adapter.ProcessExecution("NT8_1", "EXEC_2", 4201.00, 1, DateTime.UtcNow);
Assert.AreEqual(NT8.Core.OMS.OrderState.Filled, adapter.GetOrderStatus("TEST_001").State);
}
[TestMethod]
public void RiskManager_DailyLossLimit_ShouldRejectOverRisk()
{
var logger = new BasicLogger("Risk");
var risk = new BasicRiskManager(logger);
risk.OnPnLUpdate(-950.0, -950.0);
var intent = new StrategyIntent(
"ES",
OrderSide.Buy,
OrderType.Market,
null,
10,
20,
0.9,
"Risk test",
new Dictionary<string, object>());
var context = CreateTestContext("ES", 0, 100000.0, -950.0);
var cfg = new RiskConfig(1000.0, 200.0, 3, true);
var decision = risk.ValidateOrder(intent, context, cfg);
Assert.IsFalse(decision.Allow);
}
[TestMethod]
public void PositionSizer_FixedDollarRisk_ShouldCalculateCorrectly()
{
var logger = new BasicLogger("Sizer");
var sizer = new BasicPositionSizer(logger);
var intent = new StrategyIntent(
"ES",
OrderSide.Buy,
OrderType.Market,
null,
8,
16,
0.8,
"Sizing test",
new Dictionary<string, object>());
var context = CreateTestContext("ES", 0, 100000.0, 0.0);
var cfg = new SizingConfig(SizingMethod.FixedDollarRisk, 1, 10, 100.0, new Dictionary<string, object>());
var result = sizer.CalculateSize(intent, context, cfg);
Assert.IsTrue(result.Contracts > 0);
Assert.IsTrue(result.Contracts <= 10);
Assert.AreEqual(SizingMethod.FixedDollarRisk, result.Method);
}
[TestMethod]
public void ExecutionAdapter_ConcurrentAccess_ShouldBeThreadSafe()
{
var adapter = new NT8ExecutionAdapter();
var exceptions = new List<Exception>();
var sync = new object();
var success = 0;
var threadList = new List<Thread>();
for (var t = 0; t < 10; t++)
{
var tn = t;
var thread = new Thread(delegate()
{
try
{
for (var i = 0; i < 10; i++)
{
var req = new NT8.Core.OMS.OrderRequest();
req.Symbol = "ES";
req.Side = NT8.Core.OMS.OrderSide.Buy;
req.Type = NT8.Core.OMS.OrderType.Market;
req.Quantity = 1;
var id = string.Format("TH_{0}_{1}", tn, i);
adapter.SubmitOrder(req, id);
adapter.ProcessOrderUpdate(id + "_NT8", id, "WORKING", 0, 0.0, 0, null);
lock (sync)
{
success++;
}
}
}
catch (Exception ex)
{
lock (sync)
{
exceptions.Add(ex);
}
}
});
threadList.Add(thread);
thread.Start();
}
foreach (var thread in threadList)
{
thread.Join();
}
Assert.AreEqual(0, exceptions.Count);
Assert.AreEqual(100, success);
}
[TestMethod]
public void PerformanceTest_OnBarUpdate_ShouldComplete200ms()
{
var wrapper = new SimpleORBNT8Wrapper();
var context = CreateTestContext("ES", 0, 100000.0, 0.0);
var bar = CreateTestBar("ES");
for (var i = 0; i < 10; i++)
wrapper.ProcessBarUpdate(bar, context);
var iterations = 100;
var started = DateTime.UtcNow;
for (var i = 0; i < iterations; i++)
{
wrapper.ProcessBarUpdate(bar, context);
}
var elapsed = (DateTime.UtcNow - started).TotalMilliseconds / iterations;
Assert.IsTrue(elapsed < 200.0, string.Format("Average processing time too high: {0:F2} ms", elapsed));
}
[TestMethod]
public void ExecutionAdapter_CancelUnknownOrder_ShouldReturnFalse()
{
var adapter = new NT8ExecutionAdapter();
var result = adapter.CancelOrder("missing");
Assert.IsFalse(result);
}
[TestMethod]
public void ExecutionAdapter_GetOrderStatus_EmptyId_ShouldReturnNull()
{
var adapter = new NT8ExecutionAdapter();
Assert.IsNull(adapter.GetOrderStatus(""));
}
[TestMethod]
public void DataConverter_ConvertContext_WithCustomData_ShouldCloneDictionary()
{
var custom = new Dictionary<string, object>();
custom.Add("k1", 1);
custom.Add("k2", "v2");
var ctx = NT8DataConverter.ConvertContext(
"ES",
DateTime.UtcNow,
new Position("ES", 0, 0, 0, 0, DateTime.UtcNow),
new AccountInfo(100000.0, 200000.0, 0.0, 0.0, DateTime.UtcNow),
new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"),
custom);
custom.Add("k3", 3);
Assert.AreEqual(2, ctx.CustomData.Count);
}
[TestMethod]
public void DataConverter_ConvertSession_OvernightSession_ShouldWork()
{
var start = new DateTime(2026, 2, 17, 18, 0, 0, DateTimeKind.Utc);
var end = new DateTime(2026, 2, 18, 9, 30, 0, DateTimeKind.Utc);
var session = NT8DataConverter.ConvertSession(start, end, false, "ETH");
Assert.IsFalse(session.IsRth);
Assert.AreEqual("ETH", session.SessionName);
}
[TestMethod]
public void DataConverter_ConvertPosition_WithShortQuantity_ShouldPreserveNegative()
{
var pos = NT8DataConverter.ConvertPosition("ES", -2, 4200.0, -150.0, 25.0, DateTime.UtcNow);
Assert.AreEqual(-2, pos.Quantity);
Assert.AreEqual(-150.0, pos.UnrealizedPnL);
}
[TestMethod]
public void DataConverter_ConvertAccount_WithNegativePnL_ShouldPreserveValue()
{
var account = NT8DataConverter.ConvertAccount(100000.0, 180000.0, -1234.5, 5000.0, DateTime.UtcNow);
Assert.AreEqual(-1234.5, account.DailyPnL);
}
[TestMethod]
public void RiskManager_ValidIntentUnderLimits_ShouldAllow()
{
var logger = new BasicLogger("RiskAllow");
var risk = new BasicRiskManager(logger);
risk.OnPnLUpdate(0.0, 0.0);
var intent = new StrategyIntent(
"MES",
OrderSide.Buy,
OrderType.Market,
null,
8,
12,
0.7,
"allow",
new Dictionary<string, object>());
var decision = risk.ValidateOrder(
intent,
CreateTestContext("MES", 0, 50000.0, 0.0),
new RiskConfig(1000.0, 200.0, 3, true));
Assert.IsTrue(decision.Allow);
}
[TestMethod]
public void PositionSizer_InvalidIntent_ShouldReturnZeroContracts()
{
var logger = new BasicLogger("InvalidIntent");
var sizer = new BasicPositionSizer(logger);
var invalid = new StrategyIntent(
"",
OrderSide.Flat,
OrderType.Market,
null,
0,
null,
-1.0,
"",
new Dictionary<string, object>());
var result = sizer.CalculateSize(
invalid,
CreateTestContext("ES", 0, 100000.0, 0.0),
new SizingConfig(SizingMethod.FixedDollarRisk, 1, 10, 100.0, new Dictionary<string, object>()));
Assert.AreEqual(0, result.Contracts);
}
}
}