Overall Statistics
Total Orders
0
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Start Equity
10000000
End Equity
10000000
Net Profit
0%
Sharpe Ratio
0
Sortino Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
4.591
Tracking Error
0.15
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
#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 Symbol _spy;
        private Dictionary<Symbol, SelectionData> _selectionDataBySymbol = new();
        private List<Equity> _universe = new();
        private List<Fundamental> _fundamentals = new();

        // Set the universe parameters.
        private int _indicatorPeriod = 14; // days
        private int _meanVolumeThreshold = 1_000_000; // Shares
        private decimal _atrThreshold = 0.5m;

        // Set the trading parameters.
        private decimal _stopLossAtrDistance = 0.1m; // 0.1 => 10% of ATR
        private decimal _stopLossRiskSize = 0.0001m; // 0.01 => Lose 1% of the portfolio if stop loss is hit
        private int _maxPositions = 20;
        private int _openingRangeMinutes = 5;

        public override void Initialize()
        {
            SetStartDate(2016, 1, 1);
            SetEndDate(2016, 1, 12);
            SetCash(10_000_000);

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

            AddUniverse(GetFundamentals);
            Schedule.On(DateRules.EveryDay(_spy), TimeRules.AfterMarketOpen(_spy, 0), FillUniverse);
            Schedule.On(DateRules.EveryDay(_spy), TimeRules.AfterMarketOpen(_spy, _openingRangeMinutes), ScanForEntries);
            Schedule.On(DateRules.EveryDay(_spy), TimeRules.BeforeMarketClose(_spy, 16), CancelMissedEntries);
            Schedule.On(DateRules.EveryDay(_spy), TimeRules.BeforeMarketClose(_spy, 1), CancelStopLosses);
            Schedule.On(DateRules.EveryDay(_spy), TimeRules.AfterMarketClose(_spy, 10), EmptyUniverse);
        }

        private IEnumerable<Symbol> GetFundamentals(IEnumerable<Fundamental> fundamentals)
        {
            _fundamentals = fundamentals.ToList();
            return new List<Symbol>();
        }

        private void FillUniverse()
        {
            // Organize symbols into groups: New symbols, existing symbols, expired symbols.
            var previousSymbols = _selectionDataBySymbol.Keys.ToHashSet();
            var currentSymbols = _fundamentals.Select(f => f.Symbol).ToHashSet();
            var expiredSymbols = previousSymbols.Except(currentSymbols);
            var newSymbols = currentSymbols.Except(previousSymbols);
            var existingSymbols = previousSymbols.Intersect(currentSymbols);

            // Remove SelectionData objects of Symbols no longer in the universe.
            foreach (var symbol in expiredSymbols)
            {
                _selectionDataBySymbol.Remove(symbol);
            }

            // Update SelectionData objects of Symbols that were in yesterday's universe.
            foreach (var bars in History<TradeBar>(existingSymbols, 1, Resolution.Daily))
            {
                foreach (var kvp in bars)
                {
                    _selectionDataBySymbol[kvp.Key].Update(kvp.Value);
                }
            }

            // Create and warm-up SelectionData objects of Symbols that entered the universe
            foreach (var symbol in newSymbols)
            {
                _selectionDataBySymbol[symbol] = new SelectionData(_indicatorPeriod);
            }
            foreach (var bars in History<TradeBar>(existingSymbols, 1, Resolution.Daily))
            {
                foreach (var kvp in bars)
                {
                    _selectionDataBySymbol[kvp.Key].Update(kvp.Value);
                }
            }

            // Select assets based on price, mean trading volume, and ATR.
            var selected = _fundamentals.Where(f =>
            {
                if (_selectionDataBySymbol.TryGetValue(f.Symbol, out var selectionData))
                {
                    return f.Price > 5m &&
                        selectionData.IsReady &&
                        selectionData.MeanVolume.Current.Value > _meanVolumeThreshold &&
                        selectionData.Atr.Current.Value > _atrThreshold;
                }
                return false;
            }).Select(f => f.Symbol).ToList();

            Plot("Universe", "Selected", selected.Count);

            // Add the selected assets to the algorithm.
            foreach (var symbol in selected)
            {
                var equity = AddEquity(symbol);
                CreateEmptyOrderTickets(equity);
                _universe.Add(equity);
            }
        }

        // Create some members on the Equity object to store each order ticket.
        private void CreateEmptyOrderTickets(dynamic equity)
        {
            equity.EntryTicket = null;
            equity.StopLossTicket = null;
            equity.EodLiquidationTicket = null;
        }

        private void ScanForEntries()
        {
            Plot("Universe", "Size", _universe.Count);

            // Get history for the assets over the last 2 weeks.
            var symbols = _universe.Select(equity => equity.Symbol).Distinct();
            var history = History<TradeBar>(symbols, TimeSpan.FromDays(14) + TimeSpan.FromMinutes(_openingRangeMinutes));

            // Select assets with abnormally high volume for the day so far. Filter: Relative Volume > 100%.
            //  1) Calculate volume within the first 5 minutes of the day for each asset, over the last 14 days.
            //  2) Relative Volume = volume of today / mean(volume of triailing days)
            var relativeVolumeBySymbol = new Dictionary<Symbol, decimal>();
            var opens = new Dictionary<Symbol, decimal>();
            var closes = new Dictionary<Symbol, decimal>();
            foreach (var symbol in symbols)
            {
                var symbolHistory = history.Select(bars => bars[symbol]);
                // Calculate volume within the first _openingRangeMinutes of each day
                var volumesByDay = symbolHistory
                    .GroupBy(h => h.Time.Date)
                    .Select(g => new
                    {
                        Date = g.Key,
                        Volume = g.Take(_openingRangeMinutes).Sum(x => x.Volume)
                    })
                    .ToList();

                // Compute relative volume
                var todayVolume = volumesByDay.Last().Volume;
                var pastVolumes = volumesByDay.Take(volumesByDay.Count - 1).Select(v => v.Volume).Average();
                var relativeVolume = todayVolume / pastVolumes;
                if (relativeVolume > 1.0m) // Filter: Relative Volume > 100%
                {
                    relativeVolumeBySymbol[symbol] = relativeVolume;
                }

                // Store open and close prices for the last _openingRangeMinutes
                opens[symbol] = symbolHistory.TakeLast(_openingRangeMinutes).First().Open;
                closes[symbol] = symbolHistory.Last().Close;
            }

            // Select top _maxPositions assets with the greatest Relative Volume
            var selectedSymbols = relativeVolumeBySymbol
                .OrderByDescending(kv => kv.Value)
                .Take(_maxPositions)
                .Select(kv => kv.Key)
                .ToList();

            var orders = new List<Dictionary<string, object>>();
            foreach (var symbol in selectedSymbols)
            {
                decimal entryPrice, stopPrice;
                var symbolHistory = history.Select(bars => bars[symbol]);
                if (closes[symbol] > opens[symbol]) // Gainers
                {
                    entryPrice = symbolHistory.TakeLast(_openingRangeMinutes).Select(x => x.High).Max();
                    stopPrice = entryPrice - _stopLossAtrDistance * _selectionDataBySymbol[symbol].Atr.Current.Value;
                }
                else if (closes[symbol] < opens[symbol]) // Losers
                {
                    entryPrice = symbolHistory.TakeLast(_openingRangeMinutes).Select(x => x.Low).Min();
                    stopPrice = entryPrice + _stopLossAtrDistance * _selectionDataBySymbol[symbol].Atr.Current.Value;
                }
                else
                {
                    continue;
                }

                orders.Add(new Dictionary<string, object> 
                { 
                    { "symbol", symbol }, 
                    { "entry_price", entryPrice }, 
                    { "stop_price", stopPrice } 
                });
            }

            foreach (var order in orders)
            {
                var symbol = (Symbol)order["symbol"];
                dynamic security = Securities[symbol];
                var entryPrice = (decimal)order["entry_price"];
                var stopPrice = (decimal)order["stop_price"];
                var quantity = (int)((_stopLossRiskSize * Portfolio.TotalPortfolioValue) / (entryPrice - stopPrice));

                if (quantity > 0)
                {
                    security.StopLossPrice = stopPrice;
                    security.EntryTicket = StopMarketOrder(symbol, quantity, entryPrice, "Entry");
                }
            }

            // Remove untraded assets from the universe.
            for (int i = _universe.Count - 1; i >= 0; i--)
            {
                dynamic equity = _universe[i];
                if (equity.Symbol == _spy || equity.EntryTicket != null)
                {
                    continue;
                }
                RemoveSecurity(equity.Symbol);
                _universe.RemoveAt(i);
            }
        }

        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 orders.
            if (orderEvent.Ticket == security.EntryTicket)
            {
                var quantity = -security.EntryTicket.Quantity;
                // Place exit 1: Stop loss based on ATR.
                security.StopLossTicket = StopMarketOrder(orderEvent.Symbol, quantity, security.StopLossPrice, "ATR Stop");
                // Place exit 2: Liquidate at market close.
                security.EodLiquidationTicket = MarketOnCloseOrder(orderEvent.Symbol, quantity, "EoD Stop");
            }
            // When the stop loss order is hit, cancel the MOC order.
            else if (orderEvent.Ticket == security.StopLossTicket)
            {
                security.EodLiquidationTicket.Cancel("Stop loss hit");
                CreateEmptyOrderTickets(security);
            }
            else
            {
                Debug($"Unhandled order fill at {Time}");
            }
        }

        // If the entry order is never hit, cancel it 16 minutes before the close.
        // After 16 minutes, you're too close to market close to be allowed to place MOC orders.
        private void CancelMissedEntries()
        {
            foreach (dynamic equity in _universe)
            {
                var ticket = equity.EntryTicket;
                if (ticket != null && ticket.Status != OrderStatus.Filled)
                {
                    ticket.Cancel("Entry never filled"); // Cancel the entry order
                }
            }
        }

        // If the stop loss order isn't hit by 1-minute before market close, cancel it so it doesn't 
        // fill at the same time the MOC order fills.
        private void CancelStopLosses()
        {
            foreach (dynamic equity in _universe)
            {
                var ticket = equity.StopLossTicket;
                if (ticket != null && ticket.Status != OrderStatus.Filled)
                {
                    ticket.Cancel("Cancel before MOC fills"); // Cancel the stop loss order
                }
            }
        }

        // After the market closes, empty the universe and clear the order tickets.
        private void EmptyUniverse()
        {
            foreach (var equity in _universe)
            {
                if (equity.Symbol == _spy)
                {
                    continue;
                }
                CreateEmptyOrderTickets(equity);
                RemoveSecurity(equity.Symbol);
            }
            _universe.Clear();
        }
    }

    public class SelectionData
    {
        public SimpleMovingAverage MeanVolume;
        public AverageTrueRange Atr;

        public SelectionData(int period)
        {
            MeanVolume = new SimpleMovingAverage(period);
            Atr = new AverageTrueRange(period);
        }

        public void Update(TradeBar bar)
        {
            MeanVolume.Update(bar.EndTime, bar.Volume);
            Atr.Update(bar);
        }

        public bool IsReady => Atr.IsReady && MeanVolume.IsReady;
    }
}