Hey all!
Here's an algorithm I threw together to try and showcase various features and techniques available within QuantConnect. This algorithm tries to detect an opening breakout in the direction of the short term trend and takes a position in that direction. The opening breakout is nominally defined as trading outside of the range established in the first 3 minutes of trading. Orders are always submitted with accompanying stop market orders which are gradually tightened using PSAR after some initial profit condition is met.
I've tried to keep it fairly simple but still provide a good framework. As an algorithm grows in complexity, it's typically beneficial to split the pieces out into stand-alone components that can be reused between algorithms, but for the sake of simplicity and understandability, I've left all the code in one file.
In this algorithm you'll see various features and techniques including:
* Scheduled Daily Events
* History function to warm up indicators manually
* StopMarket order updates
* Dynamic position sizing using ATR/allowable losses
* Custom indicator on custom (30 min) interval
* Custom leverage settings
* Decent logging functionality
* Plotting considerations made for backtest and live
There's still plenty of work to be done here before it is made into a good, reliably profitable algorithm. Some things that could be worked on is:
* Setting the stop loss % based on the position size instead of a constant, this way we decide to lose a max% of our portfolio per trade.
* Better entry execution could be done by not entering the position once the signal is generated, but to wait for a reversion to VWAP or for a price point that is ~2 standard deviations from the mean in the beneficial direction.
* Better exit execution, currently it exits based on the stop loss criteria and the encroaching PSAR.
* There's also work that could be done to allow the algorithm to enter a position in the opposite direction of the trend, maybe given a larger breakout threshold from the opening range.
These are just a few areas of the algorithm that could be improved. If you make improvements, please share the algorithm back to this thread and we as a community can learn together!
EDIT: Looks like a previous edit removed the attached algorithm, here it is, enjoy!
Nik milev
JayJayD
PSARMin.Update((TradeBar)Security.GetLastData());
in line 236 instead of register the indicator? JJMichael Handschuh
JayJayD
JayJayD
// define our longer term indicators STD14 = STD(Symbol, 14, Resolution.Daily); ATR14 = ATR(Symbol, 14, resolution: Resolution.Daily);
But when the indicators are smoothed, the EMA period is in hours:// smooth our ATR over a week, we'll use this to determine if recent volatilty warrants entrance var oneWeekInMarketHours = (int)(5 * 6.5); SmoothedATR14 = new ExponentialMovingAverage("Smoothed_" + ATR14.Name, oneWeekInMarketHours).Of(ATR14); // smooth our STD over a week as well SmoothedSTD14 = new ExponentialMovingAverage("Smoothed_" + STD14.Name, oneWeekInMarketHours).Of(STD14);
So I test the algorithm with the long term indicators in hours and the results become negatives! Another question, in the algorithm two schedules are used:// schedule an event to run every day at five minutes after our Symbol's market open Schedule.Event("MarketOpenSpan") .EveryDay(Symbol) .AfterMarketOpen(Symbol, minutesAfterOpen: OpeningSpanInMinutes) .Run(MarketOpeningSpanHandler); Schedule.Event("MarketOpen") .EveryDay(Symbol) .AfterMarketOpen(Symbol, minutesAfterOpen: -1) .Run(() => PSARMin.Reset());
Why the PSARMin indicator is reseted just a minute before market opens? Is the same to reset the indicator in the OnEndOFDay method? Thanks in advance, JJMichael Handschuh
SherpaTrader
i tired to modify this algorithm by adding AddUniverse, and setting the secuiry inisde CoarseSelectionFunction function, but i keep running into error, that symbol is not set. my goal was to select the stock that's trending and use that to apply this algorithm. how can i correct this error?
Â
During the algorithm initialization, the following exception has occurred: The ticker SPY was not found in the SymbolCache. Use the Symbol object as key instead. Accessing the securities collection/slice object by string ticker is only available for securities added with the AddSecurity-family methods. For more details, please check out the documentation
/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using QuantConnect.Brokerages; using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Data.UniverseSelection; using QuantConnect.Indicators; using QuantConnect.Orders; using QuantConnect.Securities; namespace QuantConnect.Algorithm.CSharp { /// <summary> /// /// QCU: Opening Breakout Algorithm /// /// In this algorithm we attempt to provide a working algorithm that /// addresses many of the primary algorithm concerns. These concerns /// are: /// /// 1. Signal Generation. /// This algorithm aims to generate signals for an opening /// breakout move before 10am. Signals are generated by /// producing the opening five minute bar, and then trading /// in the direction of the breakout from that bar. /// /// 2. Position Sizing. /// Positions are sized using recently the average true range. /// The higher the recently movement, the smaller position. /// This helps to reduce the risk of losing a lot on a single /// transaction. /// /// 3. Active Stop Loss. /// Stop losses are maintained at a fixed global percentage to /// limit maximum losses per day, while also a trailing stop /// loss is implemented using the parabolic stop and reverse /// in order to gauge exit points. /// /// </summary> /// <meta name="tag" content="strategy example" /> /// <meta name="tag" content="indicators" /> public class OpeningBreakoutAlgorithm : QCAlgorithm { // the equity symbol we're trading private string symbol = "SPY"; // plotting and logging control private const bool EnablePlotting = true; private const bool EnableOrderUpdateLogging = false; private const int PricePlotFrequencyInSeconds = 15; // risk control private const decimal MaximumLeverage = 4; private const decimal GlobalStopLossPercent = 0.001m; private const decimal PercentProfitStartPsarTrailingStop = 0.0003m; private const decimal MaximumPorfolioRiskPercentPerPosition = .0025m; // entrance criteria private const int OpeningSpanInMinutes = 3; private const decimal BreakoutThresholdPercent = 0.00005m; private const decimal AtrVolatilityThresholdPercent = 0.00275m; private const decimal StdVolatilityThresholdPercent = 0.005m; // this is the security we're trading public Security Security; // define our indicators used for trading decisions public AverageTrueRange ATR14; public StandardDeviation STD14; public AverageDirectionalIndex ADX14; public ParabolicStopAndReverse PSARMin; // smoothed values public ExponentialMovingAverage SmoothedSTD14; public ExponentialMovingAverage SmoothedATR14; // working variable to control our algorithm // this flag is used to run some code only once after the algorithm is warmed up private bool FinishedWarmup; // this is used to record the last time we closed a position private DateTime LastExitTime; // this is our opening n minute bar private TradeBar OpeningBarRange; // this is the ticket from our market order (entrance) private OrderTicket MarketTicket; // this is the ticket from our stop loss order (exit) private OrderTicket StopLossTicket; // this flag is used to indicate we've switched from a global, non changing // stop loss to a dynamic trailing stop using the PSAR private bool EnablePsarTrailingStop; /// <summary> /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized. /// </summary> public override void Initialize() { // initialize algorithm level parameters SetStartDate(2020, 9, 24); SetEndDate(2020, 9, 25); //SetStartDate(2014, 01, 01); //SetEndDate(2014, 06, 01); SetCash(100000); // leverage tradier $1 traders SetBrokerageModel(BrokerageName.TradierBrokerage); UniverseSettings.Resolution = Resolution.Second; // request high resolution equity data AddUniverse(CoarseSelectionFunction); //AddSecurity(SecurityType.Equity, symbol, Resolution.Second); // save off our security so we can reference it quickly later Security = Securities[symbol]; // Set our max leverage Security.SetLeverage(MaximumLeverage); // define our longer term indicators ADX14 = ADX(symbol, 28, Resolution.Hour); STD14 = STD(symbol, 14, Resolution.Daily); ATR14 = ATR(symbol, 14, resolution: Resolution.Daily); PSARMin = new ParabolicStopAndReverse(symbol, afStart: 0.0001m, afIncrement: 0.0001m); // smooth our ATR over a week, we'll use this to determine if recent volatilty warrants entrance var oneWeekInMarketHours = (int)(5*6.5); SmoothedATR14 = new ExponentialMovingAverage("Smoothed_" + ATR14.Name, oneWeekInMarketHours).Of(ATR14); // smooth our STD over a week as well SmoothedSTD14 = new ExponentialMovingAverage("Smoothed_"+STD14.Name, oneWeekInMarketHours).Of(STD14); // initialize our charts var chart = new Chart(symbol); chart.AddSeries(new Series(ADX14.Name, SeriesType.Line, 0)); chart.AddSeries(new Series("Enter", SeriesType.Scatter, 0)); chart.AddSeries(new Series("Exit", SeriesType.Scatter, 0)); chart.AddSeries(new Series(PSARMin.Name, SeriesType.Scatter, 0)); AddChart(chart); var history = History(symbol, 20, Resolution.Daily); foreach (var bar in history) { ADX14.Update(bar); ATR14.Update(bar); STD14.Update(bar.EndTime, bar.Close); } // schedule an event to run every day at five minutes after our symbol's market open Schedule.Event("MarketOpenSpan") .EveryDay(symbol) .AfterMarketOpen(symbol, minutesAfterOpen: OpeningSpanInMinutes) .Run(MarketOpeningSpanHandler); Schedule.Event("MarketOpen") .EveryDay(symbol) .AfterMarketOpen(symbol, minutesAfterOpen: -1) .Run(() => PSARMin.Reset()); } public IEnumerable<Symbol> CoarseSelectionFunction(IEnumerable<CoarseFundamental> coarse) { // sort descending by daily dollar volume var sortedByDollarVolume = coarse.OrderByDescending(x => x.DollarVolume); // take the top entries from our sorted collection var top = sortedByDollarVolume.Take(1); // we need to return only the symbol objects symbol = top.FirstOrDefault().Symbol.Value; return top.Select(x => x.Symbol); } /// <summary> /// This function is scheduled to be run every day at the specified number of minutes after market open /// </summary> public void MarketOpeningSpanHandler() { // request the last n minutes of data in minute bars, we're going to // define the opening rang var history = History(symbol, OpeningSpanInMinutes, Resolution.Minute); // this is our bar size var openingSpan = TimeSpan.FromMinutes(OpeningSpanInMinutes); // we only care about the high and low here OpeningBarRange = new TradeBar { // time values Time = Time - openingSpan, EndTime = Time, Period = openingSpan, // high and low High = Security.Close, Low = Security.Close }; // aggregate the high/low for the opening range foreach (var tradeBar in history) { OpeningBarRange.Low = Math.Min(OpeningBarRange.Low, tradeBar.Low); OpeningBarRange.High = Math.Max(OpeningBarRange.High, tradeBar.High); } // widen the bar when looking for breakouts OpeningBarRange.Low *= 1 - BreakoutThresholdPercent; OpeningBarRange.High *= 1 + BreakoutThresholdPercent; Log("---------" + Time.Date + "---------"); Log("OpeningBarRange: Low: " + OpeningBarRange.Low.SmartRounding() + " High: " + OpeningBarRange.High.SmartRounding()); } /// <summary> /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. /// </summary> /// <param name="data">Slice object keyed by symbol containing the stock data</param> public override void OnData(Slice data) { // we don't need to run any of this during our warmup phase if (IsWarmingUp) return; // when we're done warming up, register our indicators to start plotting if (!IsWarmingUp && !FinishedWarmup) { // this is a run once flag for when we're finished warmup FinishedWarmup = true; // plot our hourly indicators automatically, wait for them to ready PlotIndicator("ADX", ADX14); PlotIndicator("ADX", ADX14.NegativeDirectionalIndex, ADX14.PositiveDirectionalIndex); PlotIndicator("ATR", true, ATR14); PlotIndicator("STD", true, STD14); PlotIndicator("ATR", true, SmoothedATR14); } // update our PSAR PSARMin.Update((TradeBar) Security.GetLastData()); // plot price until an hour after we close so we can see our execution skillz if (ShouldPlot) { // we can plot price more often if we want Plot(symbol, "Price", Security.Close); // only plot psar on the minute if (PSARMin.IsReady) { Plot(symbol, PSARMin); } } // first wait for our opening range bar to be set to today if (OpeningBarRange == null || OpeningBarRange.EndTime.Date != Time.Date || OpeningBarRange.EndTime == Time) return; // we only trade max once per day, so if we've already exited the stop loss, bail if (StopLossTicket != null && StopLossTicket.Status == OrderStatus.Filled) { // null these out to signal that we're done trading for the day OpeningBarRange = null; StopLossTicket = null; return; } // now that we have our opening bar, test to see if we're already in a position if (!Security.Invested) { ScanForEntrance(); } else { // if we haven't exited yet then manage our stop loss, this controls our exit point if (Security.Invested) { ManageStopLoss(); } else if (StopLossTicket != null && StopLossTicket.Status.IsOpen()) { StopLossTicket.Cancel(); } } } /// <summary> /// Scans for a breakout from the opening range bar /// </summary> private void ScanForEntrance() { // scan for entrances, we only want to do this before 10am if (Time.TimeOfDay.Hours >= 10) return; // expect capture 10% of the daily range var expectedCaptureRange = 0.1m*ATR14; var allowedDollarLoss = MaximumPorfolioRiskPercentPerPosition * Portfolio.TotalPortfolioValue; var shares = (int) (allowedDollarLoss/expectedCaptureRange); // determine a position size based on an acceptable loss in proporton to our total portfolio value //var shares = (int) (MaximumLeverage*MaximumPorfolioRiskPercentPerPosition*Portfolio.TotalPortfolioValue/(0.4m*ATR14)); // max out at a little below our stated max, prevents margin calls and such var maxShare = (int) CalculateOrderQuantity(symbol, MaximumLeverage); shares = Math.Min(shares, maxShare); // min out at 1x leverage //var minShare = CalculateOrderQuantity(symbol, MaximumLeverage/2m); //shares = Math.Max(shares, minShare); // we're looking for a breakout of the opening range bar in the direction of the medium term trend if (ShouldEnterLong) { // breakout to the upside, go long (fills synchronously) MarketTicket = MarketOrder(symbol, shares); Log("Enter long @ " + MarketTicket.AverageFillPrice.SmartRounding() + " Shares: " + shares); Plot(symbol, "Enter", MarketTicket.AverageFillPrice); // we'll start with a global, non-trailing stop loss EnablePsarTrailingStop = false; // submit stop loss order for max loss on the trade var stopPrice = Security.Low*(1 - GlobalStopLossPercent); StopLossTicket = StopMarketOrder(symbol, -shares, stopPrice); if (EnableOrderUpdateLogging) { Log("Submitted stop loss @ " + stopPrice.SmartRounding()); } } else if (ShouldEnterShort) { // breakout to the downside, go short MarketTicket = MarketOrder(symbol, - -shares); Log("Enter short @ " + MarketTicket.AverageFillPrice.SmartRounding()); Plot(symbol, "Enter", MarketTicket.AverageFillPrice); // we'll start with a global, non-trailing stop loss EnablePsarTrailingStop = false; // submit stop loss order for max loss on the trade var stopPrice = Security.High*(1 + GlobalStopLossPercent); StopLossTicket = StopMarketOrder(symbol, -shares, stopPrice); if (EnableOrderUpdateLogging) { Log("Submitted stop loss @ " + stopPrice.SmartRounding() + " Shares: " + shares); } } } /// <summary> /// Manages our stop loss ticket /// </summary> private void ManageStopLoss() { // if we've already exited then no need to do more if (StopLossTicket == null || StopLossTicket.Status == OrderStatus.Filled) return; // only do this once per minute //if (Time.RoundDown(TimeSpan.FromMinutes(1)) != Time) return; // get the current stop price var stopPrice = StopLossTicket.Get(OrderField.StopPrice); // check for enabling the psar trailing stop if (ShouldEnablePsarTrailingStop(stopPrice)) { EnablePsarTrailingStop = true; Log("Enabled PSAR trailing stop @ ProfitPercent: " + Security.Holdings.UnrealizedProfitPercent.SmartRounding()); } // we've trigger the psar trailing stop, so start updating our stop loss tick if (EnablePsarTrailingStop && PSARMin.IsReady) { StopLossTicket.Update(new UpdateOrderFields {StopPrice = PSARMin}); Log("Submitted stop loss @ " + PSARMin.Current.Value.SmartRounding()); } } /// <summary> /// This event handler is fired for each and every order event the algorithm /// receives. We'll perform some logging and house keeping here /// </summary> public override void OnOrderEvent(OrderEvent orderEvent) { // print debug messages for all order events if (LiveMode || orderEvent.Status.IsFill() || EnableOrderUpdateLogging) { LiveDebug("Filled: " + orderEvent.FillQuantity + " Price: " + orderEvent.FillPrice); } // if this is a fill and we now don't own any stock, that means we've closed for the day if (!Security.Invested && orderEvent.Status == OrderStatus.Filled) { // reset values for tomorrow LastExitTime = Time; var ticket = Transactions.GetOrderTickets(x => x.OrderId == orderEvent.OrderId).Single(); Plot(symbol, "Exit", ticket.AverageFillPrice); } } /// <summary> /// If we're still invested by the end of the day, liquidate /// </summary> public override void OnEndOfDay() { if (Security.Invested) { Liquidate(); } } /// <summary> /// Determines whether or not we should plot. This is used /// to provide enough plot points but not too many, we don't /// need to plot every second in backtests to get an idea of /// how good or bad our algorithm is performing /// </summary> public bool ShouldPlot { get { // always in live if (LiveMode) return true; // set in top to override plotting during long backtests if (!EnablePlotting) return false; // every 30 seconds in backtest if (Time.RoundDown(TimeSpan.FromSeconds(PricePlotFrequencyInSeconds)) != Time) return false; // always if we're invested if (Security.Invested) return true; // always if it's before noon if (Time.TimeOfDay.Hours < 10.25) return true; // for an hour after our exit if (Time - LastExitTime < TimeSpan.FromMinutes(30)) return true; return false; } } /// <summary> /// In live mode it's nice to push messages to the debug window /// as well as the log, this allows easy real time inspection of /// how the algorithm is performing /// </summary> public void LiveDebug(object msg) { if (msg == null) return; if (LiveMode) { Debug(msg.ToString()); Log(msg.ToString()); } else { Log(msg.ToString()); } } /// <summary> /// Determines whether or not we should end a long position /// </summary> private bool ShouldEnterLong { // check to go in the same direction of longer term trend and opening break out get { return IsUptrend && HasEnoughRecentVolatility && Security.Close > OpeningBarRange.High; } } /// <summary> /// Determines whether or not we're currently in a medium term up trend /// </summary> private bool IsUptrend { get { return ADX14 > 20 && ADX14.PositiveDirectionalIndex > ADX14.NegativeDirectionalIndex; } } /// <summary> /// Determines whether or not we should enter a short position /// </summary> private bool ShouldEnterShort { // check to go in the same direction of longer term trend and opening break out get { return IsDowntrend && HasEnoughRecentVolatility && Security.Close < OpeningBarRange.Low; } } /// <summary> /// Determines whether or not we're currently in a medium term down trend /// </summary> private bool IsDowntrend { get { return ADX14 > 20 && ADX14.NegativeDirectionalIndex > ADX14.PositiveDirectionalIndex; } } /// <summary> /// Determines whether or not there's been enough recent volatility for /// this strategy to work /// </summary> private bool HasEnoughRecentVolatility { get { return SmoothedATR14 > Security.Close*AtrVolatilityThresholdPercent || SmoothedSTD14 > Security.Close*StdVolatilityThresholdPercent; } } /// <summary> /// Determines whether or not we should enable the psar trailing stop /// </summary> /// <param name="stopPrice">current stop price of our stop loss tick</param> private bool ShouldEnablePsarTrailingStop(decimal stopPrice) { // no need to enable if it's already enabled return !EnablePsarTrailingStop // once we're up a certain percentage, we'll use PSAR to control our stop && Security.Holdings.UnrealizedProfitPercent > PercentProfitStartPsarTrailingStop // make sure the PSAR is on the right side && PsarIsOnRightSideOfPrice // make sure the PSAR is more profitable than our global loss && IsPsarMoreProfitableThanStop(stopPrice); } /// <summary> /// Determines whether or not the PSAR is on the right side of price depending on our long/short /// </summary> private bool PsarIsOnRightSideOfPrice { get { return (Security.Holdings.IsLong && PSARMin < Security.Close) || (Security.Holdings.IsShort && PSARMin > Security.Close); } } /// <summary> /// Determines whether or not the PSAR stop price is better than the specified stop price /// </summary> private bool IsPsarMoreProfitableThanStop(decimal stopPrice) { return (Security.Holdings.IsLong && PSARMin > stopPrice) || (Security.Holdings.IsShort && PSARMin < stopPrice); } } }
Â
Shile Wen
Hi SherpaTrader,
The problem stems from Security = Securities[symbol]; as we are trying to access the Security object for a security we are not subscribed to. Furthermore, we need to move everything in Initialize that is below AddUniverse(CoarseSelectionFunction); into OnSecuritiesChanged, and use the full name indicators (e.g. SimpleMovingAverage instead of SMA) and manually update them.
Best,
Shile Wen
Michael Handschuh
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!