Overall Statistics
Total Orders
4737
Average Win
0.65%
Average Loss
-0.70%
Compounding Annual Return
13.856%
Drawdown
23.900%
Expectancy
0.231
Start Equity
100000
End Equity
3302289.54
Net Profit
3202.290%
Sharpe Ratio
0.728
Sortino Ratio
0.875
Probabilistic Sharpe Ratio
24.963%
Loss Rate
37%
Win Rate
63%
Profit-Loss Ratio
0.94
Alpha
0.064
Beta
0.255
Annual Standard Deviation
0.107
Annual Variance
0.011
Information Ratio
0.167
Tracking Error
0.154
Treynor Ratio
0.305
Total Fees
$258110.93
Estimated Strategy Capacity
$0
Lowest Capacity Asset
CSX R735QTJ8XC9X
Portfolio Turnover
4.84%
#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.Execution;
    using QuantConnect.Algorithm.Framework.Risk;
    using QuantConnect.Parameters;
    using QuantConnect.Benchmarks;
    using QuantConnect.Brokerages;
    using QuantConnect.Util;
    using QuantConnect.Interfaces;
    using QuantConnect.Algorithm;
    using QuantConnect.Indicators;
    using QuantConnect.Data;
    using QuantConnect.Data.Consolidators;
    using QuantConnect.Data.Custom;
    using QuantConnect.DataSource;
    using QuantConnect.Data.Fundamental;
    using QuantConnect.Data.Market;
    using QuantConnect.Data.UniverseSelection;
    using QuantConnect.Notifications;
    using QuantConnect.Orders;
    using QuantConnect.Orders.Fees;
    using QuantConnect.Orders.Fills;
    using QuantConnect.Orders.Slippage;
    using QuantConnect.Scheduling;
    using QuantConnect.Securities;
    using QuantConnect.Securities.Equity;
    using QuantConnect.Securities.Future;
    using QuantConnect.Securities.Option;
    using QuantConnect.Securities.Forex;
    using QuantConnect.Securities.Crypto;
    using QuantConnect.Securities.Interfaces;
    using QuantConnect.Storage;
    using QuantConnect.Data.Custom.AlphaStreams;
    using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
    using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
#endregion

namespace QuantConnect.Algorithm.CSharp
{
    /*
    *******************************************************************************************
    Connors Mean Reversion Strategy - by CabedoVestment
    *******************************************************************************************
    
    ** Objective of the algorithm: **
    To capitalize on short-term mean reversion opportunities in stocks from the S&P 500.

    ** Key Trading Rules: **
    1. Long-term Trend Following Regime Filter: SPY’s total return over the last six months (126 trading days) is positive.
    2. Liquidity Filter. The stock must be one of the 500 most liquid US stocks. 
    3. The Weekly 2-period RSI of the stock must be below 20, indicating it has overreacted to the downside.
    4. Rank all qualifying stocks by their trailing 100-day historical volatility, then BUY on the close, in equal weight, the 10 stocks with the LOWEST historical volatility.
    5. SELL the stock on the close if its weekly 2-period RSI is above 80.
    6. SELL the stock on the close if the current price is more than 10% below the entry price, checked at the end of every business day.

    *******************************************************************************************
    */

    public class ConnorsMeanReversionStrategy : QCAlgorithm
    {
        private const int RsiPeriodBUY = 4; // Rule 3: Weekly 2-period RSI
        private const int RsiPeriod = 2; // Rule 3: Weekly 2-period RSI
        private const int LookbackPeriod = 100; // Rule 4: 100-day historical volatility
        private const decimal MaxPortfolioRisk = 0.1m; // Rule 6: Max position size (10% risk)
        private const int PortfolioStockSize = 10; // Rule 6: Max position size (10% risk)
        private RateOfChange _spyROC126; // Rule 1: Long-term trend filter for SPY
        private RateOfChange _bilROC21;
        private RateOfChange _iefROC21;


