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