785 lines
28 KiB
Markdown
785 lines
28 KiB
Markdown
# **Position Sizing Package**
|
|
|
|
## **IMPLEMENTATION INSTRUCTIONS**
|
|
|
|
Implement the BasicPositionSizer with fixed contracts and fixed dollar risk methods. This component determines how many contracts to trade based on the strategy intent and risk parameters.
|
|
|
|
### **File 1: `src/NT8.Core/Sizing/BasicPositionSizer.cs`**
|
|
```csharp
|
|
using NT8.Core.Common.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace NT8.Core.Sizing;
|
|
|
|
/// <summary>
|
|
/// Basic position sizer with fixed contracts and fixed dollar risk methods
|
|
/// Handles contract size calculations with proper rounding and clamping
|
|
/// </summary>
|
|
public class BasicPositionSizer : IPositionSizer
|
|
{
|
|
private readonly ILogger<BasicPositionSizer> _logger;
|
|
|
|
public BasicPositionSizer(ILogger<BasicPositionSizer> logger)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public SizingResult CalculateSize(StrategyIntent intent, StrategyContext context, SizingConfig config)
|
|
{
|
|
if (intent == null) throw new ArgumentNullException(nameof(intent));
|
|
if (context == null) throw new ArgumentNullException(nameof(context));
|
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
|
|
|
// Validate intent is suitable for sizing
|
|
if (!intent.IsValid())
|
|
{
|
|
_logger.LogWarning("Invalid strategy intent provided for sizing: {Intent}", intent);
|
|
return new SizingResult(0, 0, config.Method, new() { ["error"] = "Invalid intent" });
|
|
}
|
|
|
|
return config.Method switch
|
|
{
|
|
SizingMethod.FixedContracts => CalculateFixedContracts(intent, context, config),
|
|
SizingMethod.FixedDollarRisk => CalculateFixedRisk(intent, context, config),
|
|
_ => throw new NotSupportedException($"Sizing method {config.Method} not supported in Phase 0")
|
|
};
|
|
}
|
|
|
|
private SizingResult CalculateFixedContracts(StrategyIntent intent, StrategyContext context, SizingConfig config)
|
|
{
|
|
// Get target contracts from configuration
|
|
var targetContracts = GetParameterValue<int>(config, "contracts", 1);
|
|
|
|
// Apply min/max clamping
|
|
var contracts = Math.Max(config.MinContracts,
|
|
Math.Min(config.MaxContracts, targetContracts));
|
|
|
|
// Calculate actual risk amount
|
|
var tickValue = GetTickValue(intent.Symbol);
|
|
var riskAmount = contracts * intent.StopTicks * tickValue;
|
|
|
|
_logger.LogDebug("Fixed contracts sizing: {Symbol} {TargetContracts}→{ActualContracts} contracts, ${Risk:F2} risk",
|
|
intent.Symbol, targetContracts, contracts, riskAmount);
|
|
|
|
return new SizingResult(
|
|
Contracts: contracts,
|
|
RiskAmount: riskAmount,
|
|
Method: SizingMethod.FixedContracts,
|
|
Calculations: new()
|
|
{
|
|
["target_contracts"] = targetContracts,
|
|
["clamped_contracts"] = contracts,
|
|
["stop_ticks"] = intent.StopTicks,
|
|
["tick_value"] = tickValue,
|
|
["risk_amount"] = riskAmount,
|
|
["min_contracts"] = config.MinContracts,
|
|
["max_contracts"] = config.MaxContracts
|
|
}
|
|
);
|
|
}
|
|
|
|
private SizingResult CalculateFixedRisk(StrategyIntent intent, StrategyContext context, SizingConfig config)
|
|
{
|
|
var tickValue = GetTickValue(intent.Symbol);
|
|
|
|
// Validate stop ticks
|
|
if (intent.StopTicks <= 0)
|
|
{
|
|
_logger.LogWarning("Invalid stop ticks {StopTicks} for fixed risk sizing on {Symbol}",
|
|
intent.StopTicks, intent.Symbol);
|
|
|
|
return new SizingResult(0, 0, SizingMethod.FixedDollarRisk,
|
|
new() { ["error"] = "Invalid stop ticks", ["stop_ticks"] = intent.StopTicks });
|
|
}
|
|
|
|
// Calculate optimal contracts for target risk
|
|
var targetRisk = config.RiskPerTrade;
|
|
var riskPerContract = intent.StopTicks * tickValue;
|
|
var optimalContracts = targetRisk / riskPerContract;
|
|
|
|
// Round down to whole contracts (conservative approach)
|
|
var contracts = (int)Math.Floor(optimalContracts);
|
|
|
|
// Apply min/max clamping
|
|
contracts = Math.Max(config.MinContracts, Math.Min(config.MaxContracts, contracts));
|
|
|
|
// Calculate actual risk with final contract count
|
|
var actualRisk = contracts * riskPerContract;
|
|
|
|
_logger.LogDebug("Fixed risk sizing: {Symbol} ${TargetRisk:F2}→{OptimalContracts:F2}→{ActualContracts} contracts, ${ActualRisk:F2} actual risk",
|
|
intent.Symbol, targetRisk, optimalContracts, contracts, actualRisk);
|
|
|
|
return new SizingResult(
|
|
Contracts: contracts,
|
|
RiskAmount: actualRisk,
|
|
Method: SizingMethod.FixedDollarRisk,
|
|
Calculations: new()
|
|
{
|
|
["target_risk"] = targetRisk,
|
|
["stop_ticks"] = intent.StopTicks,
|
|
["tick_value"] = tickValue,
|
|
["risk_per_contract"] = riskPerContract,
|
|
["optimal_contracts"] = optimalContracts,
|
|
["clamped_contracts"] = contracts,
|
|
["actual_risk"] = actualRisk,
|
|
["min_contracts"] = config.MinContracts,
|
|
["max_contracts"] = config.MaxContracts
|
|
}
|
|
);
|
|
}
|
|
|
|
private static T GetParameterValue<T>(SizingConfig config, string key, T defaultValue)
|
|
{
|
|
if (config.MethodParameters.TryGetValue(key, out var value))
|
|
{
|
|
try
|
|
{
|
|
return (T)Convert.ChangeType(value, typeof(T));
|
|
}
|
|
catch
|
|
{
|
|
// If conversion fails, return default
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
private static double GetTickValue(string symbol)
|
|
{
|
|
// Static tick values for Phase 0 - will be configurable in Phase 1
|
|
return symbol switch
|
|
{
|
|
"ES" => 12.50, // E-mini S&P 500
|
|
"MES" => 1.25, // Micro E-mini S&P 500
|
|
"NQ" => 5.00, // E-mini NASDAQ-100
|
|
"MNQ" => 0.50, // Micro E-mini NASDAQ-100
|
|
"CL" => 10.00, // Crude Oil
|
|
"GC" => 10.00, // Gold
|
|
"6E" => 12.50, // Euro FX
|
|
"6A" => 10.00, // Australian Dollar
|
|
_ => 12.50 // Default to ES value
|
|
};
|
|
}
|
|
|
|
public SizingMetadata GetMetadata()
|
|
{
|
|
return new SizingMetadata(
|
|
Name: "Basic Position Sizer",
|
|
Description: "Fixed contracts or fixed dollar risk sizing with contract clamping",
|
|
RequiredParameters: new List<string> { "method", "risk_per_trade", "min_contracts", "max_contracts" }
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate sizing configuration parameters
|
|
/// </summary>
|
|
public static bool ValidateConfig(SizingConfig config, out List<string> errors)
|
|
{
|
|
errors = new List<string>();
|
|
|
|
if (config.MinContracts < 0)
|
|
errors.Add("MinContracts must be >= 0");
|
|
|
|
if (config.MaxContracts <= 0)
|
|
errors.Add("MaxContracts must be > 0");
|
|
|
|
if (config.MinContracts > config.MaxContracts)
|
|
errors.Add("MinContracts must be <= MaxContracts");
|
|
|
|
if (config.RiskPerTrade <= 0)
|
|
errors.Add("RiskPerTrade must be > 0");
|
|
|
|
// Method-specific validation
|
|
switch (config.Method)
|
|
{
|
|
case SizingMethod.FixedContracts:
|
|
if (!config.MethodParameters.ContainsKey("contracts"))
|
|
errors.Add("FixedContracts method requires 'contracts' parameter");
|
|
else if (GetParameterValue<int>(config, "contracts", 0) <= 0)
|
|
errors.Add("Fixed contracts parameter must be > 0");
|
|
break;
|
|
|
|
case SizingMethod.FixedDollarRisk:
|
|
// No additional parameters required for fixed dollar risk
|
|
break;
|
|
|
|
default:
|
|
errors.Add($"Unsupported sizing method: {config.Method}");
|
|
break;
|
|
}
|
|
|
|
return errors.Count == 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get supported symbols with their tick values
|
|
/// </summary>
|
|
public static Dictionary<string, double> GetSupportedSymbols()
|
|
{
|
|
return new Dictionary<string, double>
|
|
{
|
|
["ES"] = 12.50,
|
|
["MES"] = 1.25,
|
|
["NQ"] = 5.00,
|
|
["MNQ"] = 0.50,
|
|
["CL"] = 10.00,
|
|
["GC"] = 10.00,
|
|
["6E"] = 12.50,
|
|
["6A"] = 10.00
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## **COMPREHENSIVE TEST SUITE**
|
|
|
|
### **File 2: `tests/NT8.Core.Tests/Sizing/BasicPositionSizerTests.cs`**
|
|
```csharp
|
|
using NT8.Core.Sizing;
|
|
using NT8.Core.Common.Models;
|
|
using NT8.Core.Tests.TestHelpers;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using FluentAssertions;
|
|
using Xunit;
|
|
|
|
namespace NT8.Core.Tests.Sizing;
|
|
|
|
public class BasicPositionSizerTests : IDisposable
|
|
{
|
|
private readonly ILogger<BasicPositionSizer> _logger;
|
|
private readonly BasicPositionSizer _sizer;
|
|
|
|
public BasicPositionSizerTests()
|
|
{
|
|
_logger = NullLogger<BasicPositionSizer>.Instance;
|
|
_sizer = new BasicPositionSizer(_logger);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_FixedContracts_ShouldReturnCorrectSize()
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent(symbol: "ES", stopTicks: 8);
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedContracts,
|
|
MinContracts: 1,
|
|
MaxContracts: 10,
|
|
RiskPerTrade: 200,
|
|
MethodParameters: new() { ["contracts"] = 3 }
|
|
);
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
result.Contracts.Should().Be(3);
|
|
result.Method.Should().Be(SizingMethod.FixedContracts);
|
|
result.RiskAmount.Should().Be(300.0); // 3 contracts * 8 ticks * $12.50
|
|
result.Calculations.Should().ContainKey("target_contracts");
|
|
result.Calculations.Should().ContainKey("clamped_contracts");
|
|
result.Calculations["target_contracts"].Should().Be(3);
|
|
result.Calculations["clamped_contracts"].Should().Be(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_FixedContractsWithClamping_ShouldApplyLimits()
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent(symbol: "ES", stopTicks: 10);
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedContracts,
|
|
MinContracts: 2,
|
|
MaxContracts: 5,
|
|
RiskPerTrade: 200,
|
|
MethodParameters: new() { ["contracts"] = 8 } // Exceeds max
|
|
);
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
result.Contracts.Should().Be(5); // Clamped to max
|
|
result.Calculations["target_contracts"].Should().Be(8);
|
|
result.Calculations["clamped_contracts"].Should().Be(5);
|
|
result.RiskAmount.Should().Be(625.0); // 5 * 10 * $12.50
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_FixedDollarRisk_ShouldCalculateCorrectly()
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent(symbol: "ES", stopTicks: 10);
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedDollarRisk,
|
|
MinContracts: 1,
|
|
MaxContracts: 10,
|
|
RiskPerTrade: 250.0, // Target $250 risk
|
|
MethodParameters: new()
|
|
);
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
// $250 target / (10 ticks * $12.50) = 2 contracts
|
|
result.Contracts.Should().Be(2);
|
|
result.Method.Should().Be(SizingMethod.FixedDollarRisk);
|
|
result.RiskAmount.Should().Be(250.0); // 2 * 10 * $12.50
|
|
result.Calculations["target_risk"].Should().Be(250.0);
|
|
result.Calculations["optimal_contracts"].Should().Be(2.0);
|
|
result.Calculations["actual_risk"].Should().Be(250.0);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("ES", 8, 200.0, 2, 200.0)] // ES: $200 / (8 * $12.50) = 2.0 → 2 contracts
|
|
[InlineData("MES", 8, 20.0, 2, 20.0)] // MES: $20 / (8 * $1.25) = 2.0 → 2 contracts
|
|
[InlineData("NQ", 10, 100.0, 2, 100.0)] // NQ: $100 / (10 * $5.00) = 2.0 → 2 contracts
|
|
[InlineData("CL", 5, 75.0, 1, 50.0)] // CL: $75 / (5 * $10.00) = 1.5 → 1 contract (floor)
|
|
public void CalculateSize_FixedRiskVariousSymbols_ShouldCalculateCorrectly(
|
|
string symbol, int stopTicks, double targetRisk, int expectedContracts, double expectedActualRisk)
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent(symbol: symbol, stopTicks: stopTicks);
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedDollarRisk,
|
|
MinContracts: 1,
|
|
MaxContracts: 10,
|
|
RiskPerTrade: targetRisk,
|
|
MethodParameters: new()
|
|
);
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
result.Contracts.Should().Be(expectedContracts);
|
|
result.RiskAmount.Should().Be(expectedActualRisk);
|
|
result.Method.Should().Be(SizingMethod.FixedDollarRisk);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_FixedRiskWithMinClamp_ShouldApplyMinimum()
|
|
{
|
|
// Arrange - Very small risk that would calculate to 0 contracts
|
|
var intent = TestDataBuilder.CreateValidIntent(symbol: "ES", stopTicks: 20);
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedDollarRisk,
|
|
MinContracts: 2, // Force minimum
|
|
MaxContracts: 10,
|
|
RiskPerTrade: 100.0, // Only enough for 0.4 contracts
|
|
MethodParameters: new()
|
|
);
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
result.Contracts.Should().Be(2); // Applied minimum
|
|
result.RiskAmount.Should().Be(500.0); // 2 * 20 * $12.50
|
|
result.Calculations["optimal_contracts"].Should().Be(0.4);
|
|
result.Calculations["clamped_contracts"].Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_FixedRiskWithMaxClamp_ShouldApplyMaximum()
|
|
{
|
|
// Arrange - Large risk that would calculate to many contracts
|
|
var intent = TestDataBuilder.CreateValidIntent(symbol: "ES", stopTicks: 5);
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedDollarRisk,
|
|
MinContracts: 1,
|
|
MaxContracts: 3, // Limit maximum
|
|
RiskPerTrade: 1000.0, // Enough for 16 contracts
|
|
MethodParameters: new()
|
|
);
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
result.Contracts.Should().Be(3); // Applied maximum
|
|
result.RiskAmount.Should().Be(187.5); // 3 * 5 * $12.50
|
|
result.Calculations["optimal_contracts"].Should().Be(16.0);
|
|
result.Calculations["clamped_contracts"].Should().Be(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_ZeroStopTicks_ShouldReturnZeroContracts()
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent(stopTicks: 0); // Invalid
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = TestDataBuilder.CreateTestSizingConfig(SizingMethod.FixedDollarRisk);
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
result.Contracts.Should().Be(0);
|
|
result.RiskAmount.Should().Be(0);
|
|
result.Calculations.Should().ContainKey("error");
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_InvalidIntent_ShouldReturnZeroContracts()
|
|
{
|
|
// Arrange - Create invalid intent
|
|
var intent = new StrategyIntent(
|
|
Symbol: "", // Invalid empty symbol
|
|
Side: OrderSide.Buy,
|
|
EntryType: OrderType.Market,
|
|
LimitPrice: null,
|
|
StopTicks: 10,
|
|
TargetTicks: 20,
|
|
Confidence: 0.8,
|
|
Reason: "Test",
|
|
Metadata: new()
|
|
);
|
|
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = TestDataBuilder.CreateTestSizingConfig();
|
|
|
|
// Act
|
|
var result = _sizer.CalculateSize(intent, context, config);
|
|
|
|
// Assert
|
|
result.Contracts.Should().Be(0);
|
|
result.RiskAmount.Should().Be(0);
|
|
result.Calculations.Should().ContainKey("error");
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_WithNullParameters_ShouldThrow()
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent();
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = TestDataBuilder.CreateTestSizingConfig();
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => _sizer.CalculateSize(null, context, config));
|
|
Assert.Throws<ArgumentNullException>(() => _sizer.CalculateSize(intent, null, config));
|
|
Assert.Throws<ArgumentNullException>(() => _sizer.CalculateSize(intent, context, null));
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_UnsupportedMethod_ShouldThrow()
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent();
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.OptimalF, // Not supported in Phase 0
|
|
MinContracts: 1,
|
|
MaxContracts: 10,
|
|
RiskPerTrade: 200,
|
|
MethodParameters: new()
|
|
);
|
|
|
|
// Act & Assert
|
|
Assert.Throws<NotSupportedException>(() => _sizer.CalculateSize(intent, context, config));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetMetadata_ShouldReturnCorrectInformation()
|
|
{
|
|
// Act
|
|
var metadata = _sizer.GetMetadata();
|
|
|
|
// Assert
|
|
metadata.Name.Should().Be("Basic Position Sizer");
|
|
metadata.Description.Should().Contain("Fixed contracts");
|
|
metadata.Description.Should().Contain("fixed dollar risk");
|
|
metadata.RequiredParameters.Should().Contain("method");
|
|
metadata.RequiredParameters.Should().Contain("risk_per_trade");
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateConfig_ValidConfiguration_ShouldReturnTrue()
|
|
{
|
|
// Arrange
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedContracts,
|
|
MinContracts: 1,
|
|
MaxContracts: 10,
|
|
RiskPerTrade: 200,
|
|
MethodParameters: new() { ["contracts"] = 2 }
|
|
);
|
|
|
|
// Act
|
|
var isValid = BasicPositionSizer.ValidateConfig(config, out var errors);
|
|
|
|
// Assert
|
|
isValid.Should().BeTrue();
|
|
errors.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateConfig_InvalidConfiguration_ShouldReturnErrors()
|
|
{
|
|
// Arrange
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedContracts,
|
|
MinContracts: 5,
|
|
MaxContracts: 2, // Invalid: min > max
|
|
RiskPerTrade: -100, // Invalid: negative risk
|
|
MethodParameters: new() // Missing required parameter
|
|
);
|
|
|
|
// Act
|
|
var isValid = BasicPositionSizer.ValidateConfig(config, out var errors);
|
|
|
|
// Assert
|
|
isValid.Should().BeFalse();
|
|
errors.Should().Contain("MinContracts must be <= MaxContracts");
|
|
errors.Should().Contain("RiskPerTrade must be > 0");
|
|
errors.Should().Contain("FixedContracts method requires 'contracts' parameter");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSupportedSymbols_ShouldReturnAllSymbolsWithTickValues()
|
|
{
|
|
// Act
|
|
var symbols = BasicPositionSizer.GetSupportedSymbols();
|
|
|
|
// Assert
|
|
symbols.Should().ContainKey("ES").WhoseValue.Should().Be(12.50);
|
|
symbols.Should().ContainKey("MES").WhoseValue.Should().Be(1.25);
|
|
symbols.Should().ContainKey("NQ").WhoseValue.Should().Be(5.00);
|
|
symbols.Should().ContainKey("MNQ").WhoseValue.Should().Be(0.50);
|
|
symbols.Should().ContainKey("CL").WhoseValue.Should().Be(10.00);
|
|
symbols.Should().ContainKey("GC").WhoseValue.Should().Be(10.00);
|
|
symbols.Count.Should().BeGreaterOrEqualTo(6);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateSize_ConsistentResults_ShouldBeDeterministic()
|
|
{
|
|
// Arrange
|
|
var intent = TestDataBuilder.CreateValidIntent(symbol: "ES", stopTicks: 12);
|
|
var context = TestDataBuilder.CreateTestContext();
|
|
var config = new SizingConfig(
|
|
Method: SizingMethod.FixedDollarRisk,
|
|
MinContracts: 1,
|
|
MaxContracts: 10,
|
|
RiskPerTrade: 300,
|
|
MethodParameters: new()
|
|
);
|
|
|
|
// Act - Calculate multiple times
|
|
var results = new List<SizingResult>();
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
results.Add(_sizer.CalculateSize(intent, context, config));
|
|
}
|
|
|
|
// Assert - All results should be identical
|
|
var firstResult = results[0];
|
|
foreach (var result in results.Skip(1))
|
|
{
|
|
result.Contracts.Should().Be(firstResult.Contracts);
|
|
result.RiskAmount.Should().Be(firstResult.RiskAmount);
|
|
result.Method.Should().Be(firstResult.Method);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Cleanup if needed
|
|
}
|
|
}
|
|
```
|
|
|
|
## **CALCULATION EXAMPLES TEST DATA**
|
|
|
|
### **File 3: `test-data/calculation-examples.json`**
|
|
```json
|
|
{
|
|
"description": "Position sizing calculation examples for validation",
|
|
"test_cases": [
|
|
{
|
|
"name": "ES Fixed Contracts",
|
|
"symbol": "ES",
|
|
"stop_ticks": 8,
|
|
"method": "FixedContracts",
|
|
"method_parameters": { "contracts": 2 },
|
|
"min_contracts": 1,
|
|
"max_contracts": 10,
|
|
"risk_per_trade": 200,
|
|
"expected_contracts": 2,
|
|
"expected_risk": 200.0,
|
|
"calculation": "2 contracts * 8 ticks * $12.50 = $200"
|
|
},
|
|
{
|
|
"name": "ES Fixed Dollar Risk",
|
|
"symbol": "ES",
|
|
"stop_ticks": 10,
|
|
"method": "FixedDollarRisk",
|
|
"method_parameters": {},
|
|
"min_contracts": 1,
|
|
"max_contracts": 10,
|
|
"risk_per_trade": 250,
|
|
"expected_contracts": 2,
|
|
"expected_risk": 250.0,
|
|
"calculation": "$250 / (10 ticks * $12.50) = 2.0 contracts"
|
|
},
|
|
{
|
|
"name": "MES Fixed Dollar Risk",
|
|
"symbol": "MES",
|
|
"stop_ticks": 16,
|
|
"method": "FixedDollarRisk",
|
|
"method_parameters": {},
|
|
"min_contracts": 1,
|
|
"max_contracts": 50,
|
|
"risk_per_trade": 100,
|
|
"expected_contracts": 5,
|
|
"expected_risk": 100.0,
|
|
"calculation": "$100 / (16 ticks * $1.25) = 5.0 contracts"
|
|
},
|
|
{
|
|
"name": "NQ Fixed Risk with Rounding",
|
|
"symbol": "NQ",
|
|
"stop_ticks": 12,
|
|
"method": "FixedDollarRisk",
|
|
"method_parameters": {},
|
|
"min_contracts": 1,
|
|
"max_contracts": 10,
|
|
"risk_per_trade": 175,
|
|
"expected_contracts": 2,
|
|
"expected_risk": 120.0,
|
|
"calculation": "$175 / (12 ticks * $5.00) = 2.916... → 2 contracts (floor)"
|
|
},
|
|
{
|
|
"name": "CL with Min Clamp",
|
|
"symbol": "CL",
|
|
"stop_ticks": 20,
|
|
"method": "FixedDollarRisk",
|
|
"method_parameters": {},
|
|
"min_contracts": 3,
|
|
"max_contracts": 10,
|
|
"risk_per_trade": 150,
|
|
"expected_contracts": 3,
|
|
"expected_risk": 600.0,
|
|
"calculation": "$150 / (20 * $10) = 0.75 → clamped to min 3 contracts"
|
|
},
|
|
{
|
|
"name": "GC with Max Clamp",
|
|
"symbol": "GC",
|
|
"stop_ticks": 5,
|
|
"method": "FixedDollarRisk",
|
|
"method_parameters": {},
|
|
"min_contracts": 1,
|
|
"max_contracts": 2,
|
|
"risk_per_trade": 500,
|
|
"expected_contracts": 2,
|
|
"expected_risk": 100.0,
|
|
"calculation": "$500 / (5 * $10) = 10 → clamped to max 2 contracts"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## **VALIDATION SCRIPT**
|
|
|
|
### **File 4: `tools/validate-sizing-implementation.ps1`**
|
|
```powershell
|
|
# Position Sizing Validation Script
|
|
Write-Host "📏 Validating Position Sizing Implementation..." -ForegroundColor Yellow
|
|
|
|
# Build check
|
|
Write-Host "📦 Building solution..." -ForegroundColor Blue
|
|
$buildResult = dotnet build --configuration Release --verbosity quiet
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host "❌ Build failed" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# Test execution
|
|
Write-Host "🧪 Running position sizing tests..." -ForegroundColor Blue
|
|
$testResult = dotnet test tests/NT8.Core.Tests/NT8.Core.Tests.csproj --filter "Category=Sizing|FullyQualifiedName~Sizing" --configuration Release --verbosity quiet
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host "❌ Position sizing tests failed" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# Validate specific calculation examples
|
|
Write-Host "🔢 Validating calculation examples..." -ForegroundColor Blue
|
|
|
|
$calculationTests = @(
|
|
"BasicPositionSizerTests.CalculateSize_FixedContracts_ShouldReturnCorrectSize",
|
|
"BasicPositionSizerTests.CalculateSize_FixedDollarRisk_ShouldCalculateCorrectly",
|
|
"BasicPositionSizerTests.CalculateSize_FixedRiskVariousSymbols_ShouldCalculateCorrectly"
|
|
)
|
|
|
|
foreach ($test in $calculationTests) {
|
|
$result = dotnet test --filter "FullyQualifiedName~$test" --configuration Release --verbosity quiet
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Host " ✅ $test" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " ❌ $test" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Test configuration validation
|
|
Write-Host "⚙️ Testing configuration validation..." -ForegroundColor Blue
|
|
$configTests = @(
|
|
"BasicPositionSizerTests.ValidateConfig_ValidConfiguration_ShouldReturnTrue",
|
|
"BasicPositionSizerTests.ValidateConfig_InvalidConfiguration_ShouldReturnErrors"
|
|
)
|
|
|
|
foreach ($test in $configTests) {
|
|
$result = dotnet test --filter "FullyQualifiedName~$test" --configuration Release --verbosity quiet
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Host " ✅ $test" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " ❌ $test" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
Write-Host "🎉 Position sizing validation completed successfully!" -ForegroundColor Green
|
|
|
|
# Summary
|
|
Write-Host ""
|
|
Write-Host "📊 Position Sizing Implementation Summary:" -ForegroundColor Cyan
|
|
Write-Host " ✅ Fixed contracts sizing method" -ForegroundColor Green
|
|
Write-Host " ✅ Fixed dollar risk sizing method" -ForegroundColor Green
|
|
Write-Host " ✅ Contract clamping (min/max limits)" -ForegroundColor Green
|
|
Write-Host " ✅ Multi-symbol support with correct tick values" -ForegroundColor Green
|
|
Write-Host " ✅ Comprehensive error handling" -ForegroundColor Green
|
|
Write-Host " ✅ Configuration validation" -ForegroundColor Green
|
|
```
|
|
|
|
## **SUCCESS CRITERIA**
|
|
|
|
✅ **BasicPositionSizer.cs implemented exactly as specified**
|
|
✅ **Fixed contracts sizing method working correctly**
|
|
✅ **Fixed dollar risk sizing method with proper rounding**
|
|
✅ **Contract clamping applied (min/max limits)**
|
|
✅ **Multi-symbol support with accurate tick values**
|
|
✅ **Comprehensive test suite with >90% coverage**
|
|
✅ **All calculation examples produce exact expected results**
|
|
✅ **Configuration validation prevents invalid setups**
|
|
✅ **Error handling for edge cases (zero stops, invalid intents)**
|
|
|
|
## **CRITICAL REQUIREMENTS**
|
|
|
|
1. **Exact Calculations**: Must match calculation examples precisely
|
|
2. **Conservative Rounding**: Always round down (floor) for contract quantities
|
|
3. **Proper Clamping**: Apply min/max contract limits after calculation
|
|
4. **Symbol Support**: Support all specified symbols with correct tick values
|
|
5. **Error Handling**: Handle invalid inputs gracefully
|
|
6. **Deterministic**: Same inputs must always produce same outputs
|
|
|
|
**Once this is complete, position sizing is fully functional and ready for integration with the strategy framework.** |