        private Dictionary<Symbol, RelativeStrengthIndex> _RSIIndicatorBUY = new Dictionary<Symbol, RelativeStrengthIndex>();
        private Dictionary<Symbol, RelativeStrengthIndex> _RSIIndicator = new Dictionary<Symbol, RelativeStrengthIndex>();
        private Dictionary<Symbol, StandardDeviation> _volatilityIndicator = new Dictionary<Symbol, StandardDeviation>();
        private Dictionary<Symbol, RateOfChange> _ROC4Indicator = new Dictionary<Symbol, RateOfChange>();

        public override void Initialize()
        {
            // Set the start and end dates for the backtest
            SetStartDate(1998, 1, 1);
            SetEndDate(DateTime.Now);
            SetCash(100000); // Initial capital

            // Rule 1: Add SPY for market regime filter
            var spy = AddEquity("SPY", Resolution.Daily);
            _spyROC126 = ROC(spy.Symbol, 126, Resolution.Daily);
            SetBenchmark(spy.Symbol);

            var bil = AddEquity("BIL", Resolution.Daily);
            var ief = AddEquity("IEF", Resolution.Daily);

            _bilROC21 = ROC("BIL",21,Resolution.Daily);
            _iefROC21 = ROC("IEF",21,Resolution.Daily);
            
            var history = History("BIL", 21, Resolution.Daily);
            foreach (TradeBar tradeBar in history)
            {
                _bilROC21.Update(tradeBar.EndTime, tradeBar.Close);
            }
            history = History("IEF", 21, Resolution.Daily);
            foreach (TradeBar tradeBar in history)
            {
                _iefROC21.Update(tradeBar.EndTime, tradeBar.Close);
            }

            // Rule 2: The stock must be one of the 500 most liquid US stocks.
            UniverseSettings.Resolution = Resolution.Daily;
            SetUniverseSelection(new QC500UniverseSelectionModel());

            SetWarmUp(LookbackPeriod);
            Schedule.On(DateRules.Every(DayOfWeek.Monday), TimeRules.AfterMarketOpen("SPY", 0), ExecuteMondaySelling);
            Schedule.On(DateRules.Every(DayOfWeek.Monday), TimeRules.AfterMarketOpen("SPY", 1), ExecuteMondayTrading);
            Schedule.On(DateRules.EveryDay(), TimeRules.AfterMarketClose("SPY", 0), ExecuteDailySelling);
        }

        public override void OnSecuritiesChanged(SecurityChanges changes)
        {
            if (changes.AddedSecurities.Count > 0)
            {
                foreach (Security security in changes.AddedSecurities)
                {
                    // Ensure SPY is not added
                    if (!_RSIIndicator.ContainsKey(security.Symbol) && security.Symbol.Value != "SPY" && security.Symbol.Value != "BIL" && security.Symbol.Value != "IEF")
                    {
                        var symbol = security.Symbol;

                        // Rule 3: Create RSI for the stock
                        var rsi = new RelativeStrengthIndex(RsiPeriod, MovingAverageType.Wilders);
                        var consolidator = new TradeBarConsolidator(CalendarType.Weekly);
                        RegisterIndicator(symbol, rsi, consolidator);

                        // Warm up RSI with historical data
                        var rsiHistory = IndicatorHistory(rsi, symbol, LookbackPeriod, Resolution.Daily);

                        // Add RSI indicator to the dictionary
                        _RSIIndicator.Add(symbol, rsi);

                        // Rule 4: Create volatility indicator (Standard Deviation)
                        var stdDev = new StandardDeviation(LookbackPeriod);
                        RegisterIndicator(security.Symbol, stdDev, Resolution.Daily);

                        // Warm up volatility indicator with historical data
                        var stdDevHistory = IndicatorHistory(stdDev, security.Symbol, LookbackPeriod, Resolution.Daily);

                        _volatilityIndicator.Add(security.Symbol, stdDev);

                        var roc4 = new RateOfChange(4);
                        var consolidator2 = new TradeBarConsolidator(CalendarType.Weekly);
                        RegisterIndicator(security.Symbol, roc4, consolidator2);
                        // Warm up RSI with historical data
                        var roc4History = IndicatorHistory(roc4, symbol, LookbackPeriod, Resolution.Daily);
                        _ROC4Indicator.Add(security.Symbol, roc4);

                    }
                }
            }

            // Handle removed securities
            if (changes.RemovedSecurities.Count > 0)
            {
                foreach (var security in changes.RemovedSecurities)
                {
                    if (security.Invested)
                    {
                        Liquidate(security.Symbol); // Liquidate if invested
                    }
                    if (_RSIIndicator.ContainsKey(security.Symbol))
                    {
                        _RSIIndicator.Remove(security.Symbol);
                        _RSIIndicatorBUY.Remove(security.Symbol);
                        _volatilityIndicator.Remove(security.Symbol);
                        _ROC4Indicator.Remove(security.Symbol);
                    }
                }
            }
        }

