using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using NT8.Core.Common.Models; using NT8.Core.Logging; namespace NT8.Core.Analytics { /// /// Result for single-parameter optimization. /// public class OptimizationResult { public string ParameterName { get; set; } public Dictionary MetricsByValue { get; set; } public double OptimalValue { get; set; } public OptimizationResult() { MetricsByValue = new Dictionary(); } } /// /// Result for multi-parameter grid search. /// public class GridSearchResult { public Dictionary MetricsByCombination { get; set; } public Dictionary BestParameters { get; set; } public GridSearchResult() { MetricsByCombination = new Dictionary(); BestParameters = new Dictionary(); } } /// /// Walk-forward optimization result. /// public class WalkForwardResult { public PerformanceMetrics InSampleMetrics { get; set; } public PerformanceMetrics OutOfSampleMetrics { get; set; } public double StabilityScore { get; set; } public WalkForwardResult() { InSampleMetrics = new PerformanceMetrics(); OutOfSampleMetrics = new PerformanceMetrics(); } } /// /// Parameter optimization utility. /// public class ParameterOptimizer { private readonly ILogger _logger; private readonly PerformanceCalculator _calculator; public ParameterOptimizer(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; _calculator = new PerformanceCalculator(logger); } /// /// Optimizes one parameter by replaying filtered trade subsets. /// public OptimizationResult OptimizeParameter(string paramName, List values, List trades) { if (string.IsNullOrEmpty(paramName)) throw new ArgumentNullException("paramName"); if (values == null) throw new ArgumentNullException("values"); if (trades == null) throw new ArgumentNullException("trades"); try { var result = new OptimizationResult(); result.ParameterName = paramName; var bestScore = double.MinValue; var bestValue = values.Count > 0 ? values[0] : 0.0; foreach (var value in values) { var sample = BuildSyntheticSubset(paramName, value, trades); var metrics = _calculator.Calculate(sample); result.MetricsByValue[value] = metrics; var score = metrics.Expectancy; if (score > bestScore) { bestScore = score; bestValue = value; } } result.OptimalValue = bestValue; return result; } catch (Exception ex) { _logger.LogError("OptimizeParameter failed: {0}", ex.Message); throw; } } /// /// Runs a grid search for multiple parameters. /// public GridSearchResult GridSearch(Dictionary> parameters, List trades) { if (parameters == null) throw new ArgumentNullException("parameters"); if (trades == null) throw new ArgumentNullException("trades"); try { var result = new GridSearchResult(); var keys = parameters.Keys.ToList(); if (keys.Count == 0) return result; var combos = BuildCombinations(parameters, keys, 0, new Dictionary()); var bestScore = double.MinValue; Dictionary best = null; foreach (var combo in combos) { var sample = trades; foreach (var kv in combo) { sample = BuildSyntheticSubset(kv.Key, kv.Value, sample); } var metrics = _calculator.Calculate(sample); var key = SerializeCombo(combo); result.MetricsByCombination[key] = metrics; if (metrics.Expectancy > bestScore) { bestScore = metrics.Expectancy; best = new Dictionary(combo); } } if (best != null) result.BestParameters = best; return result; } catch (Exception ex) { _logger.LogError("GridSearch failed: {0}", ex.Message); throw; } } /// /// Performs basic walk-forward validation. /// public WalkForwardResult WalkForwardTest(StrategyConfig config, List historicalData) { if (config == null) throw new ArgumentNullException("config"); if (historicalData == null) throw new ArgumentNullException("historicalData"); try { var mid = historicalData.Count / 2; var inSampleBars = historicalData.Take(mid).ToList(); var outSampleBars = historicalData.Skip(mid).ToList(); var inTrades = BuildPseudoTradesFromBars(inSampleBars, config.Symbol); var outTrades = BuildPseudoTradesFromBars(outSampleBars, config.Symbol); var result = new WalkForwardResult(); result.InSampleMetrics = _calculator.Calculate(inTrades); result.OutOfSampleMetrics = _calculator.Calculate(outTrades); var inExp = result.InSampleMetrics.Expectancy; var outExp = result.OutOfSampleMetrics.Expectancy; var denominator = Math.Abs(inExp) > 0.000001 ? Math.Abs(inExp) : 1.0; var drift = Math.Abs(inExp - outExp) / denominator; result.StabilityScore = Math.Max(0.0, 1.0 - drift); return result; } catch (Exception ex) { _logger.LogError("WalkForwardTest failed: {0}", ex.Message); throw; } } private static List BuildSyntheticSubset(string paramName, double value, List trades) { if (trades.Count == 0) return new List(); var percentile = Math.Max(0.05, Math.Min(0.95, value / (Math.Abs(value) + 1.0))); var take = Math.Max(1, (int)Math.Round(trades.Count * percentile)); return trades .OrderByDescending(t => t.ConfluenceScore) .Take(take) .Select(Clone) .ToList(); } private static List> BuildCombinations( Dictionary> parameters, List keys, int index, Dictionary current) { var results = new List>(); if (index >= keys.Count) { results.Add(new Dictionary(current)); return results; } var key = keys[index]; foreach (var value in parameters[key]) { current[key] = value; results.AddRange(BuildCombinations(parameters, keys, index + 1, current)); } return results; } private static string SerializeCombo(Dictionary combo) { return string.Join(";", combo.OrderBy(k => k.Key).Select(k => string.Format(CultureInfo.InvariantCulture, "{0}={1}", k.Key, k.Value)).ToArray()); } private static List BuildPseudoTradesFromBars(List bars, string symbol) { var trades = new List(); for (var i = 1; i < bars.Count; i++) { var prev = bars[i - 1]; var curr = bars[i]; var trade = new TradeRecord(); trade.TradeId = string.Format("WF-{0}", i); trade.Symbol = symbol; trade.StrategyName = "WalkForward"; trade.EntryTime = prev.Time; trade.ExitTime = curr.Time; trade.Side = curr.Close >= prev.Close ? Common.Models.OrderSide.Buy : Common.Models.OrderSide.Sell; trade.Quantity = 1; trade.EntryPrice = prev.Close; trade.ExitPrice = curr.Close; trade.RealizedPnL = curr.Close - prev.Close; trade.UnrealizedPnL = 0.0; trade.Grade = trade.RealizedPnL >= 0.0 ? Intelligence.TradeGrade.B : Intelligence.TradeGrade.D; trade.ConfluenceScore = 0.6; trade.RiskMode = Intelligence.RiskMode.PCP; trade.VolatilityRegime = Intelligence.VolatilityRegime.Normal; trade.TrendRegime = Intelligence.TrendRegime.Range; trade.StopTicks = 8; trade.TargetTicks = 16; trade.RMultiple = trade.RealizedPnL / 8.0; trade.Duration = trade.ExitTime.Value - trade.EntryTime; trades.Add(trade); } return trades; } private static TradeRecord Clone(TradeRecord input) { var copy = new TradeRecord(); copy.TradeId = input.TradeId; copy.Symbol = input.Symbol; copy.StrategyName = input.StrategyName; copy.EntryTime = input.EntryTime; copy.ExitTime = input.ExitTime; copy.Side = input.Side; copy.Quantity = input.Quantity; copy.EntryPrice = input.EntryPrice; copy.ExitPrice = input.ExitPrice; copy.RealizedPnL = input.RealizedPnL; copy.UnrealizedPnL = input.UnrealizedPnL; copy.Grade = input.Grade; copy.ConfluenceScore = input.ConfluenceScore; copy.RiskMode = input.RiskMode; copy.VolatilityRegime = input.VolatilityRegime; copy.TrendRegime = input.TrendRegime; copy.StopTicks = input.StopTicks; copy.TargetTicks = input.TargetTicks; copy.RMultiple = input.RMultiple; copy.Duration = input.Duration; copy.Metadata = new Dictionary(input.Metadata); return copy; } } }