Overall Statistics |
Total Orders 9696 Average Win 0.10% Average Loss -0.02% Compounding Annual Return 16.313% Drawdown 2.400% Expectancy 0.221 Start Equity 10000000 End Equity 11629672.86 Net Profit 16.297% Sharpe Ratio 2.388 Sortino Ratio 5.938 Probabilistic Sharpe Ratio 98.301% Loss Rate 83% Win Rate 17% Profit-Loss Ratio 6.03 Alpha 0.104 Beta -0.042 Annual Standard Deviation 0.042 Annual Variance 0.002 Information Ratio 0.191 Tracking Error 0.121 Treynor Ratio -2.413 Total Fees $487842.09 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 { // parameters [Parameter("MaxPositions")] public int MaxPositions = 20; [Parameter("universeSize")] private int _universeSize = 1000; [Parameter("excludeETFs")] private int _excludeETFs = 0; [Parameter("atrThreshold")] private decimal _atrThreshold = 0.5m; [Parameter("indicatorPeriod")] private int _indicatorPeriod = 14; // days [Parameter("openingRangeMinutes")] private int _openingRangeMinutes = 5; // when to place entries [Parameter("stopLossAtrDistance")] public decimal stopLossAtrDistance = 0.1m; // distance for stop loss, fraction of ATR [Parameter("stopLossRiskSize")] public decimal stopLossRiskSize = 0.01m; // 0.01 => Lose maximum of 1% of the portfolio if stop loss is hit [Parameter("reversing")] public int reversing = 0; // on stop loss also open reverse position and place stop loss at the original entry price [Parameter("maximisePositions")] private int _maximisePositions = 0; // sends twice as much entry orders, cancel remaining orders when all positions are filled [Parameter("secondsResolution")] private int _secondsResolution = 0; // switch to seconds resolution for more precision [SLOW!] // todo: implement doubling [Parameter("doubling")] // double position when in profit, not ready yet private int _doubling = 0; [Parameter("fees")] // enable or disable broker fees private int _fees = 0; private int _leverage = 4; private Universe _universe; private bool _entryPlaced = false; private int _maxLongPositions = 0; private int _maxShortPositions = 0; private int _maxPositions = 0; private decimal _maxMarginUsed = 0.0m; 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; if (_fees == 0) { SetBrokerageModel(BrokerageName.Alpaca); } // 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; if (_secondsResolution == 1) UniverseSettings.Resolution = Resolution.Second; UniverseSettings.Asynchronous = true; UniverseSettings.Schedule.On(DateRules.MonthStart(spy)); _universe = AddUniverse(fundamentals => fundamentals .Where(f => f.Price > 5 && (_excludeETFs == 0 || f.HasFundamentalData) && f.Symbol != spy) // && f.MarketCap < ??? .OrderByDescending(f => f.DollarVolume) .Take(_universeSize) .Select(f => f.Symbol) .ToList() ); Schedule.On(DateRules.EveryDay(spy), TimeRules.AfterMarketOpen(spy, 0), () => ResetVars()); Schedule.On(DateRules.EveryDay(spy), TimeRules.BeforeMarketClose(spy, 1), () => Liquidate()); // Close all the open positions and cancel standing orders. Schedule.On(DateRules.EveryDay(spy), TimeRules.BeforeMarketClose(spy, 1), () => UpdatePlots()); SetWarmUp(TimeSpan.FromDays(2 * _indicatorPeriod)); Log( $"MaxPositions={MaxPositions}, universeSize={_universeSize}, excludeETFs={_excludeETFs}, atrThreshold={_atrThreshold}, " + $"indicatorPeriod={_indicatorPeriod}, openingRangeMinutes={_openingRangeMinutes}, stopLossAtrDistance={stopLossAtrDistance}, " + $"stopLossRiskSize={stopLossRiskSize}, reversing={reversing}, maximisePositions={_maximisePositions}, " + $"secondsResolution={_secondsResolution}, doubling={_doubling}, fees={_fees}" ); } private void ResetVars() { _entryPlaced = false; _maxLongPositions = 0; _maxShortPositions = 0; _maxPositions = 0; _maxMarginUsed = 0.0m; } private void UpdatePlots() { Plot("Positions", "Long", _maxLongPositions); Plot("Positions", "Short", _maxShortPositions); Plot("Positions", "Total", _maxPositions); Plot("Margin", "Used", _maxMarginUsed); } 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) { int LongPositions = 0, ShortPositions = 0; foreach (var kvp in Portfolio) { if (kvp.Value.Quantity > 0) LongPositions += 1; if (kvp.Value.Quantity < 0) ShortPositions += 1; } _maxLongPositions = Math.Max(_maxLongPositions, LongPositions); _maxShortPositions = Math.Max(_maxShortPositions, ShortPositions); _maxPositions = Math.Max(_maxPositions, LongPositions + ShortPositions); _maxMarginUsed = Math.Max(_maxMarginUsed, Portfolio.TotalMarginUsed / Portfolio.TotalPortfolioValue); if (IsWarmingUp || _entryPlaced) return; if (!(Time.Hour == 9 && Time.Minute == 30 + _openingRangeMinutes)) return; // Select the stocks in play. var take = 1; if (_maximisePositions == 1) take = 2; 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*take); // Look for trade entries. foreach (var symbolData in filtered) { symbolData.Scan(); } _entryPlaced = true; } public override void OnOrderEvent(OrderEvent orderEvent) { if (orderEvent.Status != OrderStatus.Filled) return; _symbolDataBySymbol[orderEvent.Symbol].OnOrderEvent(orderEvent.Ticket); } public void CheckToCancelRemainingEntries() { if (_maximisePositions == 0) return; int openPositionsCount = 0; foreach (var kvp in Portfolio) { if (kvp.Value.Invested) openPositionsCount += 1; } if (openPositionsCount >= MaxPositions) { foreach (var symbolData in _symbolDataBySymbol.Values) { if (symbolData.EntryTicket != null && symbolData.EntryTicket.Status == OrderStatus.Submitted) { symbolData.EntryTicket.Cancel(); symbolData.EntryTicket = null; } } } } } class SymbolData { public decimal? RelativeVolume; public TradeBar OpeningBar = new(); private OpeningRangeBreakoutUniverseAlgorithm _algorithm; private Security _security; private IDataConsolidator Consolidator; public AverageTrueRange ATR; private SimpleMovingAverage VolumeSMA; private decimal EntryPrice, StopLossPrice; private int Quantity; public OrderTicket EntryTicket, StopLossTicket; public bool Reversed = false; 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 - _algorithm.stopLossAtrDistance * ATR); } else if (OpeningBar.Close < OpeningBar.Open) { PlaceTrade(OpeningBar.Low, OpeningBar.Low + _algorithm.stopLossAtrDistance * ATR); } Reversed = false; } public void PlaceTrade(decimal entryPrice, decimal stopPrice) { var quantity = (int)((_algorithm.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) { EntryPrice = entryPrice; StopLossPrice = stopPrice; Quantity = quantity; 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, -Quantity, StopLossPrice, tag: "ATR Stop"); _algorithm.CheckToCancelRemainingEntries(); } // reverse position on stop loss. Will slip toom much in backtesting but stop orders could overuse margin if (orderTicket == StopLossTicket && _algorithm.reversing == 1 && !Reversed) { _algorithm.MarketOrder(_security.Symbol, -Quantity, tag: "Reversed"); StopLossTicket = _algorithm.StopMarketOrder(_security.Symbol, Quantity, EntryPrice, tag: "Reversed ATR Stop"); Reversed = true; } } } }