        private void ExecuteMondayTrading()
        {
            // Ensure the algorithm is not warming up
            if (IsWarmingUp) return;

            //Rule: 1 SPY’s total return over the last six months (126 trading days) is positive
            if (_spyROC126 > 0)
            {
                SetHoldings("IEF", 0);

                // List of candidate stocks and their volatility
                var candidates = new List<Symbol>();
                var volatilityDict = new Dictionary<Symbol, decimal>();

                foreach (var symbol in _RSIIndicator.Keys)
                {
                    // Check if the price data is valid (greater than zero)
                    var currentPrice = Securities[symbol].Price;
                    if (currentPrice <= 0) continue; // Ensure the price is valid

                    var rsi = _RSIIndicator[symbol]; // Access the RSI for the symbol

                    // Rule 3: Check if the RSI is below 20
                    if (rsi.Current.Value < 20)
                    
                    //var roc = _ROC4Indicator[symbol];
                    //if (roc.Current.Value < 0)
                    {
                        candidates.Add(symbol);

                        // Rule 4: Calculate historical volatility
                        var stdDev100 = _volatilityIndicator[symbol].Current.Value; // Access volatility indicator
                        volatilityDict[symbol] = stdDev100; // Store the volatility
                    }
                }

                // Select the 10 stocks with the lowest volatility
                var selectedStocks = volatilityDict
                    .Where(x => candidates.Contains(x.Key))
                    .OrderBy(x => x.Value)
                    .Take(PortfolioStockSize)
                    .Select(x => x.Key)
                    .ToList();

                // Proceed if there are selected stocks
                if (selectedStocks.Count > 0)
                {
                    // Count current holdings
                    int currentCount = Portfolio.Values.Count(v => v.Invested);

                    // Add new stocks until the portfolio reaches 10
                    foreach (var symbol in selectedStocks)
                    {
                        if (currentCount < PortfolioStockSize) // Max of 10 new positions
                        {
                            SetHoldings(symbol, MaxPortfolioRisk); // Allocate 10% to the new stock
                            currentCount++; // Increment count
                        }
                    }
                }
            }
            else
            {
                if (_iefROC21 > _bilROC21 && _iefROC21 > 0)
                {
                    SetHoldings("IEF", 1);
                }
            }
        }


        private void ExecuteMondaySelling()
        {
            // Check all positions for selling criteria
            foreach (var symbol in Portfolio.Keys.ToList())
            {
                if (symbol != "IEF")
                {
                    // Access the RSI for the symbol
                    if (_RSIIndicator.TryGetValue(symbol, out var rsi))
                    {
                        // Rule 5: Sell if RSI is above 80
                        if (rsi.Current.Value > 80)
                        {
                            Liquidate(symbol);
                        }
                    }
                }
            }
        }

        public void ExecuteDailySelling()
        {
            // Rule 6: Sell if the current price is more than 10% below entry price
            foreach (var symbol in Portfolio.Keys.ToList())
            {
                if (symbol != "IEF")
                {
                    var currentPrice = Securities[symbol].Price;
                    var entryPrice = Portfolio[symbol].AveragePrice;

                    if (currentPrice < entryPrice * 0.9m) // 10% below entry price
                    {
                        Liquidate(symbol);
                    }
                }
            }
        }
    }
}
#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.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.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;

#endregion

