| 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");
}
}
}
}
}