Phase 0 completion: NT8 SDK core framework with risk management and position sizing
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
Billy Valentine
2025-09-09 17:06:37 -04:00
parent 97e5050d3e
commit 92f3732b3d
109 changed files with 38593 additions and 380 deletions

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\NT8.Core\NT8.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,190 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Extensions.Logging;
using NT8.Core.Orders;
using NT8.Core.Risk;
using NT8.Core.Sizing;
using NT8.Core.Common.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NT8.Core.Tests.Orders
{
[TestClass]
public class OrderManagerTests
{
private TestRiskManager _testRiskManager;
private TestPositionSizer _testPositionSizer;
private TestLogger _testLogger;
private OrderManager _orderManager;
[TestInitialize]
public void TestInitialize()
{
_testRiskManager = new TestRiskManager();
_testPositionSizer = new TestPositionSizer();
_testLogger = new TestLogger();
_orderManager = new OrderManager(
_testRiskManager,
_testPositionSizer,
_testLogger);
}
[TestMethod]
public async Task SubmitOrderAsync_WithValidRequest_ReturnsSuccess()
{
// Arrange
var algorithmParameters = new Dictionary<string, object>();
var request = new OrderRequest(
"ES",
NT8.Core.Orders.OrderSide.Buy,
NT8.Core.Orders.OrderType.Market,
1,
null,
null,
NT8.Core.Orders.TimeInForce.Day,
null,
algorithmParameters
);
var customData = new Dictionary<string, object>();
var context = new StrategyContext(
"ES",
DateTime.UtcNow,
new Position("ES", 0, 0, 0, 0, DateTime.UtcNow),
new AccountInfo(10000, 100, 0, 0, DateTime.UtcNow),
new MarketSession(DateTime.UtcNow, DateTime.UtcNow.AddHours(8), true, "RTH"),
customData
);
// Act
var result = await _orderManager.SubmitOrderAsync(request, context);
// Assert
Assert.IsTrue(result.Success);
Assert.IsNotNull(result.OrderId);
Assert.AreEqual("Order submitted successfully", result.Message);
}
[TestMethod]
public async Task GetRoutingConfig_ReturnsConfig()
{
// Act
var config = _orderManager.GetRoutingConfig();
// Assert
Assert.IsNotNull(config);
Assert.IsTrue(config.SmartRoutingEnabled);
Assert.AreEqual("Primary", config.DefaultVenue);
}
[TestMethod]
public void UpdateRoutingConfig_WithValidConfig_UpdatesConfig()
{
// Arrange
var venuePreferences = new Dictionary<string, double>();
venuePreferences.Add("TestVenue", 1.0);
var newConfig = new RoutingConfig(
false, // SmartRoutingEnabled
"TestVenue", // DefaultVenue
venuePreferences,
1.0, // MaxSlippagePercent
TimeSpan.FromSeconds(60), // MaxRoutingTime
false, // RouteByCost
false, // RouteBySpeed
false // RouteByReliability
);
// Act
_orderManager.UpdateRoutingConfig(newConfig);
var config = _orderManager.GetRoutingConfig();
// Assert
Assert.IsNotNull(config);
Assert.IsFalse(config.SmartRoutingEnabled);
Assert.AreEqual("TestVenue", config.DefaultVenue);
Assert.AreEqual(1.0, config.MaxSlippagePercent);
}
#region Test Implementations
/// <summary>
/// Test implementation of IRiskManager
/// </summary>
private class TestRiskManager : IRiskManager
{
public RiskDecision ValidateOrder(StrategyIntent intent, StrategyContext context, RiskConfig config)
{
// Always approve for testing
var metrics = new Dictionary<string, object>();
return new RiskDecision(true, null, null, RiskLevel.Low, metrics);
}
public void OnFill(OrderFill fill)
{
// No-op for testing
}
public void OnPnLUpdate(double netPnL, double dayPnL)
{
// No-op for testing
}
public async Task<bool> EmergencyFlatten(string reason)
{
// Always succeed for testing
return true;
}
public RiskStatus GetRiskStatus()
{
var alerts = new List<string>();
return new RiskStatus(true, 0, 1000, 0, 0, DateTime.UtcNow, alerts);
}
}
/// <summary>
/// Test implementation of IPositionSizer
/// </summary>
private class TestPositionSizer : IPositionSizer
{
public SizingResult CalculateSize(StrategyIntent intent, StrategyContext context, SizingConfig config)
{
// Return fixed size for testing
var calculations = new Dictionary<string, object>();
return new SizingResult(1, 100, SizingMethod.FixedContracts, calculations);
}
public SizingMetadata GetMetadata()
{
var requiredParameters = new List<string>();
return new SizingMetadata("TestSizer", "Test position sizer", requiredParameters);
}
}
/// <summary>
/// Test implementation of ILogger
/// </summary>
private class TestLogger : ILogger<OrderManager>
{
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
// No-op for testing
}
}
#endregion
}
}

View File