namespace QuantConnect.Algorithm.Framework.Selection
{
    /// <summary>
    /// Defines the QC100 universe as a universe selection model for framework algorithm
    /// </summary>
    public class QC100UniverseSelectionModel : FundamentalUniverseSelectionModel
    {
        private const int _numberOfSymbolsCoarse = 500;
        private const int _numberOfSymbolsFine = 100;

        // rebalances at the start of each month
        private int _lastMonth = -1;
        private readonly Dictionary<Symbol, double> _dollarVolumeBySymbol = new();

        /// <summary>
        /// Initializes a new default instance of the <see cref="QC100UniverseSelectionModel"/>
        /// </summary>
        public QC100UniverseSelectionModel()
            : base(true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="QC100UniverseSelectionModel"/>
        /// </summary>
        /// <param name="universeSettings">Universe settings defines what subscription properties will be applied to selected securities</param>
        public QC100UniverseSelectionModel(UniverseSettings universeSettings)
            : base(true, universeSettings)
        {
        }

        /// <summary>
        /// Performs coarse selection for the QC100 constituents.
        /// The stocks must have fundamental data
        /// The stock must have positive previous-day close price
        /// The stock must have positive volume on the previous trading day
        /// </summary>
        public override IEnumerable<Symbol> SelectCoarse(QCAlgorithm algorithm, IEnumerable<CoarseFundamental> coarse)
        {
            if (algorithm.Time.Month == _lastMonth)
            {
                return Universe.Unchanged;
            }

            var sortedByDollarVolume =
                (from x in coarse
                 where x.HasFundamentalData && x.Volume > 0 && x.Price > 0
                 orderby x.DollarVolume descending
                 select x).Take(_numberOfSymbolsCoarse).ToList();

            _dollarVolumeBySymbol.Clear();
            foreach (var x in sortedByDollarVolume)
            {
                _dollarVolumeBySymbol[x.Symbol] = x.DollarVolume;
            }

            // If no security has met the QC100 criteria, the universe is unchanged.
            // A new selection will be attempted on the next trading day as _lastMonth is not updated
            if (_dollarVolumeBySymbol.Count == 0)
            {
                return Universe.Unchanged;
            }

            return _dollarVolumeBySymbol.Keys;
        }

        /// <summary>
        /// Performs fine selection for the QC100 constituents
        /// The company's headquarter must in the U.S.
        /// The stock must be traded on either the NYSE or NASDAQ
        /// At least half a year since its initial public offering
        /// The stock's market cap must follow the criteria of the S&P 100 index
        /// </summary>
        public override IEnumerable<Symbol> SelectFine(QCAlgorithm algorithm, IEnumerable<FineFundamental> fine)
        {
            var filteredFine =
                (from x in fine
                 where x.CompanyReference.CountryId == "USA" &&
                       (x.CompanyReference.PrimaryExchangeID == "NYS" || x.CompanyReference.PrimaryExchangeID == "NAS") &&
                       (algorithm.Time - x.SecurityReference.IPODate).Days > 180 &&
                       x.MarketCap > 5000000000m // Adjusted to follow S&P 100 criteria
                 select x).ToList();

            var count = filteredFine.Count;

            // If no security has met the QC100 criteria, the universe is unchanged.
            // A new selection will be attempted on the next trading day as _lastMonth is not updated
            if (count == 0)
            {
                return Universe.Unchanged;
            }

            // Update _lastMonth after all QC100 criteria checks passed
            _lastMonth = algorithm.Time.Month;

            var percent = _numberOfSymbolsFine / (double)count;

            // select stocks with top dollar volume in every single sector
            var topFineBySector =
                (from x in filteredFine
                     // Group by sector
                 group x by x.CompanyReference.IndustryTemplateCode into g
                 let y = from item in g
                         orderby _dollarVolumeBySymbol[item.Symbol] descending
                         select item
                 let c = (int)Math.Ceiling(y.Count() * percent)
                 select new { g.Key, Value = y.Take(c) }
                 ).ToDictionary(x => x.Key, x => x.Value);

            return topFineBySector.SelectMany(x => x.Value)
                .OrderByDescending(x => _dollarVolumeBySymbol[x.Symbol])
                .Take(_numberOfSymbolsFine)
                .Select(x => x.Symbol);
        }
    }
}