Overall Statistics |
Total Orders 9689 Average Win 0.11% Average Loss -0.02% Compounding Annual Return 19.001% Drawdown 2.200% Expectancy 0.253 Start Equity 10000000 End Equity 11900110.13 Net Profit 19.001% Sharpe Ratio 2.618 Sortino Ratio 7.163 Probabilistic Sharpe Ratio 99.380% Loss Rate 83% Win Rate 17% Profit-Loss Ratio 6.18 Alpha 0.122 Beta -0.044 Annual Standard Deviation 0.045 Annual Variance 0.002 Information Ratio 0.335 Tracking Error 0.123 Treynor Ratio -2.709 Total Fees $495804.01 Estimated Strategy Capacity $1700000.00 Lowest Capacity Asset SIVB R735QTJ8XC9X Portfolio Turnover 108.88% |
from AlgorithmImports import * class OpeningRangeBreakoutUniverseAlgorithm(QCAlgorithm): def initialize(self): self.set_start_date(2016, 1, 1) self.set_end_date(2017, 1, 1) self.set_cash(10_000_000) # Parameters self.max_positions = 20 self.risk = 0.01 # equity risk per position self.entry_gap = 0.1 # fraction of ATR from close price for entry order stop level self._universe_size = self.get_parameter("universeSize", 1000) self._atr_threshold = 0.5 self._indicator_period = 14 # days self._opening_range_minutes = self.get_parameter("openingRangeMinutes", 5) self._leverage = 4 self._symbol_data_by_symbol = {} # Add SPY as a benchmark and stepping asset self._spy = self.add_equity("SPY").symbol # Add a universe of the most liquid US equities self.universe_settings.leverage = self._leverage # self.universe_settings.asynchronous = True # won't work correct since self.time will be wrong self.last_month = None self._universe = self.add_universe(self.filter_universe) # Schedule daily liquidation before market close 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)) def filter_universe(self, fundamentals): # only update universe on first day of month if self.time.month == self.last_month: return Universe.UNCHANGED self.last_month = self.time.month return [ f.symbol for f in sorted( [f for f in fundamentals if f.price > 5 and f.symbol != self._spy], key=lambda f: f.dollar_volume, reverse=True )[:self._universe_size] ] def on_securities_changed(self, changes): # Add indicators for each asset that enters the universe for security in changes.added_securities: self._symbol_data_by_symbol[security.symbol] = SymbolData(self, security, self._opening_range_minutes, self._indicator_period) def on_data(self, slice): if self.is_warming_up or not (self.time.hour == 9 and self.time.minute == 30 + self._opening_range_minutes): return # Select stocks in play filtered = sorted( [self._symbol_data_by_symbol[s] for s in self.active_securities.keys if self.active_securities[s].price > 0 and s in self._universe.selected # not sure about last condition, if it is needed? and self._symbol_data_by_symbol[s].relative_volume > 1 and self._symbol_data_by_symbol[s].ATR.current.value > self._atr_threshold], key=lambda x: x.relative_volume, reverse=True )[:self.max_positions] # Look for trade entries for symbolData in filtered: symbolData.scan() def on_order_event(self, orderEvent): if orderEvent.status != OrderStatus.FILLED: return if orderEvent.symbol in self._symbol_data_by_symbol: self._symbol_data_by_symbol[orderEvent.symbol].on_order_event(orderEvent) class SymbolData: def __init__(self, algorithm: QCAlgorithm, security, openingRangeMinutes, indicatorPeriod): self.algorithm = algorithm self.security = security self.opening_bar = None self.relative_volume = 0 self.ATR = algorithm.ATR(security.symbol, indicatorPeriod, resolution=Resolution.DAILY) self.volumeSMA = SimpleMovingAverage(indicatorPeriod) self.stop_loss_price = None self.entry_ticket = None self.stop_loss_ticket = None self.consolidator = algorithm.consolidate( security.symbol, TimeSpan.from_minutes(openingRangeMinutes), self.consolidation_handler ) def consolidation_handler(self, bar): 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 scan(self): 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.entry_gap * 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.entry_gap * self.ATR.current.value) def place_trade(self, entryPrice, stopPrice): risk_per_position = (self.algorithm.portfolio.total_portfolio_value * self.algorithm.risk) / self.algorithm.max_positions quantity = int(risk_per_position / (entryPrice - stopPrice)) # Limit quantity to what is allowed by portfolio allocation quantity_limit = self.algorithm.calculate_order_quantity(self.security.symbol, 1 / self.algorithm.max_positions) if quantity > 0: sign = 1 elif quantity < 0: sign = -1 else: sign = 0 quantity = int(min(abs(quantity), quantity_limit) * sign) if quantity != 0: self.stop_loss_price = stopPrice self.entry_ticket = self.algorithm.stop_market_order(self.security.symbol, quantity, entryPrice, "Entry") def on_order_event(self, orderEvent): 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" )