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