Overall Statistics
Total Orders
10367
Average Win
0.06%
Average Loss
-0.04%
Compounding Annual Return
3.043%
Drawdown
3.900%
Expectancy
0.020
Start Equity
10000000
End Equity
10283733.61
Net Profit
2.837%
Sharpe Ratio
-0.929
Sortino Ratio
-1.463
Probabilistic Sharpe Ratio
31.716%
Loss Rate
60%
Win Rate
40%
Profit-Loss Ratio
1.54
Alpha
-0.026
Beta
-0.045
Annual Standard Deviation
0.036
Annual Variance
0.001
Information Ratio
-1.71
Tracking Error
0.113
Treynor Ratio
0.743
Total Fees
$238173.41
Estimated Strategy Capacity
$10000000.00
Lowest Capacity Asset
VONV UQ5SP71GEJXH
Portfolio Turnover
83.91%
#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.Algorithm.Selection;
    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 QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
    using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
#endregion
namespace QuantConnect.Algorithm.CSharp
{
    public class OpeningRangeBreakoutUniverseAlgorithm : QCAlgorithm
    {
        private List<Symbol> _selected = new();
        private Universe _universe;
        private int _universeSize = 1000;
        private int _indicatorPeriod = 14; // days
        private decimal _stopLossAtrDistance = 0.5m; // 0.1 => 10% of ATR
        private decimal _stopLossRiskSize = 0.01m; // 0.01 => Lose 1% of the portfolio if stop loss is hit
        private int _maxPositions = 20;
        private int _openingRangeMinutes = 5;
        private int _leverage = 10;

        public override void Initialize()
        {
            SetStartDate(2024, 1, 1);
            SetCash(10_000_000);
            Settings.AutomaticIndicatorWarmUp = true;

            // Add SPY so there is at least 1 asset at minute resolution to step the algorithm along.
            var spy = AddEquity("SPY").Symbol;

            // Add a universe of the most liquid US Equities.
            UniverseSettings.Leverage = _leverage;
            UniverseSettings.Schedule.On(DateRules.MonthStart(spy));
            _universe = AddUniverse(Universe.DollarVolume.Top(_universeSize));

            Schedule.On(DateRules.EveryDay(spy), TimeRules.AfterMarketOpen(spy), CreateConsolidators);
            Schedule.On(DateRules.EveryDay(spy), TimeRules.AfterMarketOpen(spy, _openingRangeMinutes + 1), ScanForEntries); //  1 minute late to allow consolidated bars time to update.
            Schedule.On(DateRules.EveryDay(spy), TimeRules.BeforeMarketClose(spy, 1), Exit);
            SetWarmUp(TimeSpan.FromDays(2 * _indicatorPeriod));
        }

        void CreateConsolidators()
        {
            // Create the consolidators that produce the opening range bars.
            foreach (var symbol in _universe.Selected)
            {
                (Securities[symbol] as dynamic).Consolidator = Consolidate(symbol, TimeSpan.FromMinutes(_openingRangeMinutes), ConsolidationHandler);
            }
        }
        
        public override void OnSecuritiesChanged(SecurityChanges changes)
        {
            // Add indicators for each asset that enters the universe.
            foreach (dynamic security in changes.AddedSecurities)
            {
                security.ATR = ATR(security.Symbol, _indicatorPeriod, resolution: Resolution.Daily);
                security.VolumeSMA = new SimpleMovingAverage(_indicatorPeriod);
            }
        }

        void ConsolidationHandler(TradeBar bar)
        {
            // Update the asset's indicators, save the day's opening bar, and remove the consolidator.
            dynamic security = Securities[bar.Symbol];
            security.RelativeVolume = security.VolumeSMA.IsReady && security.VolumeSMA.Current.Value > 0 ? bar.Volume / security.VolumeSMA.Current.Value : null;
            security.VolumeSMA.Update(bar.EndTime, bar.Volume);
            security.OpeningBar = bar;
            SubscriptionManager.RemoveConsolidator(security.Symbol, security.Consolidator); // Remove consolidator since we only need the first bar each day.
        }

        private void ScanForEntries()
        {
            if (IsWarmingUp)
            {
                return;
            }
            // Select the stocks in play.
            var securities = Securities.Values
                // Filter 1: Select assets in the unvierse that have a relative volume greater than 1.
                .Where(s => _universe.Selected.Contains(s.Symbol)).Select(s => s as dynamic).Where(s => s.Price != 0 && s.RelativeVolume > 1)
                // Filter 2: Select the top 20 assets with the greatest relative volume.
                .OrderByDescending(s => s.RelativeVolume).Take(_maxPositions).ToList();
            // Create orders for the stocks in play.
            var orders = new List<Dictionary<string, object>>();
            foreach (var security in securities)
            {
                var atr = security.ATR.Current.Value;
                // Calculate position sizes so that if you fill an order at the high (low) of the first 5-minute bar 
                // and hit a stop loss based on 10% of the ATR, you only lose x% of portfolio value.
                var bar = security.OpeningBar;
                if (bar.Close > bar.Open)
                {
                    orders.Add(new Dictionary<string, object>
                    {
                        { "security", security },
                        { "entryPrice", bar.High },
                        { "stopPrice", bar.High - _stopLossAtrDistance * atr }
                    });
                }
                else if (bar.Close < bar.Open)
                {
                    orders.Add(new Dictionary<string, object>
                    {
                        { "security", security },
                        { "entryPrice", bar.Low },
                        { "stopPrice", bar.Low + _stopLossAtrDistance * atr }
                    });
                }
            }
            foreach (var order in orders)
            {
                dynamic security = order["security"];
                var quantity = (int)((_stopLossRiskSize * Portfolio.TotalPortfolioValue / _maxPositions) / ((decimal)order["entryPrice"] - (decimal)order["stopPrice"]));
                var quantityLimit = CalculateOrderQuantity(security.Symbol, 1m/_maxPositions);
                quantity = (int)(Math.Min(Math.Abs(quantity), quantityLimit) * Math.Sign(quantity));
                if (quantity != 0)
                {
                    security.StopLossPrice = order["stopPrice"];
                    security.EntryTicket = StopMarketOrder(security.Symbol, quantity, (decimal)order["entryPrice"], "Entry");
                }
            }
        }

        public override void OnOrderEvent(OrderEvent orderEvent)
        {
            if (orderEvent.Status != OrderStatus.Filled)
            {
                return;
            }
            dynamic security = Securities[orderEvent.Symbol];
            // When the entry order is hit, place the exit order: Stop loss based on ATR.
            if (orderEvent.Ticket == security.EntryTicket)
            {
                security.StopLossTicket = StopMarketOrder(orderEvent.Symbol, -security.EntryTicket.Quantity, security.StopLossPrice, tag: "ATR Stop");
            }
            // When the stop loss order is hit, cancel the MOC order.
            else if (orderEvent.Ticket == security.StopLossTicket)
            {
                CreateEmptyOrderTickets(security);
            }
        }

        private void CreateEmptyOrderTickets(dynamic security)
        {
            security.EntryTicket = null;
            security.StopLossTicket = null;
        }

        private void Exit()
        {
            // Close all the open positions, cancel standing orders, and remove the saved order tickets.
            Liquidate();
            foreach (var symbol in _selected)
            {
                CreateEmptyOrderTickets(Securities[symbol]);
            }
            // Clear the list of today's stocks in play.
            _selected.Clear();
        }
    }
}