Overall Statistics
Total Orders
925
Average Win
2.00%
Average Loss
-1.19%
Compounding Annual Return
12.659%
Drawdown
56.700%
Expectancy
0.615
Start Equity
100000
End Equity
2678133.54
Net Profit
2578.134%
Sharpe Ratio
0.454
Sortino Ratio
0.501
Probabilistic Sharpe Ratio
0.270%
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
1.68
Alpha
0.036
Beta
0.841
Annual Standard Deviation
0.175
Annual Variance
0.031
Information Ratio
0.244
Tracking Error
0.115
Treynor Ratio
0.095
Total Fees
$15592.48
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SBR R735QTJ8XC9X
Portfolio Turnover
0.37%
Drawdown Recovery
1389
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
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;

namespace QuantConnect.Algorithm.CSharp
{
    /// <summary>
    /// Implements a fundamental-based stock picking strategy inspired by Joel Greenblatt's Magic Formula.
    /// Selects top 25 U.S. stocks based on Return on Assets (ROA) and Price-to-Book ratio (PB).
    /// Executes rebalancing at year-end if criteria are met.
    /// </summary>
    public class MagicFormulaStrategy : QCAlgorithm
    {
        // Number of top stocks to include in the portfolio
        private const int PortfolioStockSize = 25; 

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

            // Add SPY as a benchmark asset
            var spy = AddEquity("SPY", Resolution.Daily);
            SetBenchmark(spy.Symbol);

            // Set universe selection model using the MagicFormulaUniverseSelectionModel
            UniverseSettings.Resolution = Resolution.Daily;
            SetUniverseSelection(new MagicFormulaUniverseSelectionModel());

            // Warm-up period to calculate momentum or other historical indicators
            SetWarmUp(252);

            // Schedule stock selection and trading logic to run at the end of each year
            Schedule.On(DateRules.MonthEnd(), TimeRules.AfterMarketOpen("SPY", 0), ExecuteYearStartTrading);
        }

        /// <summary>
        /// Executes the stock selection process once a year in December.
        /// </summary>
        private void ExecuteYearStartTrading()
        {
            // Ensure warm-up period is completed before executing any logic
            if (IsWarmingUp) return;

            // Run only in December
            if ((Time.Month == 12))
            {
                top25Stocks(); // Select top 25 stocks to hold
            }
        }

        /// <summary>
        /// Selects the top 25 stocks using Magic Formula criteria (high ROA and low PB ratio),
        /// liquidates current holdings, and rebalances equally among selected stocks.
        /// </summary>
        private void top25Stocks()
        {
            // Filter active securities that have valid fundamental data (ROA and PB ratio)
            var validSymbols = ActiveSecurities.Keys
                .Where(x => Securities.ContainsKey(x) &&
                            Securities[x].Fundamentals != null &&
                            Securities[x].Fundamentals.ValuationRatios.PBRatio > 0 &&
                            Securities[x].Fundamentals.OperationRatios.ROA.Value > 0)
                .ToList();

            // Retrieve and rank fundamental metrics for sorting
            var fundamentals = validSymbols
                .Select(s => new
                {
                    Symbol = s,
                    PB = Securities[s].Fundamentals.ValuationRatios.PBRatio,
                    ROA = Securities[s].Fundamentals.OperationRatios.ROA.Value
                })
                .OrderByDescending(x => x.ROA) // Prefer higher ROA
                .ThenBy(x => x.PB)             // Prefer lower PB
                .Take(PortfolioStockSize)      // Take top 25
                .ToList();

            // Liquidate all current positions
            Liquidate();

            // Equally allocate capital to each of the selected stocks
            foreach (var stock in fundamentals)
            {
                SetHoldings(stock.Symbol, 1m / PortfolioStockSize);
            }
        }
    }
}
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.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;

