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