| Overall Statistics |
|
Total Orders 71179 Average Win 0.11% Average Loss -0.02% Compounding Annual Return 11.830% Drawdown 4.600% Expectancy 0.154 Start Equity 25000 End Equity 56762.70 Net Profit 127.051% Sharpe Ratio 1.356 Sortino Ratio 3.027 Probabilistic Sharpe Ratio 99.985% Loss Rate 84% Win Rate 16% Profit-Loss Ratio 6.00 Alpha 0.054 Beta -0.009 Annual Standard Deviation 0.04 Annual Variance 0.002 Information Ratio -0.099 Tracking Error 0.172 Treynor Ratio -5.871 Total Fees $902.15 Estimated Strategy Capacity $2400000.00 Lowest Capacity Asset ARE R735QTJ8XC9X Portfolio Turnover 106.36% |
# https://quantpedia.com/strategies/opening-range-breakout-strategy-in-individual-stocks/
#
# Into the investment universe, including all US stocks/equities listed on US exchanges (NYSE and Nasdaq).
# (The stock data are sourced from the Center for Research in Security Prices (CRSP). Intraday data for all stocks are obtained from IQFeed.)
# As for coarse selection, these must be obeyed to enter a trade:
# 1. The opening price is to be above $5.
# 2. The average trading volume over the previous 14 days must be at least 1,000,000
# daily shares.
# 3. The ATR over the previous 14 days was more than $0.50.
# 4. An additional constraint for the selected strategy variant: exclusively focus on those stocks whose Relative Volume is at least 100% and pick the top 20 stocks
# experiencing the highest Relative Volume.
# Strategy conditions: The direction of each trade (long or short) is determined by the initial movement of the opening range. A positive opening range prompted a
# stop buy order, whereas a negative one led to a stop sell order. Then, there are 2 outcomes:
# Set a stop loss at 10% of the ATR for every position opened.
# If a position is not stopped during the day, wound at the end of the trading day.
# This is an intraday trading strategy consisting of equally-weighting 20 stocks every day.
#
# QC implementation changes:
# - Investment universe consists of 500 most liquid US stocks listed on NYSE and Nasdaq.
# - Average trading volume over the previous 14 days doesn't need be at least 1,000,000.
# region imports
from AlgorithmImports import *
from typing import List, Dict
# endregion
class OpeningRangeBreakoutStrategyInIndividualStocks(QCAlgorithm):
def initialize(self):
# self.set_start_date(2016, 1, 1)
self.set_start_date(2018, 1, 1)
self.set_end_date(2025, 4, 30)
self.set_cash(25_000)
self._exchange_codes: List[str] = ['NYS', 'NAS']
# Parameters
self._stock_count: int = 20
self._risk: float = 0.01 # equity risk per position
self._SL_percentage: float = 0.1 # fraction of ATR from close price for entry order stop level
self._ATR_threshold: float = 0.5
self._indicator_period: int = 14
self.average_trading_volume_threshold: int = 1_000_000
self._bar_period: int = 5
self._min_share_price: int = 5
leverage: int = 4
self._fundamental_count: int = 500
self.fundamental_sorting_key = lambda x: x.dollar_volume
self._data: Dict[Symbol, SymbolData] = {}
self._spy: Symbol = self.add_equity("SPY").symbol
self.universe_settings.leverage = leverage
self._universe = self.add_universe(self.fundamental_selection_function)
self.schedule.on(
self.date_rules.every_day(self._spy),
self.time_rules.before_market_close(self._spy, 1),
self.liquidate
)
# Warm-up indicators
self.set_warm_up(timedelta(days=2 * self._indicator_period))
self._last_month: int = -1
def on_securities_changed(self, changes: SecurityChanges) -> None:
# Add indicators for each asset that enters the universe
for security in changes.added_securities:
security.set_fee_model(CustomFeeModel())
self._data[security.symbol] = SymbolData(
self, security,
self._bar_period,
self._indicator_period
)
def fundamental_selection_function(self, fundamental: List[Fundamental]) -> List[Symbol]:
# only update universe on first day of month
if self.time.month == self._last_month:
return Universe.UNCHANGED
self._last_month = self.time.month
selected: List[Fundamental] = [
x for x in fundamental
if x.has_fundamental_data
and x.market == 'usa'
and x.price > self._min_share_price
and x.dollar_volume != 0
and x.security_reference.exchange_id in self._exchange_codes
]
if len(selected) > self._fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self._fundamental_count]]
return list(map(lambda x: x.symbol, selected))
def on_data(self, slice: Slice) -> None:
if self.is_warming_up or not (self.time.hour == 9 and self.time.minute == 30 + self._bar_period):
return
filtered: List[SymbolData] = sorted(
[self._data[s] for s in self.active_securities.keys
if self.active_securities[s].price > 0 and s in self._universe.selected
and self._data[s]._relative_volume > 1
# and self._data[s]._volumeSMA.current.value > self.average_trading_volume_threshold
and self._data[s]._ATR.current.value > self._ATR_threshold],
key=lambda x: x._relative_volume,
reverse=True
)[:self._stock_count]
# Look for trade entries
for symbol_data in filtered:
symbol_data.trade()
def on_order_event(self, orderEvent: OrderEvent) -> None:
if orderEvent.status != OrderStatus.FILLED:
return
if orderEvent.symbol in self._data:
self._data[orderEvent.symbol].on_order_event(orderEvent)
class SymbolData:
def __init__(self, algorithm: QCAlgorithm, security: Security, opening_range_minutes: int, indicator_period: int) -> None:
self._algorithm: QCAlgorithm = algorithm
self._security: Security = security
self._opening_bar: Optional[TradeBar] = None
self._relative_volume: float = 0.
self._ATR: AverageTrueRange = algorithm.ATR(security.symbol, indicator_period, resolution=Resolution.DAILY)
self._volumeSMA: SimpleMovingAverage = SimpleMovingAverage(indicator_period)
self._stop_loss_price: Optional[float] = None
self._entry_ticket: Optional[OrderTicket] = None
self._stop_loss_ticket: Optional[OrderTicket] = None
self.consolidator = algorithm.consolidate(
security.symbol, TimeSpan.from_minutes(opening_range_minutes), self.consolidation_handler
)
def consolidation_handler(self, bar: TradeBar) -> None:
if self._opening_bar and self._opening_bar.time.date() == bar.time.date():
return
self._relative_volume = bar.volume / self._volumeSMA.current.value if self._volumeSMA.is_ready and self._volumeSMA.current.value > 0 else 0
self._volumeSMA.update(bar.end_time, bar.volume)
self._opening_bar = bar
def trade(self) -> None:
if not self._opening_bar:
return
if self._opening_bar.close > self._opening_bar.open:
self.place_trade(self._opening_bar.high, self._opening_bar.high - self._algorithm._SL_percentage * self._ATR.current.value)
elif self._opening_bar.close < self._opening_bar.open:
self.place_trade(self._opening_bar.low, self._opening_bar.low + self._algorithm._SL_percentage * self._ATR.current.value)
def place_trade(self, entry_price: float, stop_price: float) -> None:
risk_per_position: float = (self._algorithm.portfolio.total_portfolio_value * self._algorithm._risk) / self._algorithm._stock_count
quantity: int = int(risk_per_position / (entry_price - stop_price))
# Limit quantity to what is allowed by portfolio allocation
quantity_limit: int = self._algorithm.calculate_order_quantity(self._security.symbol, 1 / self._algorithm._stock_count)
quantity = int(min(abs(quantity), quantity_limit) * math.copysign(1, quantity))
# quantity = quantity_limit * sign
if quantity != 0:
self._stop_loss_price = stop_price
self._entry_ticket = self._algorithm.stop_market_order(self._security.symbol, quantity, entry_price, "Entry")
def on_order_event(self, orderEvent: OrderEvent) -> None:
if self._entry_ticket and orderEvent.order_id == self._entry_ticket.order_id:
self._stop_loss_ticket = self._algorithm.stop_market_order(
self._security.symbol, -self._entry_ticket.quantity, self._stop_loss_price, "Stop Loss"
)
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Order.AbsoluteQuantity * 0.0005
return OrderFee(CashAmount(fee, "USD"))