| Overall Statistics |
|
Total Orders 87169 Average Win 0.11% Average Loss -0.02% Compounding Annual Return 12.289% Drawdown 8.700% Expectancy 0.152 Start Equity 10000000 End Equity 28243663.32 Net Profit 182.437% Sharpe Ratio 1.429 Sortino Ratio 3.026 Probabilistic Sharpe Ratio 99.988% Loss Rate 83% Win Rate 17% Profit-Loss Ratio 5.88 Alpha 0.06 Beta 0.008 Annual Standard Deviation 0.042 Annual Variance 0.002 Information Ratio -0.183 Tracking Error 0.153 Treynor Ratio 7.57 Total Fees $5726659.64 Estimated Strategy Capacity $2000000.00 Lowest Capacity Asset SRV R735QTJ8XC9X Portfolio Turnover 109.83% |
#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
{
public int MaxPositions = 20;
private Universe _universe;
[Parameter("universeSize")]
private int _universeSize = 1000;
private decimal _atrThreshold = 0.5m;
private int _indicatorPeriod = 14; // days
[Parameter("openingRangeMinutes")]
private int _openingRangeMinutes = 5;
private int _leverage = 4;
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;
// 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;
UniverseSettings.Asynchronous = true;
UniverseSettings.Schedule.On(DateRules.MonthStart(spy));
_universe = AddUniverse(fundamentals => fundamentals
.Where(f => f.Price > 5 && f.Symbol != spy)
.OrderByDescending(f => f.DollarVolume)
.Take(_universeSize)
.Select(f => f.Symbol)
.ToList()
);
Schedule.On(DateRules.EveryDay(spy), TimeRules.BeforeMarketClose(spy, 1), () => Liquidate()); // Close all the open positions and cancel standing orders.
SetWarmUp(TimeSpan.FromDays(2 * _indicatorPeriod));
}
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)
{
if (IsWarmingUp || !(Time.Hour == 9 && Time.Minute == 30 + _openingRangeMinutes)) return;
// Select the stocks in play.
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);
// Look for trade entries.
foreach (var symbolData in filtered)
{
symbolData.Scan();
}
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status != OrderStatus.Filled) return;
_symbolDataBySymbol[orderEvent.Symbol].OnOrderEvent(orderEvent.Ticket);
}
}
class SymbolData
{
public decimal? RelativeVolume;
public TradeBar OpeningBar = new();
private OpeningRangeBreakoutUniverseAlgorithm _algorithm;
private Security _security;
private decimal _stopLossAtrDistance = 0.1m; // 0.1 => 10% of ATR
private decimal _stopLossRiskSize = 0.01m; // 0.01 => Lose 1% of the portfolio if stop loss is hit
private IDataConsolidator Consolidator;
public AverageTrueRange ATR;
private SimpleMovingAverage VolumeSMA;
private decimal StopLossPrice;
private OrderTicket EntryTicket, StopLossTicket;
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 - _stopLossAtrDistance * ATR);
}
else if (OpeningBar.Close < OpeningBar.Open)
{
PlaceTrade(OpeningBar.Low, OpeningBar.Low + _stopLossAtrDistance * ATR);
}
}
public void PlaceTrade(decimal entryPrice, decimal stopPrice)
{
var quantity = (int)((_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)
{
StopLossPrice = stopPrice;
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, -EntryTicket.Quantity, StopLossPrice, tag: "ATR Stop");
}
}
}
}