Overall Statistics |
Total Orders 1413 Average Win 5.13% Average Loss -0.55% Compounding Annual Return 81.292% Drawdown 35.300% Expectancy 3.087 Start Equity 20000 End Equity 820470.92 Net Profit 4002.355% Sharpe Ratio 0.776 Sortino Ratio 5.757 Probabilistic Sharpe Ratio 0.002% Loss Rate 61% Win Rate 39% Profit-Loss Ratio 9.38 Alpha 1.633 Beta 0.921 Annual Standard Deviation 2.261 Annual Variance 5.113 Information Ratio 0.72 Tracking Error 2.254 Treynor Ratio 1.905 Total Fees $32202.57 Estimated Strategy Capacity $3000.00 Lowest Capacity Asset MNQ YQYHC5L1GPA9 Portfolio Turnover 15.43% |
#region imports using System; using System.Collections.Generic; using System.Linq; using QuantConnect.Data.Market; using Microsoft.ML; using Microsoft.ML.Data; using Microsoft.ML.FastTree; #endregion namespace QuantConnect.Algorithm.CSharp { public class PreTradeData { // Z-Score Features public float CurrentZScore; // Current z-score value public float ZScoreChange1Day; // 1-day change in z-score public float ZScoreChange3Day; // 3-day change in z-score public float ZScoreAbsValue; // Absolute value of z-score public float ZScoreVolatility; // Volatility of z-score (std dev of recent z-scores) // Mean Reversion Features public float PriceToSMA; // Price relative to SMA (ratio) public float DaysAboveThreshold; // How many days z-score has been beyond threshold public float MeanReversionSpeed; // Average daily change when reverting to mean // Price Action Features public float SignalBarHigh; public float SignalBarLow; public float SignalBarClose; public float SignalBarOpen; public float SignalBarVolume; public float DailyTrueRange; // ATR-style volatility measure public float PriceVolatility; // Standard deviation of recent prices // Trade Setup Features public bool IsLongPosition; // Direction of the trade public bool IsHighConviction; // Whether it's a high conviction trade public float ExpectedProfit; // Estimated profit based on z-score extremity public float StopLossPercent; // Stop loss percentage used // Time Features public int DayOfWeek; // 0-6 for day of week public int HourOfDay; // 0-23 for hour of day public float DaysToExpiration; // Days until contract expiration // Instrument Features public string Instrument; // The futures instrument (e.g., "MES", "MNQ") public float HistoricalWinRate; // Historical win rate for this instrument // Outcome public bool Win; // Whether the trade was profitable public float ReturnPercent; // Actual return percentage public string ExitReason; // Reason for exit (time, z-score, stop-loss) // Default parameterless constructor needed for ML.NET public PreTradeData() { } // Constructor for creating from trade data public PreTradeData( float currentZScore, float zScoreChange1Day, float priceToSMA, TradeBar signalBar, bool isLongPosition, bool isHighConviction, string instrument, float historicalWinRate, float daysToExpiration) { // Set core z-score features CurrentZScore = currentZScore; ZScoreChange1Day = zScoreChange1Day; ZScoreAbsValue = Math.Abs(currentZScore); PriceToSMA = priceToSMA; // Set price bar data SignalBarHigh = (float)signalBar.High; SignalBarLow = (float)signalBar.Low; SignalBarClose = (float)signalBar.Close; SignalBarOpen = (float)signalBar.Open; SignalBarVolume = (float)signalBar.Volume; // Set trade setup features IsLongPosition = isLongPosition; IsHighConviction = isHighConviction; ExpectedProfit = isHighConviction ? 0.03f : 0.02f; // Higher for high conviction StopLossPercent = isHighConviction ? 0.03f : 0.02f; // Set time features DateTime time = signalBar.Time; DayOfWeek = (int)time.DayOfWeek; HourOfDay = time.Hour; DaysToExpiration = daysToExpiration; // Set instrument features Instrument = instrument; HistoricalWinRate = historicalWinRate; // Initialize outcome (will be set later) Win = false; ReturnPercent = 0; ExitReason = ""; } // Additional constructor with more z-score history public PreTradeData( float[] recentZScores, // Array of z-scores [current, 1 day ago, 2 days ago, 3 days ago] float priceToSMA, float[] recentPrices, // Array of recent prices for volatility calculation TradeBar signalBar, bool isLongPosition, bool isHighConviction, float daysAboveThreshold, string instrument, float historicalWinRate, float daysToExpiration) : this(recentZScores[0], recentZScores[0] - recentZScores[1], priceToSMA, signalBar, isLongPosition, isHighConviction, instrument, historicalWinRate, daysToExpiration) { // Calculate additional z-score features if (recentZScores.Length >= 4) { ZScoreChange3Day = recentZScores[0] - recentZScores[3]; // Calculate z-score volatility float sum = 0; float mean = recentZScores.Average(); for (int i = 0; i < recentZScores.Length; i++) { sum += (recentZScores[i] - mean) * (recentZScores[i] - mean); } ZScoreVolatility = (float)Math.Sqrt(sum / recentZScores.Length); } // Calculate price volatility if (recentPrices.Length > 1) { float sum = 0; float mean = recentPrices.Average(); for (int i = 0; i < recentPrices.Length; i++) { sum += (recentPrices[i] - mean) * (recentPrices[i] - mean); } PriceVolatility = (float)Math.Sqrt(sum / recentPrices.Length); // Calculate daily true range (simplified) float highestHigh = recentPrices.Max(); float lowestLow = recentPrices.Min(); DailyTrueRange = highestHigh - lowestLow; } // Set days above threshold DaysAboveThreshold = daysAboveThreshold; // Calculate mean reversion speed if (recentZScores.Length > 1 && Math.Abs(recentZScores[0]) < Math.Abs(recentZScores[1])) { MeanReversionSpeed = Math.Abs(recentZScores[0] - recentZScores[1]); } else { MeanReversionSpeed = 0; } } // Method to set the win/loss outcome public void SetOutcome(bool isWin, float returnPercent, string exitReason) { Win = isWin; ReturnPercent = returnPercent; ExitReason = exitReason; } // For debugging - creates a readable string of the trade data public override string ToString() { return $"Instrument:{Instrument}, ZScore:{CurrentZScore:F2}, IsLong:{IsLongPosition}, " + $"HighConv:{IsHighConviction}, Win:{Win}, Return:{ReturnPercent:P2}, Exit:{ExitReason}"; } } /// <summary> /// Simplified ML.NET utility for predicting expected trade returns /// Uses FastTree regression to predict actual return percentage /// </summary> public static class ReturnPredictor { /// <summary> /// Class for holding return predictions from ML.NET /// </summary> public class ReturnPrediction { [ColumnName("Score")] public float PredictedReturn { get; set; } // For feature importance and logging public float[] FeatureContributions { get; set; } } /// <summary> /// Trade decision including expected return and position sizing /// </summary> public class TradeDecision { public bool ShouldTake { get; set; } public float ExpectedReturn { get; set; } public float RecommendedSize { get; set; } public string Reason { get; set; } public override string ToString() => $"Take Trade: {ShouldTake}, Expected Return: {ExpectedReturn:P2}, " + $"Size: {RecommendedSize:F2}x, Reason: {Reason}"; } /// <summary> /// Trains a FastTree regression model to predict trade returns /// </summary> public static PredictionEngine<PreTradeData, ReturnPrediction> TrainModel(List<PreTradeData> historicalTrades) { // Validate training data if (historicalTrades == null || historicalTrades.Count < 40) { Console.WriteLine($"WARNING: Insufficient data ({historicalTrades?.Count ?? 0} trades). Need at least 40 samples."); return null; } // Create ML context with fixed seed for reproducibility var mlContext = new MLContext(42); // Print basic data summary var avgReturn = historicalTrades.Average(t => t.ReturnPercent) * 100; var winRate = historicalTrades.Count(t => t.Win) / (float)historicalTrades.Count * 100; Console.WriteLine($"Training with {historicalTrades.Count} trades. " + $"Avg Return: {avgReturn:F2}%, Win Rate: {winRate:F2}%"); // Shuffle and load data var shuffledData = historicalTrades.OrderBy(x => Guid.NewGuid()).ToList(); var trainingData = mlContext.Data.LoadFromEnumerable(shuffledData); // Define feature columns for the model var featureColumns = new[] { // Most important features for return prediction nameof(PreTradeData.CurrentZScore), nameof(PreTradeData.ZScoreAbsValue), nameof(PreTradeData.ZScoreChange1Day), nameof(PreTradeData.PriceToSMA), nameof(PreTradeData.IsLongPosition), nameof(PreTradeData.IsHighConviction), nameof(PreTradeData.DayOfWeek), nameof(PreTradeData.HistoricalWinRate) }; // Create a simple pipeline with FastTree regression var pipeline = mlContext.Transforms.Categorical.OneHotEncoding( outputColumnName: "InstrumentEncoded", inputColumnName: nameof(PreTradeData.Instrument)) .Append(mlContext.Transforms.Concatenate("Features", new[] { "InstrumentEncoded" }.Concat(featureColumns).ToArray())) .Append(mlContext.Transforms.NormalizeMinMax("Features")) .Append(mlContext.Transforms.CopyColumns("Label", nameof(PreTradeData.ReturnPercent))) .Append(mlContext.Regression.Trainers.FastTree( numberOfLeaves: 20, numberOfTrees: 200, minimumExampleCountPerLeaf: 3)); // Train model Console.WriteLine("Training FastTree regression model..."); var model = pipeline.Fit(trainingData); // Create prediction engine var predictor = mlContext.Model.CreatePredictionEngine<PreTradeData, ReturnPrediction>(model); Console.WriteLine("Model training complete"); return predictor; } /// <summary> /// Makes a trade decision based on predicted return and strategy parameters /// </summary> public static TradeDecision EvaluateTrade( PredictionEngine<PreTradeData, ReturnPrediction> predictor, PreTradeData setup, float minReturnThreshold = 0.01f) // Minimum 1% return threshold { if (predictor == null || setup == null) { return new TradeDecision { ShouldTake = false, ExpectedReturn = 0, RecommendedSize = 0, Reason = "Invalid model or trade setup" }; } try { // Get return prediction var prediction = predictor.Predict(setup); float expectedReturn = prediction.PredictedReturn; // Base decision bool takeBasedOnReturn = expectedReturn >= minReturnThreshold; // Adjust threshold based on special conditions if (setup.IsHighConviction) { // Lower threshold for high conviction trades minReturnThreshold *= 0.8f; } if (setup.DayOfWeek == 5) // Friday { // Higher threshold on Friday to avoid weekend risk minReturnThreshold *= 1.25f; } // Consider extreme Z-scores bool extremeZScore = Math.Abs(setup.CurrentZScore) >= 2.5; // Make final decision bool shouldTake = takeBasedOnReturn || (extremeZScore && expectedReturn > 0); // Determine position size multiplier (0.5x to 2.0x) float sizeMultiplier = 1.0f; if (expectedReturn >= 0.03f) sizeMultiplier = 1.5f; // 3%+ expected return else if (expectedReturn >= 0.05f) sizeMultiplier = 2.0f; // 5%+ expected return else if (expectedReturn < 0.015f) sizeMultiplier = 0.75f; // <1.5% expected return // For high conviction extreme z-scores, use larger size if (extremeZScore && setup.IsHighConviction) { sizeMultiplier = Math.Max(sizeMultiplier, 1.5f); } // Create decision object string reason = shouldTake ? $"Expected return {expectedReturn:P2} exceeds {minReturnThreshold:P2} threshold" : $"Expected return {expectedReturn:P2} below {minReturnThreshold:P2} threshold"; if (extremeZScore && shouldTake) { reason += " and extreme Z-score detected"; } // Log decision Console.WriteLine($"Trade evaluation for {setup.Instrument}: " + $"Z-Score={setup.CurrentZScore:F2}, " + $"Expected Return={expectedReturn:P2}, " + $"Take={shouldTake}, Size={sizeMultiplier:F1}x"); return new TradeDecision { ShouldTake = shouldTake, ExpectedReturn = expectedReturn, RecommendedSize = sizeMultiplier, Reason = reason }; } catch (Exception ex) { Console.WriteLine($"Error evaluating trade: {ex.Message}"); return new TradeDecision { ShouldTake = false, ExpectedReturn = 0, RecommendedSize = 0, Reason = $"Error: {ex.Message}" }; } } /// <summary> /// Integrates with the mean reversion strategy to filter trades /// </summary> public static decimal GetOptimizedPositionSize( PredictionEngine<PreTradeData, ReturnPrediction> predictor, PreTradeData setup, decimal baseQuantity, float minReturnThreshold = 0.01f) { // Get trade evaluation var decision = EvaluateTrade(predictor, setup, minReturnThreshold); if (!decision.ShouldTake) { return 0; // Skip trade } // Scale position size based on recommendation decimal adjustedQuantity = baseQuantity * (decimal)decision.RecommendedSize; // Ensure quantity is at least 1 but not too large int maxSize = setup.IsHighConviction ? 10 : 5; adjustedQuantity = Math.Max(1, Math.Min(adjustedQuantity, maxSize)); return Math.Floor(adjustedQuantity); // Round down to whole contracts } } }
#region imports using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Globalization; using System.Drawing; using QuantConnect; using QuantConnect.Algorithm.Framework; using QuantConnect.Algorithm.Framework.Selection; using QuantConnect.Algorithm.Framework.Alphas; using QuantConnect.Algorithm.Framework.Portfolio; using QuantConnect.Algorithm.Framework.Portfolio.SignalExports; using QuantConnect.Algorithm.Framework.Execution; using QuantConnect.Algorithm.Framework.Risk; using QuantConnect.Algorithm.Selection; using QuantConnect.Api; using QuantConnect.Parameters; using QuantConnect.Benchmarks; using QuantConnect.Brokerages; using QuantConnect.Commands; using QuantConnect.Configuration; using QuantConnect.Util; using QuantConnect.Interfaces; using QuantConnect.Algorithm; using QuantConnect.Indicators; using QuantConnect.Data; using QuantConnect.Data.Auxiliary; using QuantConnect.Data.Consolidators; using QuantConnect.Data.Custom; using QuantConnect.Data.Custom.IconicTypes; using QuantConnect.DataSource; using QuantConnect.Data.Fundamental; using QuantConnect.Data.Market; using QuantConnect.Data.Shortable; using QuantConnect.Data.UniverseSelection; using QuantConnect.Notifications; using QuantConnect.Orders; using QuantConnect.Orders.Fees; using QuantConnect.Orders.Fills; using QuantConnect.Orders.OptionExercise; using QuantConnect.Orders.Slippage; using QuantConnect.Orders.TimeInForces; using QuantConnect.Python; using QuantConnect.Scheduling; using QuantConnect.Securities; using QuantConnect.Securities.Equity; using QuantConnect.Securities.Future; using QuantConnect.Securities.Option; using QuantConnect.Securities.Positions; using QuantConnect.Securities.Forex; using QuantConnect.Securities.Crypto; using QuantConnect.Securities.CryptoFuture; using QuantConnect.Securities.IndexOption; using QuantConnect.Securities.Interfaces; using QuantConnect.Securities.Volatility; using QuantConnect.Storage; using QuantConnect.Statistics; using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm; using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm; using Calendar = QuantConnect.Data.Consolidators.Calendar; #endregion namespace QuantConnect.Algorithm.CSharp { /// <summary> /// Revised Multi-Futures Mean Reversion Algorithm with Z-Score Thresholds /// Preserving the core edge while slightly increasing trade frequency /// </summary> public class MeanReversionFuturesAlgorithm : QCAlgorithm { // Futures objects to properly handle continuous contracts and mapping private readonly Dictionary<string, Future> _futures = new(); // Z-score settings - preserve the original robust parameters private readonly int _lookbackPeriod = 50; // Original lookback period private readonly int _holdingPeriodDays = 5; // Original hold time private readonly int _maxSize = 10; // Keep the original strict thresholds that were working well private double _entryZScoreThresholdLong = -1.5; // Original threshold that works private double _entryZScoreThresholdShort = 1.5; // Original threshold that works private readonly double _exitZScoreThresholdLong = 0.0; // Original exit threshold private readonly double _exitZScoreThresholdShort = 0.0; // Original exit threshold // Very extreme thresholds for "high conviction" trades private double _highConvictionZScoreLong = -2; // More extreme z-score for high conviction entries private double _highConvictionZScoreShort = 2; // More extreme z-score for high conviction entries // Risk management private decimal _riskPerTrade = 0.0025m; // 0.25% risk per trade private readonly decimal _maxPortfolioRisk = 0.025m; // 2.5% maximum overall risk // Track trade information private Dictionary<Symbol, DateTime> _tradeOpenTime = new(); private Dictionary<Symbol, List<bool>> _recentTradeResults = new(); private Dictionary<Symbol, Dictionary<string, object>> _instrumentStats = new(); // Z-Score indicators for each continuous contract private Dictionary<Symbol, SimpleMovingAverage> _smas = new(); private Dictionary<Symbol, StandardDeviation> _stdDevs = new(); // Add a slightly larger set of futures to increase opportunities private readonly List<string> _tickersToTrade = new List<string>{"MNQ", "MES", "MYM", "MGC", "MBT", "MCL", "M2K"}; public override void Initialize() { SetStartDate(2019, 1, 1); // Start 3 years ago SetEndDate(2025, 4, 30); // End at current date SetCash(20000); // Starting capital SetBenchmark("QQQ"); SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin); SetRiskManagement(new MaximumDrawdownPercentPerSecurity(0.1m)); // Add all the futures we want to trade foreach (var ticker in _tickersToTrade) { AddFuture(ticker); } // Schedule rebalancing twice per day - morning and afternoon // This slightly increases trade frequency without being excessive Schedule.On(DateRules.EveryDay(), TimeRules.Every(TimeSpan.FromHours(3)), Rebalance); // Schedule monthly parameter optimization Schedule.On(DateRules.MonthStart(), TimeRules.At(9, 30), OptimizeParameters); } public override void OnMarginCallWarning() { Debug("Warning: Close to margin call"); } private void AddFuture(string ticker) { try { // Add the continuous future contract with proper settings var future = AddFuture( ticker, Resolution.Hour, extendedMarketHours: true, dataMappingMode: DataMappingMode.OpenInterest, dataNormalizationMode: DataNormalizationMode.BackwardsRatio, contractDepthOffset: 0 ); // Original filter worked well - stick with it but slightly expanded future.SetFilter(futureFilterUniverse => futureFilterUniverse.Expiration(0, 120) // Slightly wider range than original ); // Store the Future object for easy access _futures[ticker] = future; // Create indicators for the continuous contract _smas[future.Symbol] = SMA(future.Symbol, _lookbackPeriod, Resolution.Daily); _stdDevs[future.Symbol] = STD(future.Symbol, _lookbackPeriod, Resolution.Daily); // Initialize trade tracking _recentTradeResults[future.Symbol] = new List<bool>(); _instrumentStats[future.Symbol] = new Dictionary<string, object> { { "TotalTrades", 0 }, { "WinningTrades", 0 }, { "TotalPnl", 0m } }; // Warm up the indicators WarmUpIndicator(future.Symbol, _smas[future.Symbol]); WarmUpIndicator(future.Symbol, _stdDevs[future.Symbol]); Log($"Added {ticker} future contract: {future.Symbol}. Current mapping: {future.Mapped}"); } catch (Exception e) { Log($"Error adding {ticker}: {e.Message}"); } } public override void OnData(Slice data) { // Handle existing positions ManageExistingPositions(); } private double CalculateZScore(Symbol symbol) { // Use the properly warmed up indicators for z-score calculation if (!_smas[symbol].IsReady || !_stdDevs[symbol].IsReady) { return 0; } decimal mean = _smas[symbol].Current.Value; decimal stdDev = _stdDevs[symbol].Current.Value; if (stdDev == 0) return 0; // Get the current price of the continuous contract decimal currentPrice = Securities[symbol].Price; // Calculate z-score double zScore = (double)((currentPrice - mean) / stdDev); return zScore; } // Calculate daily volatility from standard deviation indicator private double CalculateVolatility(Symbol symbol) { if (!_stdDevs[symbol].IsReady) return 0; // Convert standard deviation to annualized volatility double dailyStdDev = (double)_stdDevs[symbol].Current.Value / (double)_smas[symbol].Current.Value; return dailyStdDev; } private void Rebalance() { // Count current open positions var openPositions = Portfolio.Securities.Count(pair => pair.Value.Invested); var availablePositions = Math.Max(0, (int)(_maxPortfolioRisk / _riskPerTrade) - openPositions); if (availablePositions <= 0) { return; } // Dictionary to store trade candidates var tradeCandidates = new Dictionary<Symbol, Tuple<double, bool, bool>>(); // symbol -> (score, isLong, isHighConviction) foreach (var kvp in _futures) { string ticker = kvp.Key; Future future = kvp.Value; Symbol continuousSymbol = future.Symbol; // Skip if we already have a position in this future if (Portfolio[future.Mapped].Invested) continue; // Skip if indicators aren't ready if (!_smas[continuousSymbol].IsReady || !_stdDevs[continuousSymbol].IsReady) { continue; } // Calculate z-score var zScore = CalculateZScore(continuousSymbol); // Determine if this is a valid trade candidate bool isLongCandidate = zScore <= _entryZScoreThresholdLong; bool isShortCandidate = zScore >= _entryZScoreThresholdShort; // Check for high conviction trades bool isHighConvictionLong = zScore <= _highConvictionZScoreLong; bool isHighConvictionShort = zScore >= _highConvictionZScoreShort; bool isHighConviction = isHighConvictionLong || isHighConvictionShort; if (isLongCandidate || isShortCandidate) { bool isLong = isLongCandidate; // Calculate volatility - lower volatility assets are preferred double volatility = CalculateVolatility(continuousSymbol); // Basic score - absolute z-score value, higher is better double score = Math.Abs(zScore); // Adjust score for volatility - prefer lower volatility assets if (volatility > 0) { score *= (1 / volatility); } // Adjust score for win rate double winRate = GetDynamicWinRate(continuousSymbol); score *= (winRate / 0.5); // Normalize around 1.0 tradeCandidates.Add(continuousSymbol, Tuple.Create(score, isLong, isHighConviction)); } } // Take the best candidates based on score var selectedCandidates = tradeCandidates .OrderByDescending(pair => pair.Value.Item1) .Take(availablePositions) .ToList(); // Execute trades for selected candidates foreach (var candidate in selectedCandidates) { var continuousSymbol = candidate.Key; var isLong = candidate.Value.Item2; var isHighConviction = candidate.Value.Item3; var zScore = CalculateZScore(continuousSymbol); // Get the actual tradable contract using the Mapped property var mappedSymbol = _futures.First(f => f.Value.Symbol == continuousSymbol).Value.Mapped; // Set stop-loss based on conviction decimal stopLossPct = isHighConviction ? 0.03m : 0.02m; // More room for high conviction trades // Calculate position size - use slightly larger size for high conviction decimal quantity = CalculatePositionSize(mappedSymbol, stopLossPct); if (isHighConviction) quantity = Math.Min(quantity * 1.5m, 10); // Up to 50% larger for high conviction if (quantity > 0) { try { if (isLong) { var orderTicket = MarketOrder(mappedSymbol, quantity); Log($"LONG {mappedSymbol}: Z-Score={zScore:F2}, Qty={quantity}, HighConviction={isHighConviction}, OrderId={orderTicket.OrderId}"); } else { var orderTicket = MarketOrder(mappedSymbol, -quantity); Log($"SHORT {mappedSymbol}: Z-Score={zScore:F2}, Qty={quantity}, HighConviction={isHighConviction}, OrderId={orderTicket.OrderId}"); } // Track trade open time _tradeOpenTime[mappedSymbol] = Time; // Increment total trades counter _instrumentStats[continuousSymbol]["TotalTrades"] = (int)_instrumentStats[continuousSymbol]["TotalTrades"] + 1; } catch (Exception e) { Log($"Error placing order for {mappedSymbol}: {e.Message}"); } } } } private void ManageExistingPositions() { foreach (var kvp in _futures) { string ticker = kvp.Key; Future future = kvp.Value; Symbol mappedSymbol = future.Mapped; Symbol continuousSymbol = future.Symbol; if (Portfolio[mappedSymbol].Invested && _tradeOpenTime.ContainsKey(mappedSymbol)) { var position = Portfolio[mappedSymbol]; var zScore = CalculateZScore(continuousSymbol); var holdingDays = (Time - _tradeOpenTime[mappedSymbol]).TotalDays; // Original exit conditions - these worked well bool timeExitCondition = holdingDays >= _holdingPeriodDays; bool zScoreExitCondition = false; // Stop loss as an additional safety measure bool stopLossCondition = position.UnrealizedProfitPercent <= -0.03m; // 3% stop loss if (position.IsLong) { zScoreExitCondition = zScore >= _exitZScoreThresholdLong; } else { zScoreExitCondition = zScore <= _exitZScoreThresholdShort; } // Exit position if any condition is met if (timeExitCondition || zScoreExitCondition || stopLossCondition) { try { // Liquidate returns a list of order tickets var orderTickets = Liquidate(mappedSymbol); string orderIds = string.Join(",", orderTickets.Select(t => t.OrderId)); // Calculate trade result bool isWin = position.UnrealizedProfitPercent > 0; _recentTradeResults[continuousSymbol].Add(isWin); // Keep only last 20 trades for win rate calculation if (_recentTradeResults[continuousSymbol].Count > 20) { _recentTradeResults[continuousSymbol].RemoveAt(0); } // Update instrument statistics if (isWin) { _instrumentStats[continuousSymbol]["WinningTrades"] = (int)_instrumentStats[continuousSymbol]["WinningTrades"] + 1; } // Calculate realized PnL for this trade decimal tradePnl = position.LastTradeProfit; _instrumentStats[continuousSymbol]["TotalPnl"] = (decimal)_instrumentStats[continuousSymbol]["TotalPnl"] + tradePnl; string exitReason = timeExitCondition ? "time" : (zScoreExitCondition ? "z-score" : "stop-loss"); Log($"EXIT {mappedSymbol}: Reason={exitReason}, Z-Score={zScore:F2}, PnL={tradePnl:C}, Win={isWin}, OrderIds={orderIds}"); // Remove from tracking _tradeOpenTime.Remove(mappedSymbol); } catch (Exception e) { Log($"Error liquidating position for {mappedSymbol}: {e.Message}"); } } } } } private void OptimizeParameters() { // Calculate overall win rate across all instruments int totalTrades = 0; int totalWins = 0; foreach (var kvp in _recentTradeResults) { totalTrades += kvp.Value.Count; totalWins += kvp.Value.Count(win => win); } if (totalTrades >= 10) { double overallWinRate = (double)totalWins / totalTrades; // Adjust thresholds based on historical performance - but keep the core edge intact if (overallWinRate < 0.5) // Underperforming strategy { // Make entry more conservative (require more extreme z-scores) _entryZScoreThresholdLong = -2.2; _entryZScoreThresholdShort = 2.2; _highConvictionZScoreLong = -2.7; _highConvictionZScoreShort = 2.7; // Reduce risk per trade _riskPerTrade = Math.Max(0.007m, _riskPerTrade * 0.9m); } else if (overallWinRate > 0.7) // Performing even better than expected { // Can be very slightly more aggressive on entries _entryZScoreThresholdLong = -1.9; _entryZScoreThresholdShort = 1.9; _highConvictionZScoreLong = -2.4; _highConvictionZScoreShort = 2.4; // Increase risk slightly for consistently good performance _riskPerTrade = Math.Min(0.012m, _riskPerTrade * 1.05m); } else // Standard good performance - maintain the edge { // Default settings _entryZScoreThresholdLong = -2.0; _entryZScoreThresholdShort = 2.0; _highConvictionZScoreLong = -2.5; _highConvictionZScoreShort = 2.5; } Log($"Optimized parameters: WinRate={overallWinRate:P2}, EntryLong={_entryZScoreThresholdLong:F1}, EntryShort={_entryZScoreThresholdShort:F1}, RiskPerTrade={_riskPerTrade:P2}"); } } private double GetDynamicWinRate(Symbol symbol) { // Return actual win rate if we have enough data if (_recentTradeResults[symbol].Count >= 5) { return _recentTradeResults[symbol].Count(win => win) / (double)_recentTradeResults[symbol].Count; } // Otherwise return a default value return 0.5; } private decimal CalculatePositionSize(Symbol symbol, decimal stopLossPct) { if (stopLossPct == 0) return 0; var security = Securities[symbol]; var price = security.Price; if (price == 0) { return 0; } // Calculate dollar risk amount decimal riskAmount = Portfolio.TotalPortfolioValue * _riskPerTrade; // Calculate position size based on risk per trade decimal positionValue = riskAmount / stopLossPct; // Calculate quantity - handle potential zero contract multiplier decimal contractMultiplier = security.SymbolProperties.ContractMultiplier; if (contractMultiplier == 0) { contractMultiplier = 1; } decimal contractValue = price * contractMultiplier; if (contractValue == 0) { return 0; } decimal quantity = Math.Floor(positionValue / contractValue); // Ensure quantity is at least 1 but not too large quantity = Math.Max(1, Math.Min(quantity, _maxSize)); // Cap at 10 contracts for safety return quantity; } public override void OnOrderEvent(OrderEvent orderEvent) { // Log all order events for debugging Log($"Order {orderEvent.OrderId}: {orderEvent.Status} - {orderEvent.FillQuantity} @ {orderEvent.FillPrice:C} Value: {orderEvent.FillPrice * orderEvent.FillQuantity}"); } public override void OnSecuritiesChanged(SecurityChanges changes) { // Handle securities being added or removed from the universe foreach (var security in changes.RemovedSecurities) { // Liquidate positions in securities that are removed from the universe // This handles futures contracts that are expiring if (Portfolio[security.Symbol].Invested) { Log($"Security removed from universe: {security.Symbol}. Liquidating position."); Liquidate(security.Symbol); } } } public override void OnEndOfAlgorithm() { Log("Strategy Performance Summary:"); foreach (var kvp in _futures) { var ticker = kvp.Key; var future = kvp.Value; var continuousSymbol = future.Symbol; var totalTrades = (int)_instrumentStats[continuousSymbol]["TotalTrades"]; if (totalTrades > 0) { var winningTrades = (int)_instrumentStats[continuousSymbol]["WinningTrades"]; var totalPnl = (decimal)_instrumentStats[continuousSymbol]["TotalPnl"]; var winRate = (double)winningTrades / totalTrades; var avgPnlPerTrade = totalTrades > 0 ? totalPnl / totalTrades : 0; Log($"{ticker}: Trades={totalTrades}, WinRate={winRate:P2}, AvgPnL={avgPnlPerTrade:C}, TotalPnL={totalPnl:C}"); } else { Log($"{ticker}: No trades executed"); } } } } }