diff --git a/src/NT8.Core/Sizing/AdvancedPositionSizer.cs b/src/NT8.Core/Sizing/AdvancedPositionSizer.cs index 4011ab8..af27346 100644 --- a/src/NT8.Core/Sizing/AdvancedPositionSizer.cs +++ b/src/NT8.Core/Sizing/AdvancedPositionSizer.cs @@ -1,6 +1,7 @@ using NT8.Core.Common.Models; using NT8.Core.Logging; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -14,6 +15,16 @@ namespace NT8.Core.Sizing { private readonly ILogger _logger; + // Performance metrics + private readonly SizingMetrics _metrics = new SizingMetrics(); + + // Object pools for frequently used objects + private readonly ConcurrentQueue> _dictionaryPool = new ConcurrentQueue>(); + private readonly ConcurrentQueue> _tradeListPool = new ConcurrentQueue>(); + + // Pool sizes + private const int MaxPoolSize = 100; + public AdvancedPositionSizer(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); @@ -26,36 +37,75 @@ namespace NT8.Core.Sizing if (context == null) throw new ArgumentNullException("context"); if (config == null) throw new ArgumentNullException("config"); + var startTime = DateTime.UtcNow; + // Validate intent is suitable for sizing if (!intent.IsValid()) { _logger.LogWarning("Invalid strategy intent provided for sizing: {0}", intent); - var errorCalcs = new Dictionary(); + Dictionary errorCalcs; + if (!_dictionaryPool.TryDequeue(out errorCalcs)) + { + errorCalcs = new Dictionary(); + } + errorCalcs.Clear(); errorCalcs.Add("error", "Invalid intent"); - return new SizingResult(0, 0, config.Method, errorCalcs); + var result = new SizingResult(0, 0, config.Method, errorCalcs); + + // Record metrics + var endTime = DateTime.UtcNow; + var processingTime = (endTime - startTime).TotalMilliseconds; + _metrics.RecordOperation(config.Method, (long)processingTime); + + return result; } + SizingResult sizingResult; + switch (config.Method) { case SizingMethod.OptimalF: - return CalculateOptimalF(intent, context, config); + sizingResult = CalculateOptimalF(intent, context, config); + break; case SizingMethod.KellyCriterion: - return CalculateKellyCriterion(intent, context, config); + sizingResult = CalculateKellyCriterion(intent, context, config); + break; case SizingMethod.VolatilityAdjusted: - return CalculateVolatilityAdjustedSizing(intent, context, config); + sizingResult = CalculateVolatilityAdjustedSizing(intent, context, config); + break; default: throw new NotSupportedException(String.Format("Sizing method {0} not supported in AdvancedPositionSizer", config.Method)); } + + // Record metrics + var endTime2 = DateTime.UtcNow; + var processingTime2 = (endTime2 - startTime).TotalMilliseconds; + _metrics.RecordOperation(config.Method, (long)processingTime2); + + return sizingResult; } private SizingResult CalculateOptimalF(StrategyIntent intent, StrategyContext context, SizingConfig config) { // Get trade history for calculating Optimal f - var tradeHistory = GetRecentTradeHistory(context, config); + List tradeHistory; + if (!_tradeListPool.TryDequeue(out tradeHistory)) + { + tradeHistory = new List(); + } + tradeHistory.Clear(); + tradeHistory.AddRange(GetRecentTradeHistory(context, config)); + if (tradeHistory.Count == 0) { + // Return trade history to pool + if (_tradeListPool.Count < MaxPoolSize) + { + _tradeListPool.Enqueue(tradeHistory); + } + // Fall back to fixed risk if no trade history return CalculateFixedRiskFallback(intent, context, config); } @@ -85,7 +135,12 @@ namespace NT8.Core.Sizing _logger.LogDebug("Optimal f sizing: {0} f={1:F4} ${2:F2}→{3:F2}→{4} contracts, ${5:F2} actual risk", intent.Symbol, optimalF, equity, optimalContracts, contracts, actualRisk); - var calculations = new Dictionary(); + Dictionary calculations; + if (!_dictionaryPool.TryDequeue(out calculations)) + { + calculations = new Dictionary(); + } + calculations.Clear(); calculations.Add("optimal_f", optimalF); calculations.Add("equity", equity); calculations.Add("max_loss", maxLoss); @@ -98,20 +153,41 @@ namespace NT8.Core.Sizing calculations.Add("min_contracts", config.MinContracts); calculations.Add("max_contracts", config.MaxContracts); - return new SizingResult( + // Return trade history to pool + if (_tradeListPool.Count < MaxPoolSize) + { + _tradeListPool.Enqueue(tradeHistory); + } + + var result = new SizingResult( contracts: contracts, riskAmount: actualRisk, method: SizingMethod.OptimalF, calculations: calculations ); + + return result; } private SizingResult CalculateKellyCriterion(StrategyIntent intent, StrategyContext context, SizingConfig config) { // Get trade history for calculating win rate and average win/loss - var tradeHistory = GetRecentTradeHistory(context, config); + List tradeHistory; + if (!_tradeListPool.TryDequeue(out tradeHistory)) + { + tradeHistory = new List(); + } + tradeHistory.Clear(); + tradeHistory.AddRange(GetRecentTradeHistory(context, config)); + if (tradeHistory.Count == 0) { + // Return trade history to pool + if (_tradeListPool.Count < MaxPoolSize) + { + _tradeListPool.Enqueue(tradeHistory); + } + // Fall back to fixed risk if no trade history return CalculateFixedRiskFallback(intent, context, config); } @@ -149,7 +225,12 @@ namespace NT8.Core.Sizing _logger.LogDebug("Kelly Criterion sizing: {0} K={1:F4} adj={2:F4} ${3:F2}→{4:F2}→{5} contracts, ${6:F2} actual risk", intent.Symbol, kellyFraction, adjustedKelly, equity, kellyContracts, contracts, actualRisk); - var calculations = new Dictionary(); + Dictionary calculations; + if (!_dictionaryPool.TryDequeue(out calculations)) + { + calculations = new Dictionary(); + } + calculations.Clear(); calculations.Add("win_rate", winRate); calculations.Add("avg_win", avgWin); calculations.Add("avg_loss", avgLoss); @@ -166,12 +247,20 @@ namespace NT8.Core.Sizing calculations.Add("min_contracts", config.MinContracts); calculations.Add("max_contracts", config.MaxContracts); - return new SizingResult( + // Return trade history to pool + if (_tradeListPool.Count < MaxPoolSize) + { + _tradeListPool.Enqueue(tradeHistory); + } + + var result = new SizingResult( contracts: contracts, riskAmount: actualRisk, method: SizingMethod.KellyCriterion, calculations: calculations ); + + return result; } private SizingResult CalculateVolatilityAdjustedSizing(StrategyIntent intent, StrategyContext context, SizingConfig config) @@ -202,7 +291,12 @@ namespace NT8.Core.Sizing _logger.LogDebug("Volatility-adjusted sizing: {0} ATR={1:F4} adj={2:F4} ${3:F2}→${4:F2}→{5} contracts, ${6:F2} actual risk", intent.Symbol, atr, volatilityAdjustment, baseRisk, adjustedRisk, contracts, actualRisk); - var calculations = new Dictionary(); + Dictionary calculations; + if (!_dictionaryPool.TryDequeue(out calculations)) + { + calculations = new Dictionary(); + } + calculations.Clear(); calculations.Add("atr", atr); calculations.Add("volatility_adjustment", volatilityAdjustment); calculations.Add("base_risk", baseRisk); @@ -216,12 +310,14 @@ namespace NT8.Core.Sizing calculations.Add("min_contracts", config.MinContracts); calculations.Add("max_contracts", config.MaxContracts); - return new SizingResult( + var result = new SizingResult( contracts: contracts, riskAmount: actualRisk, method: SizingMethod.VolatilityAdjusted, calculations: calculations ); + + return result; } private SizingResult CalculateFixedRiskFallback(StrategyIntent intent, StrategyContext context, SizingConfig config) @@ -234,11 +330,18 @@ namespace NT8.Core.Sizing _logger.LogWarning("Invalid stop ticks {0} for fixed risk sizing on {1}", intent.StopTicks, intent.Symbol); - var errorCalcs = new Dictionary(); + Dictionary errorCalcs; + if (!_dictionaryPool.TryDequeue(out errorCalcs)) + { + errorCalcs = new Dictionary(); + } + errorCalcs.Clear(); errorCalcs.Add("error", "Invalid stop ticks"); errorCalcs.Add("stop_ticks", intent.StopTicks); - return new SizingResult(0, 0, SizingMethod.FixedDollarRisk, errorCalcs); + var errorResult = new SizingResult(0, 0, SizingMethod.FixedDollarRisk, errorCalcs); + + return errorResult; } // Calculate optimal contracts for target risk @@ -258,7 +361,12 @@ namespace NT8.Core.Sizing _logger.LogDebug("Fixed risk fallback sizing: {0} ${1:F2}→{2:F2}→{3} contracts, ${4:F2} actual risk", intent.Symbol, targetRisk, optimalContracts, contracts, actualRisk); - var calculations = new Dictionary(); + Dictionary calculations; + if (!_dictionaryPool.TryDequeue(out calculations)) + { + calculations = new Dictionary(); + } + calculations.Clear(); calculations.Add("target_risk", targetRisk); calculations.Add("stop_ticks", intent.StopTicks); calculations.Add("tick_value", tickValue); @@ -269,12 +377,14 @@ namespace NT8.Core.Sizing calculations.Add("min_contracts", config.MinContracts); calculations.Add("max_contracts", config.MaxContracts); - return new SizingResult( + var result = new SizingResult( contracts: contracts, riskAmount: actualRisk, method: SizingMethod.FixedDollarRisk, calculations: calculations ); + + return result; } private static double CalculateOptimalFValue(List tradeHistory) @@ -425,6 +535,14 @@ namespace NT8.Core.Sizing ); } + /// + /// Get current performance metrics snapshot + /// + public SizingMetricsSnapshot GetMetricsSnapshot() + { + return _metrics.GetSnapshot(); + } + /// /// Validate sizing configuration parameters /// @@ -494,4 +612,97 @@ namespace NT8.Core.Sizing } } } + + /// + /// Performance metrics for sizing operations + /// + public class SizingMetrics + { + // Operation counters + public long TotalOperations { get; private set; } + public long OptimalFOperations { get; private set; } + public long KellyCriterionOperations { get; private set; } + public long VolatilityAdjustedOperations { get; private set; } + public long FallbackOperations { get; private set; } + + // Timing metrics + public long TotalProcessingTimeMs { get; private set; } + public long MaxProcessingTimeMs { get; private set; } + public long MinProcessingTimeMs { get; private set; } + + // Thread-safe counters + private readonly object _lock = new object(); + + public SizingMetrics() + { + MinProcessingTimeMs = long.MaxValue; + } + + public void RecordOperation(SizingMethod method, long processingTimeMs) + { + lock (_lock) + { + TotalOperations++; + TotalProcessingTimeMs += processingTimeMs; + + // Update min/max timing + if (processingTimeMs > MaxProcessingTimeMs) + MaxProcessingTimeMs = processingTimeMs; + if (processingTimeMs < MinProcessingTimeMs) + MinProcessingTimeMs = processingTimeMs; + + // Update method-specific counters + switch (method) + { + case SizingMethod.OptimalF: + OptimalFOperations++; + break; + case SizingMethod.KellyCriterion: + KellyCriterionOperations++; + break; + case SizingMethod.VolatilityAdjusted: + VolatilityAdjustedOperations++; + break; + case SizingMethod.FixedDollarRisk: + FallbackOperations++; + break; + } + } + } + + public SizingMetricsSnapshot GetSnapshot() + { + lock (_lock) + { + return new SizingMetricsSnapshot + { + TotalOperations = TotalOperations, + OptimalFOperations = OptimalFOperations, + KellyCriterionOperations = KellyCriterionOperations, + VolatilityAdjustedOperations = VolatilityAdjustedOperations, + FallbackOperations = FallbackOperations, + TotalProcessingTimeMs = TotalProcessingTimeMs, + MaxProcessingTimeMs = MaxProcessingTimeMs, + MinProcessingTimeMs = MinProcessingTimeMs, + AverageProcessingTimeMs = TotalOperations > 0 ? (double)TotalProcessingTimeMs / TotalOperations : 0 + }; + } + } + } + + /// + /// Snapshot of sizing metrics + /// + public class SizingMetricsSnapshot + { + public long TotalOperations { get; set; } + public long OptimalFOperations { get; set; } + public long KellyCriterionOperations { get; set; } + public long VolatilityAdjustedOperations { get; set; } + public long FallbackOperations { get; set; } + public long TotalProcessingTimeMs { get; set; } + public long MaxProcessingTimeMs { get; set; } + public long MinProcessingTimeMs { get; set; } + public double AverageProcessingTimeMs { get; set; } + } } diff --git a/tests/NT8.Core.Tests/Sizing/AdvancedPositionSizerPerformanceTests.cs b/tests/NT8.Core.Tests/Sizing/AdvancedPositionSizerPerformanceTests.cs new file mode 100644 index 0000000..749dd3d --- /dev/null +++ b/tests/NT8.Core.Tests/Sizing/AdvancedPositionSizerPerformanceTests.cs @@ -0,0 +1,299 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NT8.Core.Common.Models; +using NT8.Core.Logging; +using NT8.Core.Sizing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace NT8.Core.Tests.Sizing +{ + [TestClass] + public class AdvancedPositionSizerPerformanceTests + { + private TestLogger _logger; + private AdvancedPositionSizer _positionSizer; + + [TestInitialize] + public void TestInitialize() + { + _logger = new TestLogger(); + _positionSizer = new AdvancedPositionSizer(_logger); + } + + [TestMethod] + public void AdvancedPositionSizer_Performance_MetricsRecording() + { + // Arrange + var intent = CreateValidIntent(); + var context = CreateTestContext(); + var config = CreateTestSizingConfig(SizingMethod.OptimalF); + + // Act + var stopwatch = Stopwatch.StartNew(); + var result = _positionSizer.CalculateSize(intent, context, config); + stopwatch.Stop(); + + // Assert + Assert.IsNotNull(result); + Assert.IsTrue(result.Contracts >= 0); + + // Check that metrics were recorded + var metricsSnapshot = _positionSizer.GetMetricsSnapshot(); + Assert.IsNotNull(metricsSnapshot); + Assert.IsTrue(metricsSnapshot.TotalOperations >= 1); + Assert.IsTrue(metricsSnapshot.OptimalFOperations >= 1); + Assert.IsTrue(metricsSnapshot.TotalProcessingTimeMs >= 0); + Assert.IsTrue(metricsSnapshot.AverageProcessingTimeMs >= 0); + } + + [TestMethod] + public void AdvancedPositionSizer_Performance_ObjectPooling() + { + // Arrange + var intent = CreateValidIntent(); + var context = CreateTestContext(); + var config = CreateTestSizingConfig(SizingMethod.KellyCriterion); + + // Act & Assert + // Run multiple calculations to test object pooling + for (int i = 0; i < 100; i++) + { + var result = _positionSizer.CalculateSize(intent, context, config); + Assert.IsNotNull(result); + Assert.IsTrue(result.Contracts >= 0); + } + + // Check that we still have reasonable performance + var metricsSnapshot = _positionSizer.GetMetricsSnapshot(); + Assert.IsTrue(metricsSnapshot.TotalOperations >= 100); + Assert.IsTrue(metricsSnapshot.AverageProcessingTimeMs < 100); // Should be fast with pooling + } + + [TestMethod] + public async Task AdvancedPositionSizer_Performance_ConcurrentAccess() + { + // Arrange + var tasks = new List>(); + var intent = CreateValidIntent(); + var context = CreateTestContext(); + var config = CreateTestSizingConfig(SizingMethod.VolatilityAdjusted); + + // Act + var stopwatch = Stopwatch.StartNew(); + for (int i = 0; i < 50; i++) + { + var task = Task.Run(() => _positionSizer.CalculateSize(intent, context, config)); + tasks.Add(task); + } + + var results = await Task.WhenAll(tasks); + stopwatch.Stop(); + + // Assert + Assert.AreEqual(50, results.Length); + foreach (var result in results) + { + Assert.IsNotNull(result); + Assert.IsTrue(result.Contracts >= 0); + } + + // Check metrics + var metricsSnapshot = _positionSizer.GetMetricsSnapshot(); + Assert.IsTrue(metricsSnapshot.TotalOperations >= 50); + Assert.IsTrue(metricsSnapshot.VolatilityAdjustedOperations >= 50); + } + + [TestMethod] + public void AdvancedPositionSizer_Performance_Throughput() + { + // Arrange + var intent = CreateValidIntent(); + var context = CreateTestContext(); + var config = CreateTestSizingConfig(SizingMethod.OptimalF); + + // Act + var stopwatch = Stopwatch.StartNew(); + const int iterations = 1000; + + for (int i = 0; i < iterations; i++) + { + var result = _positionSizer.CalculateSize(intent, context, config); + Assert.IsNotNull(result); + } + + stopwatch.Stop(); + + // Assert + var metricsSnapshot = _positionSizer.GetMetricsSnapshot(); + Assert.IsTrue(metricsSnapshot.TotalOperations >= iterations); + + // Calculate throughput + var throughput = (double)iterations / stopwatch.Elapsed.TotalSeconds; + Assert.IsTrue(throughput > 100); // Should process at least 100 operations per second + + _logger.LogInformation(String.Format("Processed {0} operations in {1:F2} ms ({2:F2} ops/sec)", + iterations, stopwatch.Elapsed.TotalMilliseconds, throughput)); + } + + [TestMethod] + public void AdvancedPositionSizer_Performance_MemoryAllocation() + { + // Arrange + var intent = CreateValidIntent(); + var context = CreateTestContext(); + var config = CreateTestSizingConfig(SizingMethod.KellyCriterion); + + // Force garbage collection to get a clean baseline + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var initialMemory = GC.GetTotalMemory(false); + + // Act + for (int i = 0; i < 1000; i++) + { + var result = _positionSizer.CalculateSize(intent, context, config); + Assert.IsNotNull(result); + } + + // Force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + var finalMemory = GC.GetTotalMemory(false); + + // Assert - with object pooling, memory growth should be minimal + var memoryGrowth = finalMemory - initialMemory; + Assert.IsTrue(memoryGrowth < 10 * 1024 * 1024); // Less than 10MB growth for 1000 operations + + _logger.LogInformation(String.Format("Memory growth: {0:F2} KB for 1000 operations", + memoryGrowth / 1024.0)); + } + + [TestMethod] + public void AdvancedPositionSizer_Performance_MetricsConsistency() + { + // Arrange + var intent = CreateValidIntent(); + var context = CreateTestContext(); + var config1 = CreateTestSizingConfig(SizingMethod.OptimalF); + var config2 = CreateTestSizingConfig(SizingMethod.KellyCriterion); + var config3 = CreateTestSizingConfig(SizingMethod.VolatilityAdjusted); + + // Act + // Run mixed operations + for (int i = 0; i < 30; i++) + { + _positionSizer.CalculateSize(intent, context, config1); + _positionSizer.CalculateSize(intent, context, config2); + _positionSizer.CalculateSize(intent, context, config3); + } + + // Assert + var metricsSnapshot = _positionSizer.GetMetricsSnapshot(); + Assert.IsTrue(metricsSnapshot.TotalOperations >= 90); + Assert.IsTrue(metricsSnapshot.OptimalFOperations >= 30); + Assert.IsTrue(metricsSnapshot.KellyCriterionOperations >= 30); + Assert.IsTrue(metricsSnapshot.VolatilityAdjustedOperations >= 30); + + // Verify timing metrics are reasonable + Assert.IsTrue(metricsSnapshot.MaxProcessingTimeMs >= metricsSnapshot.MinProcessingTimeMs); + Assert.IsTrue(metricsSnapshot.AverageProcessingTimeMs >= 0); + } + + #region Helper Methods + + private 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() + ); + } + + private 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() + ); + } + + private SizingConfig CreateTestSizingConfig(SizingMethod method) + { + var methodParameters = new Dictionary(); + if (method == SizingMethod.KellyCriterion) + { + methodParameters.Add("kelly_fraction", 0.5); + } + + return new SizingConfig( + method: method, + minContracts: 1, + maxContracts: 10, + riskPerTrade: 500, + methodParameters: methodParameters + ); + } + + #endregion + } + + /// + /// Test implementation of ILogger for testing + /// + public class TestLogger : ILogger + { + public void LogCritical(string message, params object[] args) + { + // No-op for testing + } + + public void LogDebug(string message, params object[] args) + { + // No-op for testing + } + + public void LogError(string message, params object[] args) + { + // No-op for testing + } + + public void LogError(Exception exception, string message, params object[] args) + { + // No-op for testing + } + + public void LogInformation(string message, params object[] args) + { + // No-op for testing + Console.WriteLine("[INFO] " + message, args); + } + + public void LogWarning(string message, params object[] args) + { + // No-op for testing + } + + public bool IsEnabled(int logLevel) + { + return true; + } + } +}