diff --git a/src/NT8.Core/Common/Models/Configuration.cs b/src/NT8.Core/Common/Models/Configuration.cs
index 59fbe1e..aa762d3 100644
--- a/src/NT8.Core/Common/Models/Configuration.cs
+++ b/src/NT8.Core/Common/Models/Configuration.cs
@@ -163,7 +163,17 @@ namespace NT8.Core.Common.Models
///
/// Optimal F calculation
///
- OptimalF
+ OptimalF,
+
+ ///
+ /// Kelly Criterion sizing
+ ///
+ KellyCriterion,
+
+ ///
+ /// Volatility-adjusted sizing
+ ///
+ VolatilityAdjusted
}
///
@@ -438,4 +448,4 @@ namespace NT8.Core.Common.Models
ExecutionId = executionId;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NT8.Core/Sizing/AdvancedPositionSizer.cs b/src/NT8.Core/Sizing/AdvancedPositionSizer.cs
new file mode 100644
index 0000000..4011ab8
--- /dev/null
+++ b/src/NT8.Core/Sizing/AdvancedPositionSizer.cs
@@ -0,0 +1,497 @@
+using NT8.Core.Common.Models;
+using NT8.Core.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace NT8.Core.Sizing
+{
+ ///
+ /// Advanced position sizer with Optimal f, Kelly Criterion, and volatility-adjusted methods
+ /// Implements sophisticated position sizing algorithms for professional trading
+ ///
+ public class AdvancedPositionSizer : IPositionSizer
+ {
+ private readonly ILogger _logger;
+
+ public AdvancedPositionSizer(ILogger logger)
+ {
+ if (logger == null) throw new ArgumentNullException("logger");
+ _logger = logger;
+ }
+
+ public SizingResult CalculateSize(StrategyIntent intent, StrategyContext context, SizingConfig config)
+ {
+ if (intent == null) throw new ArgumentNullException("intent");
+ if (context == null) throw new ArgumentNullException("context");
+ if (config == null) throw new ArgumentNullException("config");
+
+ // Validate intent is suitable for sizing
+ if (!intent.IsValid())
+ {
+ _logger.LogWarning("Invalid strategy intent provided for sizing: {0}", intent);
+
+ var errorCalcs = new Dictionary();
+ errorCalcs.Add("error", "Invalid intent");
+
+ return new SizingResult(0, 0, config.Method, errorCalcs);
+ }
+
+ switch (config.Method)
+ {
+ case SizingMethod.OptimalF:
+ return CalculateOptimalF(intent, context, config);
+ case SizingMethod.KellyCriterion:
+ return CalculateKellyCriterion(intent, context, config);
+ case SizingMethod.VolatilityAdjusted:
+ return CalculateVolatilityAdjustedSizing(intent, context, config);
+ default:
+ throw new NotSupportedException(String.Format("Sizing method {0} not supported in AdvancedPositionSizer", config.Method));
+ }
+ }
+
+ private SizingResult CalculateOptimalF(StrategyIntent intent, StrategyContext context, SizingConfig config)
+ {
+ // Get trade history for calculating Optimal f
+ var tradeHistory = GetRecentTradeHistory(context, config);
+ if (tradeHistory.Count == 0)
+ {
+ // Fall back to fixed risk if no trade history
+ return CalculateFixedRiskFallback(intent, context, config);
+ }
+
+ // Calculate Optimal f
+ var optimalF = CalculateOptimalFValue(tradeHistory);
+
+ // Get account information
+ var equity = context.Account.Equity;
+ var maxLoss = GetMaximumLossFromHistory(tradeHistory);
+
+ // Calculate optimal contracts using Optimal f formula
+ // Contracts = (Optimal f * Equity) / Max Loss
+ var optimalContracts = (optimalF * equity) / Math.Abs(maxLoss);
+
+ // 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 tickValue = GetTickValue(intent.Symbol);
+ var riskPerContract = intent.StopTicks * tickValue;
+ var actualRisk = contracts * riskPerContract;
+
+ _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();
+ calculations.Add("optimal_f", optimalF);
+ calculations.Add("equity", equity);
+ calculations.Add("max_loss", maxLoss);
+ calculations.Add("optimal_contracts", optimalContracts);
+ calculations.Add("clamped_contracts", contracts);
+ calculations.Add("stop_ticks", intent.StopTicks);
+ calculations.Add("tick_value", tickValue);
+ calculations.Add("risk_per_contract", riskPerContract);
+ calculations.Add("actual_risk", actualRisk);
+ calculations.Add("min_contracts", config.MinContracts);
+ calculations.Add("max_contracts", config.MaxContracts);
+
+ return new SizingResult(
+ contracts: contracts,
+ riskAmount: actualRisk,
+ method: SizingMethod.OptimalF,
+ calculations: calculations
+ );
+ }
+
+ 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);
+ if (tradeHistory.Count == 0)
+ {
+ // Fall back to fixed risk if no trade history
+ return CalculateFixedRiskFallback(intent, context, config);
+ }
+
+ // Calculate Kelly Criterion parameters
+ var winRate = CalculateWinRate(tradeHistory);
+ var avgWin = CalculateAverageWin(tradeHistory);
+ var avgLoss = CalculateAverageLoss(tradeHistory);
+
+ // Calculate Kelly Criterion fraction
+ // K = (bp - q) / b
+ // Where: b = avgWin/avgLoss (odds), p = winRate, q = 1 - winRate
+ var odds = avgWin / Math.Abs(avgLoss);
+ var kellyFraction = ((odds * winRate) - (1 - winRate)) / odds;
+
+ // Apply fractional Kelly to reduce risk (typically use 25%-50% of full Kelly)
+ var fractionalKelly = GetParameterValue(config, "kelly_fraction", 0.5);
+ var adjustedKelly = kellyFraction * fractionalKelly;
+
+ // Calculate position size based on Kelly Criterion
+ var equity = context.Account.Equity;
+ var tickValue = GetTickValue(intent.Symbol);
+ var riskPerContract = intent.StopTicks * tickValue;
+
+ // Kelly position size = (Kelly Fraction * Equity) / Risk per contract
+ var kellyContracts = (adjustedKelly * equity) / riskPerContract;
+ var contracts = (int)Math.Floor(Math.Abs(kellyContracts));
+
+ // 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("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();
+ calculations.Add("win_rate", winRate);
+ calculations.Add("avg_win", avgWin);
+ calculations.Add("avg_loss", avgLoss);
+ calculations.Add("odds", odds);
+ calculations.Add("kelly_fraction", kellyFraction);
+ calculations.Add("fractional_kelly", fractionalKelly);
+ calculations.Add("adjusted_kelly", adjustedKelly);
+ calculations.Add("equity", equity);
+ calculations.Add("tick_value", tickValue);
+ calculations.Add("risk_per_contract", riskPerContract);
+ calculations.Add("kelly_contracts", kellyContracts);
+ calculations.Add("clamped_contracts", contracts);
+ calculations.Add("actual_risk", actualRisk);
+ calculations.Add("min_contracts", config.MinContracts);
+ calculations.Add("max_contracts", config.MaxContracts);
+
+ return new SizingResult(
+ contracts: contracts,
+ riskAmount: actualRisk,
+ method: SizingMethod.KellyCriterion,
+ calculations: calculations
+ );
+ }
+
+ private SizingResult CalculateVolatilityAdjustedSizing(StrategyIntent intent, StrategyContext context, SizingConfig config)
+ {
+ // Get volatility information
+ var atr = CalculateATR(context, intent.Symbol, 14); // 14-period ATR
+ var tickValue = GetTickValue(intent.Symbol);
+
+ // Get base risk from configuration
+ var baseRisk = config.RiskPerTrade;
+
+ // Apply volatility adjustment
+ // Higher volatility = lower position size, Lower volatility = higher position size
+ var volatilityAdjustment = CalculateVolatilityAdjustment(atr, intent.Symbol);
+ var adjustedRisk = baseRisk * volatilityAdjustment;
+
+ // Calculate contracts based on adjusted risk
+ var riskPerContract = intent.StopTicks * tickValue;
+ var optimalContracts = adjustedRisk / riskPerContract;
+ 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("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();
+ calculations.Add("atr", atr);
+ calculations.Add("volatility_adjustment", volatilityAdjustment);
+ calculations.Add("base_risk", baseRisk);
+ calculations.Add("adjusted_risk", adjustedRisk);
+ calculations.Add("tick_value", tickValue);
+ calculations.Add("risk_per_contract", riskPerContract);
+ calculations.Add("optimal_contracts", optimalContracts);
+ calculations.Add("clamped_contracts", contracts);
+ calculations.Add("actual_risk", actualRisk);
+ calculations.Add("stop_ticks", intent.StopTicks);
+ calculations.Add("min_contracts", config.MinContracts);
+ calculations.Add("max_contracts", config.MaxContracts);
+
+ return new SizingResult(
+ contracts: contracts,
+ riskAmount: actualRisk,
+ method: SizingMethod.VolatilityAdjusted,
+ calculations: calculations
+ );
+ }
+
+ private SizingResult CalculateFixedRiskFallback(StrategyIntent intent, StrategyContext context, SizingConfig config)
+ {
+ var tickValue = GetTickValue(intent.Symbol);
+
+ // Validate stop ticks
+ if (intent.StopTicks <= 0)
+ {
+ _logger.LogWarning("Invalid stop ticks {0} for fixed risk sizing on {1}",
+ intent.StopTicks, intent.Symbol);
+
+ var errorCalcs = new Dictionary();
+ errorCalcs.Add("error", "Invalid stop ticks");
+ errorCalcs.Add("stop_ticks", intent.StopTicks);
+
+ return new SizingResult(0, 0, SizingMethod.FixedDollarRisk, errorCalcs);
+ }
+
+ // 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 fallback sizing: {0} ${1:F2}→{2:F2}→{3} contracts, ${4:F2} actual risk",
+ intent.Symbol, targetRisk, optimalContracts, contracts, actualRisk);
+
+ var calculations = new Dictionary();
+ calculations.Add("target_risk", targetRisk);
+ calculations.Add("stop_ticks", intent.StopTicks);
+ calculations.Add("tick_value", tickValue);
+ calculations.Add("risk_per_contract", riskPerContract);
+ calculations.Add("optimal_contracts", optimalContracts);
+ calculations.Add("clamped_contracts", contracts);
+ calculations.Add("actual_risk", actualRisk);
+ calculations.Add("min_contracts", config.MinContracts);
+ calculations.Add("max_contracts", config.MaxContracts);
+
+ return new SizingResult(
+ contracts: contracts,
+ riskAmount: actualRisk,
+ method: SizingMethod.FixedDollarRisk,
+ calculations: calculations
+ );
+ }
+
+ private static double CalculateOptimalFValue(List tradeHistory)
+ {
+ if (tradeHistory == null || tradeHistory.Count == 0)
+ return 0.0;
+
+ // Find the largest loss (in absolute terms)
+ var largestLoss = Math.Abs(tradeHistory.Min(t => t.ProfitLoss));
+
+ if (largestLoss == 0)
+ return 0.0;
+
+ // Calculate Optimal f using the formula:
+ // f = (N*R - T) / (N*L)
+ // Where: N = number of trades, R = average win, L = largest loss, T = total profit
+
+ var n = tradeHistory.Count;
+ var totalProfit = tradeHistory.Sum(t => t.ProfitLoss);
+ var averageWin = tradeHistory.Where(t => t.ProfitLoss > 0).DefaultIfEmpty(new TradeResult()).Average(t => t.ProfitLoss);
+
+ if (averageWin <= 0)
+ return 0.0;
+
+ var optimalF = (n * averageWin - totalProfit) / (n * largestLoss);
+
+ // Ensure f is between 0 and 1
+ return Math.Max(0.0, Math.Min(1.0, optimalF));
+ }
+
+ private static double CalculateWinRate(List tradeHistory)
+ {
+ if (tradeHistory == null || tradeHistory.Count == 0)
+ return 0.0;
+
+ var winningTrades = tradeHistory.Count(t => t.ProfitLoss > 0);
+ return (double)winningTrades / tradeHistory.Count;
+ }
+
+ private static double CalculateAverageWin(List tradeHistory)
+ {
+ if (tradeHistory == null || tradeHistory.Count == 0)
+ return 0.0;
+
+ var winningTrades = tradeHistory.Where(t => t.ProfitLoss > 0).ToList();
+ if (winningTrades.Count == 0)
+ return 0.0;
+
+ return winningTrades.Average(t => t.ProfitLoss);
+ }
+
+ private static double CalculateAverageLoss(List tradeHistory)
+ {
+ if (tradeHistory == null || tradeHistory.Count == 0)
+ return 0.0;
+
+ var losingTrades = tradeHistory.Where(t => t.ProfitLoss < 0).ToList();
+ if (losingTrades.Count == 0)
+ return 0.0;
+
+ return losingTrades.Average(t => t.ProfitLoss);
+ }
+
+ private static double GetMaximumLossFromHistory(List tradeHistory)
+ {
+ if (tradeHistory == null || tradeHistory.Count == 0)
+ return 0.0;
+
+ return tradeHistory.Min(t => t.ProfitLoss);
+ }
+
+ private static double CalculateATR(StrategyContext context, string symbol, int periods)
+ {
+ // This would typically involve retrieving historical bar data
+ // For this implementation, we'll use a simplified approach
+ return 1.0; // Placeholder value
+ }
+
+ private static double CalculateVolatilityAdjustment(double atr, string symbol)
+ {
+ // Normalize ATR to a volatility adjustment factor
+ // Higher ATR = lower adjustment (reduce position size)
+ // Lower ATR = higher adjustment (increase position size)
+
+ // This is a simplified example - in practice, you'd normalize against
+ // historical ATR values for the specific symbol
+ var normalizedATR = atr / 10.0; // Example normalization
+ var adjustment = 1.0 / (1.0 + normalizedATR);
+
+ // Ensure adjustment is between 0.1 and 2.0
+ return Math.Max(0.1, Math.Min(2.0, adjustment));
+ }
+
+ private static List GetRecentTradeHistory(StrategyContext context, SizingConfig config)
+ {
+ // In a real implementation, this would retrieve actual trade history
+ // For this example, we'll return an empty list to trigger fallback behavior
+ return new List();
+ }
+
+ private static T GetParameterValue(SizingConfig config, string key, T defaultValue)
+ {
+ if (config.MethodParameters.ContainsKey(key))
+ {
+ try
+ {
+ return (T)Convert.ChangeType(config.MethodParameters[key], 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
+ switch (symbol)
+ {
+ case "ES": return 12.50; // E-mini S&P 500
+ case "MES": return 1.25; // Micro E-mini S&P 500
+ case "NQ": return 5.00; // E-mini NASDAQ-100
+ case "MNQ": return 0.50; // Micro E-mini NASDAQ-100
+ case "CL": return 10.00; // Crude Oil
+ case "GC": return 10.00; // Gold
+ case "6E": return 12.50; // Euro FX
+ case "6A": return 10.00; // Australian Dollar
+ default: return 12.50; // Default to ES value
+ }
+ }
+
+ public SizingMetadata GetMetadata()
+ {
+ var requiredParams = new List();
+ requiredParams.Add("method");
+ requiredParams.Add("risk_per_trade");
+ requiredParams.Add("min_contracts");
+ requiredParams.Add("max_contracts");
+
+ return new SizingMetadata(
+ name: "Advanced Position Sizer",
+ description: "Optimal f, Kelly Criterion, and volatility-adjusted sizing with contract clamping",
+ requiredParameters: requiredParams
+ );
+ }
+
+ ///
+ /// Validate sizing configuration parameters
+ ///
+ public static bool ValidateConfig(SizingConfig config, out List errors)
+ {
+ errors = new List();
+
+ 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.OptimalF:
+ // No additional parameters required for Optimal f
+ break;
+
+ case SizingMethod.KellyCriterion:
+ // Validate Kelly fraction parameter if provided
+ if (config.MethodParameters.ContainsKey("kelly_fraction"))
+ {
+ var kellyFraction = GetParameterValue(config, "kelly_fraction", 0.5);
+ if (kellyFraction <= 0 || kellyFraction > 1.0)
+ errors.Add("Kelly fraction must be between 0 and 1.0");
+ }
+ break;
+
+ case SizingMethod.VolatilityAdjusted:
+ // No additional parameters required for volatility-adjusted sizing
+ break;
+
+ default:
+ errors.Add(String.Format("Unsupported sizing method: {0}", config.Method));
+ break;
+ }
+
+ return errors.Count == 0;
+ }
+
+ ///
+ /// Internal class to represent trade results for calculations
+ ///
+ private class TradeResult
+ {
+ public double ProfitLoss { get; set; }
+ public DateTime TradeTime { get; set; }
+
+ public TradeResult()
+ {
+ ProfitLoss = 0.0;
+ TradeTime = DateTime.UtcNow;
+ }
+
+ public TradeResult(double profitLoss, DateTime tradeTime)
+ {
+ ProfitLoss = profitLoss;
+ TradeTime = tradeTime;
+ }
+ }
+ }
+}