Overall Statistics
Total Orders
9696
Average Win
0.10%
Average Loss
-0.02%
Compounding Annual Return
16.369%
Drawdown
2.400%
Expectancy
0.222
Start Equity
10000000
End Equity
11635330.71
Net Profit
16.353%
Sharpe Ratio
2.396
Sortino Ratio
5.961
Probabilistic Sharpe Ratio
98.352%
Loss Rate
83%
Win Rate
17%
Profit-Loss Ratio
6.03
Alpha
0.105
Beta
-0.042
Annual Standard Deviation
0.042
Annual Variance
0.002
Information Ratio
0.194
Tracking Error
0.121
Treynor Ratio
-2.427
Total Fees
$486876.29
Estimated Strategy Capacity
$1500000.00
Lowest Capacity Asset
SIVB R735QTJ8XC9X
Portfolio Turnover
109.03%
#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
    {
        public int MaxPositions = 20;
        private Universe _universe;
        [Parameter("universeSize")]
        private int _universeSize = 1000;
        private decimal _atrThreshold = 0.5m;
        private int _indicatorPeriod = 14; // days
        [Parameter("openingRangeMinutes")]
        private int _openingRangeMinutes = 5;
        private int _leverage = 4;
        private Dictionary<Symbol, SymbolData> _symbolDataBySymbol = new();

        public override void Initialize()
        {
            SetStartDate(2016, 1, 1);
            SetEndDate(2017, 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.Asynchronous = true;
            UniverseSettings.Schedule.On(DateRules.MonthStart(spy));
            _universe = AddUniverse(fundamentals => fundamentals
                .Where(f => f.Price > 5 && f.Symbol != spy)
                .OrderByDescending(f => f.DollarVolume)
                .Take(_universeSize)
                .Select(f => f.Symbol)
                .ToList()
            );

            Schedule.On(DateRules.EveryDay(spy), TimeRules.BeforeMarketClose(spy, 1), () => Liquidate());  // Close all the open positions and cancel standing orders.
            SetWarmUp(TimeSpan.FromDays(2 * _indicatorPeriod));
        }
        
        public override void OnSecuritiesChanged(SecurityChanges changes)
        {
            // Add indicators for each asset that enters the universe.
            foreach (var security in changes.AddedSecurities)
            {
                _symbolDataBySymbol[security.Symbol] = new SymbolData(this, security, _openingRangeMinutes, _indicatorPeriod);
            }
        }

        public override void OnData(Slice slice)
        {
            if (IsWarmingUp || !(Time.Hour == 9 && Time.Minute == 30 + _openingRangeMinutes)) return;
            // Select the stocks in play.
            var filtered = ActiveSecurities.Values
                // Filter 1: Select assets in the unvierse that have a relative volume greater than 100%.
                .Where(s => s.Price != 0 && _universe.Selected.Contains(s.Symbol)).Select(s => _symbolDataBySymbol[s.Symbol]).Where(s => s.RelativeVolume > 1 && s.ATR > _atrThreshold)
                // Filter 2: Select the top 20 assets with the greatest relative volume.
                .OrderByDescending(s => s.RelativeVolume).Take(MaxPositions);
            // Look for trade entries.
            foreach (var symbolData in filtered)
            {
                symbolData.Scan();
            }
        }

        public override void OnOrderEvent(OrderEvent orderEvent)
        {
            if (orderEvent.Status != OrderStatus.Filled) return;
            _symbolDataBySymbol[orderEvent.Symbol].OnOrderEvent(orderEvent.Ticket);
        }
    }

    class SymbolData 
    {
        public decimal? RelativeVolume;
        public TradeBar OpeningBar = new();
        private OpeningRangeBreakoutUniverseAlgorithm _algorithm;
        private Security _security;
        private decimal _stopLossAtrDistance = 0.1m; // 0.1 => 10% of ATR
        private decimal _stopLossRiskSize = 0.01m; // 0.01 => Lose 1% of the portfolio if stop loss is hit
        private IDataConsolidator Consolidator;
        public AverageTrueRange ATR;
        private SimpleMovingAverage VolumeSMA;
        private decimal StopLossPrice;
        private OrderTicket EntryTicket, StopLossTicket;


        public SymbolData(OpeningRangeBreakoutUniverseAlgorithm algorithm, Security security, int openingRangeMinutes, int indicatorPeriod) 
        {
            _algorithm = algorithm;
            _security = security;
            Consolidator = algorithm.Consolidate(security.Symbol, TimeSpan.FromMinutes(openingRangeMinutes), ConsolidationHandler);
            ATR = algorithm.ATR(security.Symbol, indicatorPeriod, resolution: Resolution.Daily);
            VolumeSMA = new SimpleMovingAverage(indicatorPeriod);
        }

        void ConsolidationHandler(TradeBar bar)
        {
            if (OpeningBar.Time.Date == bar.Time.Date) return;
            // Update the asset's indicators and save the day's opening bar.
            RelativeVolume = VolumeSMA.IsReady && VolumeSMA > 0 ? bar.Volume / VolumeSMA : null;
            VolumeSMA.Update(bar.EndTime, bar.Volume);
            OpeningBar = bar;
        }

        public void Scan() {
            // 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.
            if (OpeningBar.Close > OpeningBar.Open)
            {
                PlaceTrade(OpeningBar.High, OpeningBar.High - _stopLossAtrDistance * ATR);
            }
            else if (OpeningBar.Close < OpeningBar.Open)
            {
                PlaceTrade(OpeningBar.Low, OpeningBar.Low + _stopLossAtrDistance * ATR);
            }

        }

        public void PlaceTrade(decimal entryPrice, decimal stopPrice)
        {
            var quantity = (int)((_stopLossRiskSize * _algorithm.Portfolio.TotalPortfolioValue / _algorithm.MaxPositions) / (entryPrice - stopPrice));
            var quantityLimit = _algorithm.CalculateOrderQuantity(_security.Symbol, 1m/_algorithm.MaxPositions);
            quantity = (int)(Math.Min(Math.Abs(quantity), quantityLimit) * Math.Sign(quantity));
            if (quantity != 0)
            {
                StopLossPrice = stopPrice;
                EntryTicket = _algorithm.StopMarketOrder(_security.Symbol, quantity, entryPrice, $"Entry");
            }
        }

        public void OnOrderEvent(OrderTicket orderTicket)
        {
            // When the entry order is hit, place the exit order: Stop loss based on ATR.
            if (orderTicket == EntryTicket)
            {
                StopLossTicket = _algorithm.StopMarketOrder(_security.Symbol, -EntryTicket.Quantity, StopLossPrice, tag: "ATR Stop");
            }
        }
    }
}