@@ -0,0 +1,111 @@
using NT8.Core.Risk;
using NT8.Core.Common.Models;
using NT8.Core.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace NT8.Core.Tests.Risk
{
[TestClass]
public class BasicRiskManagerTests
{
private ILogger _logger;
private BasicRiskManager _riskManager;
[TestInitialize]
public void TestInitialize()
{
_logger = new BasicLogger("BasicRiskManagerTests");
_riskManager = new BasicRiskManager(_logger);
}
[TestMethod]
public void ValidateOrder_WithinLimits_ShouldAllow()
{
// Arrange
var intent = TestDataBuilder.CreateValidIntent(stopTicks: 8);
var context = TestDataBuilder.CreateTestContext();
var config = TestDataBuilder.CreateTestRiskConfig();
// Act
var result = _riskManager.ValidateOrder(intent, context, config);
// Assert
Assert.IsTrue(result.Allow);
Assert.IsNull(result.RejectReason);
Assert.AreEqual(RiskLevel.Low, result.RiskLevel);
Assert.IsTrue(result.RiskMetrics.ContainsKey("trade_risk"));
Assert.IsTrue(result.RiskMetrics.ContainsKey("daily_pnl"));
}
[TestMethod]
public void ValidateOrder_ExceedsDailyLimit_ShouldReject()
{
// Arrange
var intent = TestDataBuilder.CreateValidIntent();
var context = TestDataBuilder.CreateTestContext();
var config = new RiskConfig(
dailyLossLimit: 1000,
maxTradeRisk: 500,
maxOpenPositions: 5,
emergencyFlattenEnabled: true
);
// Simulate daily loss exceeding limit
_riskManager.OnPnLUpdate(0, -1001);
// Act
var result = _riskManager.ValidateOrder(intent, context, config);
// Assert
Assert.IsFalse(result.Allow);
// Accept either "Trading halted" or "Daily loss limit" as valid rejection reasons
Assert.IsTrue(result.RejectReason.Contains("Trading halted") || result.RejectReason.Contains("Daily loss limit breached"),
"Expected reject reason to contain either 'Trading halted' or 'Daily loss limit breached', but got: " + result.RejectReason);
Assert.AreEqual(RiskLevel.Critical, result.RiskLevel);
Assert.AreEqual(-1001.0, result.RiskMetrics["daily_pnl"]);
}
[TestMethod]
public void ValidateOrder_ExceedsTradeRisk_ShouldReject()
{
// Arrange
var intent = TestDataBuilder.CreateValidIntent(stopTicks: 100); // High risk trade
var context = TestDataBuilder.CreateTestContext();
var config = new RiskConfig(
dailyLossLimit: 10000,
maxTradeRisk: 500, // Lower than calculated trade risk
maxOpenPositions: 5,
emergencyFlattenEnabled: true
);
// Act
var result = _riskManager.ValidateOrder(intent, context, config);
// Assert
Assert.IsFalse(result.Allow);
// Accept either "Trading halted" or "Trade risk too high" as valid rejection reasons
Assert.IsTrue(result.RejectReason.Contains("Trading halted") || result.RejectReason.Contains("Trade risk too high"),
"Expected reject reason to contain either 'Trading halted' or 'Trade risk too high', but got: " + result.RejectReason);
Assert.AreEqual(RiskLevel.High, result.RiskLevel);
// Verify risk calculation
var expectedRisk = 100 * 12.50; // 100 ticks * ES tick value
Assert.AreEqual(expectedRisk, result.RiskMetrics["trade_risk"]);
}
[TestMethod]
public void ValidateOrder_WithNullParameters_ShouldThrow()
{
// Arrange
var intent = TestDataBuilder.CreateValidIntent();
var context = TestDataBuilder.CreateTestContext();
var config = TestDataBuilder.CreateTestRiskConfig();
// Act & Assert
Assert.ThrowsException<ArgumentNullException>(() => _riskManager.ValidateOrder(null, context, config));
Assert.ThrowsException<ArgumentNullException>(() => _riskManager.ValidateOrder(intent, null, config));
Assert.ThrowsException<ArgumentNullException>(() => _riskManager.ValidateOrder(intent, context, null));
}
}
}

View File

@@ -0,0 +1 @@
// Removed - replaced with MSTest version

View File

@@ -0,0 +1,49 @@
using NT8.Core.Common.Models;
using System;
using System.Collections.Generic;
namespace NT8.Core.Tests
{
public static class TestDataBuilder
{
public static StrategyIntent CreateValidIntent(
string symbol = "ES",
int stopTicks = 8,
OrderSide side = OrderSide.Buy)
{
return new StrategyIntent(
symbol: symbol,
side: side,
entryType: OrderType.Market,
limitPrice: null,
stopTicks: stopTicks,
targetTicks: 16,
confidence: 0.8,
reason: "Test intent",
metadata: new Dictionary<string, object>()
);
}
public static StrategyContext CreateTestContext(string symbol = "ES")
{
return new StrategyContext(
symbol: symbol,
currentTime: DateTime.UtcNow,
currentPosition: new Position(symbol, 0, 0, 0, 0, DateTime.UtcNow),
account: new AccountInfo(50000, 50000, 0, 0, DateTime.UtcNow),
session: new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"),
customData: new Dictionary<string, object>()
);
}
public static RiskConfig CreateTestRiskConfig()
{
return new RiskConfig(
dailyLossLimit: 1000,
maxTradeRisk: 500,
maxOpenPositions: 5,
emergencyFlattenEnabled: true
);
}
}
}

View File

@@ -0,0 +1,15 @@
namespace NT8.Core.Tests;
/// <summary>
/// Unit tests for the core NT8 SDK functionality.
/// </summary>
public class UnitTest1
{
/// <summary>
/// A basic test to verify the test framework is working.
/// </summary>
[Fact]
public void Test1()
{
}
}