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