Optimize AdvancedPositionSizer performance with object pooling and metrics tracking. Added performance tests.
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
Billy Valentine
2025-09-09 19:48:06 -04:00
parent 86422ff540
commit 6c48a2ad05
2 changed files with 527 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
using NT8.Core.Common.Models; using NT8.Core.Common.Models;
using NT8.Core.Logging; using NT8.Core.Logging;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -14,6 +15,16 @@ namespace NT8.Core.Sizing
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
// Performance metrics
private readonly SizingMetrics _metrics = new SizingMetrics();
// Object pools for frequently used objects
private readonly ConcurrentQueue<Dictionary<string, object>> _dictionaryPool = new ConcurrentQueue<Dictionary<string, object>>();
private readonly ConcurrentQueue<List<TradeResult>> _tradeListPool = new ConcurrentQueue<List<TradeResult>>();
// Pool sizes
private const int MaxPoolSize = 100;
public AdvancedPositionSizer(ILogger logger) public AdvancedPositionSizer(ILogger logger)
{ {
if (logger == null) throw new ArgumentNullException("logger"); if (logger == null) throw new ArgumentNullException("logger");
@@ -26,36 +37,75 @@ namespace NT8.Core.Sizing
if (context == null) throw new ArgumentNullException("context"); if (context == null) throw new ArgumentNullException("context");
if (config == null) throw new ArgumentNullException("config"); if (config == null) throw new ArgumentNullException("config");
var startTime = DateTime.UtcNow;
// Validate intent is suitable for sizing // Validate intent is suitable for sizing
if (!intent.IsValid()) if (!intent.IsValid())
{ {
_logger.LogWarning("Invalid strategy intent provided for sizing: {0}", intent); _logger.LogWarning("Invalid strategy intent provided for sizing: {0}", intent);
var errorCalcs = new Dictionary<string, object>(); Dictionary<string, object> errorCalcs;
if (!_dictionaryPool.TryDequeue(out errorCalcs))
{
errorCalcs = new Dictionary<string, object>();
}
errorCalcs.Clear();
errorCalcs.Add("error", "Invalid intent"); 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) switch (config.Method)
{ {
case SizingMethod.OptimalF: case SizingMethod.OptimalF:
return CalculateOptimalF(intent, context, config); sizingResult = CalculateOptimalF(intent, context, config);
break;
case SizingMethod.KellyCriterion: case SizingMethod.KellyCriterion:
return CalculateKellyCriterion(intent, context, config); sizingResult = CalculateKellyCriterion(intent, context, config);
break;
case SizingMethod.VolatilityAdjusted: case SizingMethod.VolatilityAdjusted:
return CalculateVolatilityAdjustedSizing(intent, context, config); sizingResult = CalculateVolatilityAdjustedSizing(intent, context, config);
break;
default: default:
throw new NotSupportedException(String.Format("Sizing method {0} not supported in AdvancedPositionSizer", config.Method)); 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) private SizingResult CalculateOptimalF(StrategyIntent intent, StrategyContext context, SizingConfig config)
{ {
// Get trade history for calculating Optimal f // Get trade history for calculating Optimal f
var tradeHistory = GetRecentTradeHistory(context, config); List<TradeResult> tradeHistory;
if (!_tradeListPool.TryDequeue(out tradeHistory))
{
tradeHistory = new List<TradeResult>();
}
tradeHistory.Clear();
tradeHistory.AddRange(GetRecentTradeHistory(context, config));
if (tradeHistory.Count == 0) 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 // Fall back to fixed risk if no trade history
return CalculateFixedRiskFallback(intent, context, config); 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", _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); intent.Symbol, optimalF, equity, optimalContracts, contracts, actualRisk);
var calculations = new Dictionary<string, object>(); Dictionary<string, object> calculations;
if (!_dictionaryPool.TryDequeue(out calculations))
{
calculations = new Dictionary<string, object>();
}
calculations.Clear();
calculations.Add("optimal_f", optimalF); calculations.Add("optimal_f", optimalF);
calculations.Add("equity", equity); calculations.Add("equity", equity);
calculations.Add("max_loss", maxLoss); calculations.Add("max_loss", maxLoss);
@@ -98,20 +153,41 @@ namespace NT8.Core.Sizing
calculations.Add("min_contracts", config.MinContracts); calculations.Add("min_contracts", config.MinContracts);
calculations.Add("max_contracts", config.MaxContracts); 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, contracts: contracts,
riskAmount: actualRisk, riskAmount: actualRisk,
method: SizingMethod.OptimalF, method: SizingMethod.OptimalF,
calculations: calculations calculations: calculations
); );
return result;
} }
private SizingResult CalculateKellyCriterion(StrategyIntent intent, StrategyContext context, SizingConfig config) private SizingResult CalculateKellyCriterion(StrategyIntent intent, StrategyContext context, SizingConfig config)
{ {
// Get trade history for calculating win rate and average win/loss // Get trade history for calculating win rate and average win/loss
var tradeHistory = GetRecentTradeHistory(context, config); List<TradeResult> tradeHistory;
if (!_tradeListPool.TryDequeue(out tradeHistory))
{
tradeHistory = new List<TradeResult>();
}
tradeHistory.Clear();
tradeHistory.AddRange(GetRecentTradeHistory(context, config));
if (tradeHistory.Count == 0) 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 // Fall back to fixed risk if no trade history
return CalculateFixedRiskFallback(intent, context, config); 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", _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); intent.Symbol, kellyFraction, adjustedKelly, equity, kellyContracts, contracts, actualRisk);
var calculations = new Dictionary<string, object>(); Dictionary<string, object> calculations;
if (!_dictionaryPool.TryDequeue(out calculations))
{
calculations = new Dictionary<string, object>();
}
calculations.Clear();
calculations.Add("win_rate", winRate); calculations.Add("win_rate", winRate);
calculations.Add("avg_win", avgWin); calculations.Add("avg_win", avgWin);
calculations.Add("avg_loss", avgLoss); calculations.Add("avg_loss", avgLoss);
@@ -166,12 +247,20 @@ namespace NT8.Core.Sizing
calculations.Add("min_contracts", config.MinContracts); calculations.Add("min_contracts", config.MinContracts);
calculations.Add("max_contracts", config.MaxContracts); 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, contracts: contracts,
riskAmount: actualRisk, riskAmount: actualRisk,
method: SizingMethod.KellyCriterion, method: SizingMethod.KellyCriterion,
calculations: calculations calculations: calculations
); );
return result;
} }
private SizingResult CalculateVolatilityAdjustedSizing(StrategyIntent intent, StrategyContext context, SizingConfig config) 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", _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); intent.Symbol, atr, volatilityAdjustment, baseRisk, adjustedRisk, contracts, actualRisk);
var calculations = new Dictionary<string, object>(); Dictionary<string, object> calculations;
if (!_dictionaryPool.TryDequeue(out calculations))
{
calculations = new Dictionary<string, object>();
}
calculations.Clear();
calculations.Add("atr", atr); calculations.Add("atr", atr);
calculations.Add("volatility_adjustment", volatilityAdjustment); calculations.Add("volatility_adjustment", volatilityAdjustment);
calculations.Add("base_risk", baseRisk); calculations.Add("base_risk", baseRisk);
@@ -216,12 +310,14 @@ namespace NT8.Core.Sizing
calculations.Add("min_contracts", config.MinContracts); calculations.Add("min_contracts", config.MinContracts);
calculations.Add("max_contracts", config.MaxContracts); calculations.Add("max_contracts", config.MaxContracts);
return new SizingResult( var result = new SizingResult(
contracts: contracts, contracts: contracts,
riskAmount: actualRisk, riskAmount: actualRisk,
method: SizingMethod.VolatilityAdjusted, method: SizingMethod.VolatilityAdjusted,
calculations: calculations calculations: calculations
); );
return result;
} }
private SizingResult CalculateFixedRiskFallback(StrategyIntent intent, StrategyContext context, SizingConfig config) 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}", _logger.LogWarning("Invalid stop ticks {0} for fixed risk sizing on {1}",
intent.StopTicks, intent.Symbol); intent.StopTicks, intent.Symbol);
var errorCalcs = new Dictionary<string, object>(); Dictionary<string, object> errorCalcs;
if (!_dictionaryPool.TryDequeue(out errorCalcs))
{
errorCalcs = new Dictionary<string, object>();
}
errorCalcs.Clear();
errorCalcs.Add("error", "Invalid stop ticks"); errorCalcs.Add("error", "Invalid stop ticks");
errorCalcs.Add("stop_ticks", intent.StopTicks); 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 // 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", _logger.LogDebug("Fixed risk fallback sizing: {0} ${1:F2}→{2:F2}→{3} contracts, ${4:F2} actual risk",
intent.Symbol, targetRisk, optimalContracts, contracts, actualRisk); intent.Symbol, targetRisk, optimalContracts, contracts, actualRisk);
var calculations = new Dictionary<string, object>(); Dictionary<string, object> calculations;
if (!_dictionaryPool.TryDequeue(out calculations))
{
calculations = new Dictionary<string, object>();
}
calculations.Clear();
calculations.Add("target_risk", targetRisk); calculations.Add("target_risk", targetRisk);
calculations.Add("stop_ticks", intent.StopTicks); calculations.Add("stop_ticks", intent.StopTicks);
calculations.Add("tick_value", tickValue); calculations.Add("tick_value", tickValue);
@@ -269,12 +377,14 @@ namespace NT8.Core.Sizing
calculations.Add("min_contracts", config.MinContracts); calculations.Add("min_contracts", config.MinContracts);
calculations.Add("max_contracts", config.MaxContracts); calculations.Add("max_contracts", config.MaxContracts);
return new SizingResult( var result = new SizingResult(
contracts: contracts, contracts: contracts,
riskAmount: actualRisk, riskAmount: actualRisk,
method: SizingMethod.FixedDollarRisk, method: SizingMethod.FixedDollarRisk,
calculations: calculations calculations: calculations
); );
return result;
} }
private static double CalculateOptimalFValue(List<TradeResult> tradeHistory) private static double CalculateOptimalFValue(List<TradeResult> tradeHistory)
@@ -425,6 +535,14 @@ namespace NT8.Core.Sizing
); );
} }
/// <summary>
/// Get current performance metrics snapshot
/// </summary>
public SizingMetricsSnapshot GetMetricsSnapshot()
{
return _metrics.GetSnapshot();
}
/// <summary> /// <summary>
/// Validate sizing configuration parameters /// Validate sizing configuration parameters
/// </summary> /// </summary>
@@ -494,4 +612,97 @@ namespace NT8.Core.Sizing
} }
} }
} }
/// <summary>
/// Performance metrics for sizing operations
/// </summary>
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
};
}
}
}
/// <summary>
/// Snapshot of sizing metrics
/// </summary>
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; }
}
} }

View File

@@ -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<Task<SizingResult>>();
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<string, object>()
);
}
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<string, object>()
);
}
private SizingConfig CreateTestSizingConfig(SizingMethod method)
{
var methodParameters = new Dictionary<string, object>();
if (method == SizingMethod.KellyCriterion)
{
methodParameters.Add("kelly_fraction", 0.5);
}
return new SizingConfig(
method: method,
minContracts: 1,
maxContracts: 10,
riskPerTrade: 500,
methodParameters: methodParameters
);
}
#endregion
}
/// <summary>
/// Test implementation of ILogger for testing
/// </summary>
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;
}
}
}