Overall Statistics
Total Orders
2679
Average Win
0.91%
Average Loss
-0.47%
Compounding Annual Return
20.907%
Drawdown
14.500%
Expectancy
0.192
Start Equity
100000
End Equity
312836.54
Net Profit
212.837%
Sharpe Ratio
1.015
Sortino Ratio
1.306
Probabilistic Sharpe Ratio
70.750%
Loss Rate
60%
Win Rate
40%
Profit-Loss Ratio
1.94
Alpha
0.123
Beta
-0.043
Annual Standard Deviation
0.118
Annual Variance
0.014
Information Ratio
0.197
Tracking Error
0.215
Treynor Ratio
-2.755
Total Fees
$13314.81
Estimated Strategy Capacity
$75000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
300.03%
# region imports
from AlgorithmImports import *
# endregion


class NoiseAreaBreakoutAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2019, 5, 1)
        self.set_end_date(2025, 5, 1)
        self.set_cash(100_000)
        # Set some parameters.
        self._trading_interval_length = timedelta(minutes=30)
        self._lookback = 14  # days
        self._target_volatility = 0.02 # 0.02 = 2%
        self._max_exposure = 4  # 4 = 400%
        # Add SPY to trade.
        self._equity = self.add_equity('SPY', leverage=self._max_exposure)
        self._equity.vwap = self.vwap(self._equity.symbol)
        self._equity.daily_volatility = IndicatorExtensions.of(StandardDeviation(self._lookback), self.roc(self._equity.symbol, 1, Resolution.DAILY))
        self._equity.avg_move_by_interval = {}
        self._equity.yesterdays_close = None
        self._equity.todays_open = None
        # Add Scheduled Events to update members and place trades.
        date_rule = self.date_rules.every_day(self._equity.symbol)
        self.schedule.on(date_rule, self.time_rules.midnight, lambda: setattr(self._equity, 'yesterdays_close', self._equity.price))
        self.schedule.on(date_rule, self.time_rules.after_market_open(self._equity.symbol, 1), lambda: setattr(self._equity, 'todays_open', self._equity.open))
        self.schedule.on(date_rule, self.time_rules.every(self._trading_interval_length), self._rebalance)
        self.schedule.on(date_rule, self.time_rules.before_market_close(self._equity.symbol, 1), self.liquidate)
        # Set a warm-up period to warm-up the indicators.
        self.set_warm_up(timedelta(30))

    def _rebalance(self):
        # Wait until the market is open.
        t = self.time
        if (not self._equity.yesterdays_close or
            not self._equity.exchange.hours.is_open(t, False) or
            not self._equity.exchange.hours.is_open(t - self._trading_interval_length, False)):
            return
        # Create an indicator for this time interval if it doesn't already exist.
        trading_interval = (t.hour, t.minute)
        if trading_interval not in self._equity.avg_move_by_interval:
            self._equity.avg_move_by_interval[trading_interval] = SimpleMovingAverage(self._lookback)
        avg_move = self._equity.avg_move_by_interval[trading_interval]
        # Update the average move indicator.
        move = abs(self._equity.price / self._equity.todays_open - 1)
        if not avg_move.update(t, move):
            return
        # Wait until the daily volatility indicator is ready.
        if not self._equity.daily_volatility.is_ready or self.is_warming_up:
            return
        # Calculate the noise area.
        upper_bound = max(self._equity.yesterdays_close, self._equity.todays_open) * (1+avg_move.current.value)
        lower_bound = min(self._equity.yesterdays_close, self._equity.todays_open) * (1-avg_move.current.value)
        # Scan for entries.
        weight = min(self._max_exposure, self._target_volatility/self._equity.daily_volatility.current.value)
        if not self._equity.holdings.is_long and self._equity.price > upper_bound:
            self.set_holdings(self._equity.symbol, weight)
        elif not self._equity.holdings.is_short and self._equity.price < lower_bound:
            self.set_holdings(self._equity.symbol, -weight)
        # Scan for exits.
        elif (self._equity.holdings.is_long and self._equity.price < max(upper_bound, self._equity.vwap.current.value) or
            self._equity.holdings.is_short and self._equity.price > min(lower_bound, self._equity.vwap.current.value)):
            self.liquidate()
        # Plot the current state.
        self.plot('Weight', 'value', weight)
        self.plot('Noise Area', 'Upper Bound', upper_bound)
        self.plot('Noise Area', 'Lower Bound', lower_bound)
        self.plot('Noise Area', 'Price', self._equity.price)
        self.plot('Noise Area', 'VWAP', self._equity.vwap.current.value)
        self.plot('Volatility', 'SPY', self._equity.daily_volatility.current.value)