namespace QuantConnect.Algorithm.Framework.Selection
{
    /// <summary>
    /// Defines the Magic Formula universe as a universe selection model for framework algorithm.
    /// Applies a two-stage filtering process based on coarse and fine fundamental criteria,
    /// following principles similar to Joel Greenblatt's Magic Formula (quality and value).
    /// </summary>
    public class MagicFormulaUniverseSelectionModel : FundamentalUniverseSelectionModel
    {
        // Max number of stocks selected in coarse filtering
        private const int _numberOfSymbolsCoarse = 10000; 

        // Max number of stocks selected after fine filtering
        private const int _numberOfSymbolsFine = 1000;   

        // Stores the last processed month to ensure universe selection only happens once per month
        private int _lastMonth = -1; 

        // Dictionary to cache dollar volume for each symbol from coarse selection stage
        private readonly Dictionary<Symbol, double> _dollarVolumeBySymbol = new();

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

        /// <summary>
        /// Initializes a new instance with custom universe settings.
        /// </summary>
        public MagicFormulaUniverseSelectionModel(UniverseSettings universeSettings)
            : base(true, universeSettings)
        {
        }

        /// <summary>
        /// Performs coarse universe selection. Filters for stocks with fundamental data,
        /// positive price and volume, and ranks them by dollar volume.
        /// </summary>
        public override IEnumerable<Symbol> SelectCoarse(QCAlgorithm algorithm, IEnumerable<CoarseFundamental> coarse)
        {
            // Skip re-selection if we're still in the same month
            if (algorithm.Time.Month == _lastMonth)
            {
                return Universe.Unchanged;
            }

            // Filter stocks that have fundamental data and are actively traded (positive price and volume)
            var sortedByDollarVolume =
                (from x in coarse
                 where x.HasFundamentalData && x.Volume > 0 && x.Price > 0
                 orderby x.DollarVolume descending
                 select x).Take(_numberOfSymbolsCoarse).ToList();

            // Store dollar volume values for use in fine selection
            _dollarVolumeBySymbol.Clear();
            foreach (var x in sortedByDollarVolume)
            {
                _dollarVolumeBySymbol[x.Symbol] = x.DollarVolume;
            }

            // Return unchanged universe if no valid stocks found
            if (_dollarVolumeBySymbol.Count == 0)
            {
                return Universe.Unchanged;
            }

            // Return the set of selected symbols for the next selection stage
            return _dollarVolumeBySymbol.Keys;
        }

        /// <summary>
        /// Performs fine universe selection. Filters for U.S. companies traded on NYSE/NASDAQ,
        /// listed for over 6 months, with positive ROA and PB ratios, and excludes financial sector.
        /// Applies sector-based normalization to select top stocks by dollar volume.
        /// </summary>
        public override IEnumerable<Symbol> SelectFine(QCAlgorithm algorithm, IEnumerable<FineFundamental> fine)
        {
            // Apply filters for U.S. companies with required fundamentals and exclude financials
            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.OperationRatios.ROA.Value > 0
                    && x.ValuationRatios.PBRatio > 0
                    && x.CompanyReference.IndustryTemplateCode != "FN"
                    && x.MarketCap > 50000000m
                 select x).ToList();

            // Exit if no symbols passed fine filtering
            if (filteredFine.Count == 0)
            {
                return Universe.Unchanged;
            }

            // Mark the current month to prevent re-processing during the same month
            _lastMonth = algorithm.Time.Month;

            // Determine the selection ratio to apply within each sector
            var percent = _numberOfSymbolsFine / (double)filteredFine.Count;

            // Group stocks by industry sector and select a top percentage from each group by dollar volume
            var topFineBySector =
                (from x in filteredFine
                 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);

