Overall Statistics
Total Orders
5652
Average Win
0.06%
Average Loss
-0.07%
Compounding Annual Return
-25.425%
Drawdown
25.800%
Expectancy
-0.149
Start Equity
10000000
End Equity
7459461.66
Net Profit
-25.405%
Sharpe Ratio
-2.451
Sortino Ratio
-2.343
Probabilistic Sharpe Ratio
0.000%
Loss Rate
55%
Win Rate
45%
Profit-Loss Ratio
0.91
Alpha
-0.174
Beta
-0.219
Annual Standard Deviation
0.078
Annual Variance
0.006
Information Ratio
-1.766
Tracking Error
0.152
Treynor Ratio
0.875
Total Fees
$114183.14
Estimated Strategy Capacity
$20000000.00
Lowest Capacity Asset
AGIO VIHVU20BO679
Portfolio Turnover
77.23%
# region imports
from AlgorithmImports import *
# endregion

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)
        
        # Set the universe parameters.
        self._indicator_period = 14 # days
        self._mean_volume_threshold = 1_000_000 # Shares
        self._universe_size = 20

        # Set the trading parameters.
        self._opening_range_minutes = 5

        # Add SPY so there is at least 1 asset at minute resolution to step the algorithm along.
        self._spy = self.add_equity('SPY').symbol 
        # Create the objects and Scheduled Events we need to select the universe and place trades.
        self._selection_data_by_symbol = {}
        self.universe_settings.schedule.on(self.date_rules.week_start(self._spy))
        self._universe = self.add_universe(self._get_fundamentals)
        self.schedule.on(self.date_rules.every_day(self._spy), self.time_rules.after_market_open(self._spy, self._opening_range_minutes), self._scan_for_entries)
        self.schedule.on(self.date_rules.every_day(self._spy), self.time_rules.before_market_close(self._spy, 1), self.liquidate)

    def _get_fundamentals(self, fundamentals):
        # Select 1000 largest Equities.
        fundamentals = sorted(fundamentals, key=lambda f: f.market_cap)[-1_000:]
        
        # Create and warm-up SelectionData objects for each Equity.
        self._selection_data_by_symbol = {f.symbol: SelectionData(self._indicator_period) for f in fundamentals}
        for bars in self.history[TradeBar](list(self._selection_data_by_symbol.keys()), self._indicator_period, Resolution.DAILY):
            for bar in bars.values():
                self._selection_data_by_symbol[bar.symbol].update(bar)

        # Select assets based on price, mean trading volume, and ATR ratio.
        return sorted(
            [
                symbol for symbol, selection_data in self._selection_data_by_symbol.items() 
                if (selection_data.sma.current.value > 5 and 
                    selection_data.is_ready and 
                    selection_data.mean_volume.current.value > self._mean_volume_threshold)
            ],
            key=lambda symbol: self._selection_data_by_symbol[symbol].atr.current.value / self._selection_data_by_symbol[symbol].sma.current.value
        )[-self._universe_size:]

    def on_securities_changed(self, changes):
        for security in changes.added_securities:
            security.max = self.max(security.symbol, self._opening_range_minutes)
            security.min = self.min(security.symbol, self._opening_range_minutes)
            security.roc = self.roc(security.symbol, self._opening_range_minutes)

    def _scan_for_entries(self):
        for symbol in self._universe.selected:
            security = self.securities[symbol]
            if security.roc.current.value > 0 and security.price > security.max.current.value:
                self.set_holdings(security.symbol, 1/self._universe_size)
            elif security.roc.current.value < 0 and security.price < security.max.current.value:
                self.set_holdings(security.symbol, -1/self._universe_size)


class SelectionData:

    def __init__(self, period):
        self.mean_volume = SimpleMovingAverage(period)
        self.atr = AverageTrueRange(period)
        self.sma = SimpleMovingAverage(period)

    def update(self, bar):
        self.mean_volume.update(bar.end_time, bar.volume)
        self.atr.update(bar)
        self.sma.update(bar.end_time, bar.volume)

    @property
    def is_ready(self):
        return self.atr.is_ready and self.mean_volume.is_ready and self.sma.is_ready