Overall Statistics |
Total Trades 12 Average Win 0.38% Average Loss -0.17% Compounding Annual Return -12.526% Drawdown 0.900% Expectancy -0.455 Net Profit -0.463% Sharpe Ratio -3.556 Loss Rate 83% Win Rate 17% Profit-Loss Ratio 2.27 Alpha -0.098 Beta -0.516 Annual Standard Deviation 0.03 Annual Variance 0.001 Information Ratio -4.06 Tracking Error 0.03 Treynor Ratio 0.205 Total Fees $12.00 |
/* * 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.Linq; using QuantConnect.Brokerages; using QuantConnect.Data; using QuantConnect.Data.Consolidators; using QuantConnect.Data.Market; 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> public class OpeningBreakoutAlgorithm : QCAlgorithm { // the equity symbol we're trading private const 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 bool UseRecentVolatilityRequirement = true; private const decimal MaximumLeverage = 4; private const decimal PercentProfitStartPsarTrailingStop = 0.0005m; // @100k order size this is 50 bucks private const decimal MaximumPorfolioRiskPercentPerPosition = .0025m; // entrance criteria private const int OpeningSpanInMinutes = 3; private const decimal BreakoutThresholdPercent = 0.00005m; private const decimal AtrVolatilityThresholdPercent = 0.002m; private const decimal StdVolatilityThresholdPercent = 0.0025m; // this is the security we're trading public Security Security; // define our indicators used for trading decisions public HullMovingAverage HMA; public AverageTrueRange ATR14; public StandardDeviation STD14; 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(2018, 4, 1); SetEndDate(2018, 4, 14); SetCash(100000); // leverage tradier $1 traders SetBrokerageModel(BrokerageName.TradierBrokerage); // request high resolution equity data 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 a hull for trend detection HMA = new HullMovingAverage(Symbol + "_HMA14", 4); var hmaDaily = new TradeBarConsolidator(TimeSpan.FromMinutes(30)); RegisterIndicator(Symbol, HMA, hmaDaily, Field.Close); // define our longer term indicators STD14 = STD(Symbol, 14, Resolution.Daily); ATR14 = ATR(Symbol, 14, resolution: Resolution.Daily); PSARMin = new ParabolicStopAndReverse(Symbol, afStart: 0, afIncrement: 0.000025m); // 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(HMA.Name)); chart.AddSeries(new Series("Enter", SeriesType.Scatter)); chart.AddSeries(new Series("Exit", SeriesType.Scatter)); chart.AddSeries(new Series(PSARMin.Name, SeriesType.Scatter)); AddChart(chart); var history = History(Symbol, 20, Resolution.Daily); foreach (var bar in history) { hmaDaily.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()); } /// <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(Symbol, HMA); PlotIndicator("ATR", ATR14); PlotIndicator("STD", STD14); PlotIndicator("ATR", 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 && Time.RoundDown(TimeSpan.FromMinutes(1)) == Time) { 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 positio 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 = (decimal) (allowedDollarLoss/expectedCaptureRange); // max out at a little below our stated max, prevents margin calls and such var maxShare = CalculateOrderQuantity(Symbol, .75m*MaximumLeverage); shares = Math.Min(shares, maxShare); // the stop percentage defined by dollars loss var stopLossPercentage = allowedDollarLoss/(shares*Security.Close); // 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 - stopLossPercentage); StopLossTicket = StopMarketOrder(Symbol, -shares, stopPrice); 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 + stopLossPercentage); StopLossTicket = StopMarketOrder(Symbol, -shares, stopPrice); 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; if (EnableOrderUpdateLogging) { 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 }); if (EnableOrderUpdateLogging) { 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() { 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 Security.Close > HMA; } } /// <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 Security.Close < HMA; } } /// <summary> /// Determines whether or not there's been enough recent volatility for /// this strategy to work /// </summary> private bool HasEnoughRecentVolatility { get { return !UseRecentVolatilityRequirement || 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); } } public class HullMovingAverage : IndicatorBase<IndicatorDataPoint> { private readonly LinearWeightedMovingAverage _fast; private readonly LinearWeightedMovingAverage _slow; private readonly LinearWeightedMovingAverage _smooth; public HullMovingAverage(string name, int period) : base(name) { var nsquared = period*period; _fast = new LinearWeightedMovingAverage(nsquared/2); _slow = new LinearWeightedMovingAverage(nsquared); _smooth = new LinearWeightedMovingAverage(period); } public override bool IsReady { get { return _smooth.IsReady; } } protected override decimal ComputeNextValue(IndicatorDataPoint input) { _fast.Update(input); _slow.Update(input); _smooth.Update(input.Time, 2*_fast - _slow); return _smooth; } } }