            // Flatten selected stocks from all sectors, sort again by dollar volume and return top N symbols
            return topFineBySector.SelectMany(x => x.Value)
                .OrderByDescending(x => _dollarVolumeBySymbol[x.Symbol])
                .Take(_numberOfSymbolsFine)
                .Select(x => x.Symbol);
        }
    }
}
/*#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
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
{
    public class MagicFormulaStrategy : QCAlgorithm
    {
        private RateOfChange _spyROC252;  // Indicator for SPY momentum
        private RateOfChange _bilROC21;
        private RateOfChange _iefROC21;
        private const int MomentumLookbackPeriod = 252; // Lookback period of 252 trading days (~1 year)
        private const int PortfolioStockSize = 25; // Number of top stocks to select for the portfolio
   
        public override void Initialize()
        {
            // Set start and end dates for backtest
            SetStartDate(1998, 1, 1);
            SetEndDate(DateTime.Now);
            SetCash(100000); // Set initial cash to $100,000

            // Rule 1: Add SPY for market regime filter (momentum check)
            var spy = AddEquity("SPY", Resolution.Daily);
            _spyROC252 = ROC(spy.Symbol, MomentumLookbackPeriod, Resolution.Daily); // Compute the 252-day momentum for SPY

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


            SetBenchmark(spy.Symbol); // Set SPY as the benchmark for performance comparison

            // Set universe selection model to filter for liquid U.S. stocks
            UniverseSettings.Resolution = Resolution.Daily;
            SetUniverseSelection(new MagicFormulaUniverseSelectionModel()); // Use the MagicFormulaUniverseSelectionModel to select stocks

            SetWarmUp(MomentumLookbackPeriod); // Set a warm-up period for the momentum calculation
            Schedule.On(DateRules.MonthEnd(), TimeRules.AfterMarketOpen("SPY", 0), ExecuteYearStartTrading); // Schedule yearly trading action
           //Schedule.On(DateRules.MonthStart(), TimeRules.AfterMarketOpen("SPY", 0), ExecuteMonthly); // Schedule monthly rebalancing
        }

        private void ExecuteYearStartTrading()
        {
            // Ensure the algorithm is not warming up before executing trades
            if (IsWarmingUp) return;

            // Check if it's December and if SPY's momentum is positive, then select the top stocks
            //if ((Time.Month == 12 || Time.Month == 9 || Time.Month == 6 || Time.Month == 3) && _spyROC252 >= 0)
            if ((Time.Month == 12))// && _spyROC252 >= 0)
            {
                top25Stocks(); // Select top 25 stocks 
            }
        }

        private void ExecuteMonthly()
        {
            // If SPY momentum is negative, liquidate the portfolio
            if (_spyROC252 < 0)
            {
                Liquidate();
                if (_iefROC21 > _bilROC21 && _iefROC21 > 0)
                {
                    SetHoldings("IEF", 1);
                }
            }
            else
            {
                // If the portfolio is not invested, select the top stocks for investment
                if (Portfolio["IEF"].Invested || !Portfolio.Invested)
                {
                    top25Stocks(); // Select top 50 stocks based on PSR and momentum
                }
            }
        }

        private void top25Stocks()
        {
            // Filtrar las acciones que tienen valores fundamentales disponibles
            var validSymbols = ActiveSecurities.Keys
                .Where(x => Securities.ContainsKey(x) &&
                            Securities[x].Fundamentals != null &&
                            Securities[x].Fundamentals.ValuationRatios.PBRatio > 0 &&
                            Securities[x].Fundamentals.OperationRatios.ROA.Value > 0)
                .ToList();

            // Obtener las métricas fundamentales
            var fundamentals = validSymbols
                .Select(s => new
                {
                    Symbol = s,
                    PB = Securities[s].Fundamentals.ValuationRatios.PBRatio,
                    ROA = Securities[s].Fundamentals.OperationRatios.ROA.Value
                })
                .OrderByDescending(x => x.ROA)
                .ThenBy(x => x.PB)
                .Take(PortfolioStockSize)
                .ToList();

            // Liquidar y comprar acciones seleccionadas
            Liquidate();
            foreach (var stock in fundamentals)
            {
                SetHoldings(stock.Symbol, 1m / PortfolioStockSize);
            }
        }
    }
}
*/