Files
nt8-sdk/Specs/SDK/position_sizing_package.md
Billy Valentine 92f3732b3d
Some checks failed
Build and Test / build (push) Has been cancelled
Phase 0 completion: NT8 SDK core framework with risk management and position sizing
2025-09-09 17:06:37 -04:00

28 KiB

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

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

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

{
  "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

# 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.