Compare commits
1 Commits
86422ff540
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c48a2ad05 |
@@ -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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user