552 lines
22 KiB
C#
552 lines
22 KiB
C#
/*
|
|
* 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.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>
|
|
/// <meta name="tag" content="strategy example" />
|
|
/// <meta name="tag" content="indicators" />
|
|
public class OpeningBreakoutAlgorithm : QCAlgorithm
|
|
{
|
|
#pragma warning disable 00162 // File contains unreachable code when EnableOrderUpdateLogging is false
|
|
|
|
// 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 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
|
|
private Security _security;
|
|
|
|
// define our indicators used for trading decisions
|
|
private AverageTrueRange ATR14;
|
|
private StandardDeviation STD14;
|
|
private AverageDirectionalIndex ADX14;
|
|
private ParabolicStopAndReverse PSARMin;
|
|
|
|
// smoothed values
|
|
private ExponentialMovingAverage _smoothedSTD14;
|
|
private 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(2013, 10, 07);
|
|
SetEndDate(2013, 10, 11);
|
|
//SetStartDate(2014, 01, 01);
|
|
//SetEndDate(2014, 06, 01);
|
|
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 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());
|
|
}
|
|
|
|
/// <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(Symbol symbol)
|
|
{
|
|
if (symbol == _security.Symbol && _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);
|
|
}
|
|
#pragma warning restore 00162
|
|
}
|
|
}
|