Overall Statistics |
Total Orders 4339 Average Win 0.87% Average Loss -0.89% Compounding Annual Return 2.689% Drawdown 41.700% Expectancy 0.030 Start Equity 20000 End Equity 29755.96 Net Profit 48.780% Sharpe Ratio 0.078 Sortino Ratio 0.091 Probabilistic Sharpe Ratio 0.011% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 0.99 Alpha -0.028 Beta 0.433 Annual Standard Deviation 0.131 Annual Variance 0.017 Information Ratio -0.558 Tracking Error 0.141 Treynor Ratio 0.024 Total Fees $7759.34 Estimated Strategy Capacity $40000000.00 Lowest Capacity Asset EEM SNQLASP67O85 Portfolio Turnover 79.02% |
# region imports from AlgorithmImports import * # endregion PRINT_YTD = False PRINT_TRADES = True PRINT_BENCHMARK = True
#region imports from AlgorithmImports import * from System.Drawing import Color from pandas.core.frame import DataFrame from enum import Enum import charting_setup from vix_filter import VixFilterType, subscribe_assets, get_VIX_rank, move_to_safe_asset #endregion class BBZone(Enum): NONE = 0 ZONE_1 = 1 ZONE_2 = 2 ZONE_3 = 3 ZONE_4 = 4 class DirectionAsset(Enum): MARKET = 1 VIX = 2 class BollingerBands(QCAlgorithm): # Directional Filter _direction_asset: DirectionAsset = DirectionAsset.VIX # VIX Filter _vix_filter_type: VixFilterType = VixFilterType.VIX_RATIO _vix_filter_value_threshold: float = 1. _safe_asset_ticker: str = 'IEF' def initialize(self): self.set_start_date(2010, 1, 1) self._init_cash: int = 20_000 self._start_of_year_portfolio_value = self._init_cash self.set_cash('USD', self._init_cash) tickers: List[str] = [ # symbol 1, symbol 2, symbol 3, symbol 4 # 'SPY', 'SPY', 'SPY', 'SPY', 'SPY', 'IEF', 'EEM', 'FXC', # symbol 5, symbol 6, symbol 7, symbol 8 # 'SPY', 'SPY', 'SPY', 'SPY', 'BIL', 'IWM', 'QQQ', 'UUP' ] self._symbols: List[Symbol] = [ self.add_equity(t, Resolution.DAILY).symbol for t in tickers ] self._traded_symbol_by_zone: Dict[Dict[int, Symbol]] = { BBZone.ZONE_1 : { -1: self._symbols[0], 1: self._symbols[4] }, BBZone.ZONE_2 : { -1: self._symbols[1], 1: self._symbols[5] }, BBZone.ZONE_3 : { -1: self._symbols[2], 1: self._symbols[6] }, BBZone.ZONE_4 : { -1: self._symbols[3], 1: self._symbols[7] }, } period: int = 2 self._iv: Symbol = self.add_data(CBOE, 'VIX', Resolution.DAILY).symbol self._market: Symbol = self.add_equity('SPY', Resolution.DAILY).symbol self._safe_asset: Symbol = self.add_equity(self._safe_asset_ticker, Resolution.DAILY).symbol self._signal_assets: List[Symbol] = subscribe_assets(self, self._vix_filter_type) # bollinger bands setup bb_period: int = 20 bb_std: int = 2 self._bb: BollingerBands = self.BB(self._iv, bb_period, bb_std) self._historical_prices: Dict[Symbol, RollingWindow] = { self._iv : RollingWindow[float](period), self._market : RollingWindow[float](period), } # benchmark self._benchmark_values: List[float] = [] # charting # BB chart self._bb_chart_name: str = "Bollinger Bands Signals" stock_plot: Chart = Chart(self._bb_chart_name) # NOTE there's a limited number of chart series we can print if charting_setup.PRINT_TRADES: # [:4] is temporary workaround to display only trades for the first 4 original symbols for symbol in list(set(self._symbols))[:4]: stock_plot.add_series(Series(f'Buy {symbol.value}', SeriesType.SCATTER, '', Color.GREEN, ScatterMarkerSymbol.TRIANGLE)) stock_plot.add_series(Series(f'Sell {symbol.value}', SeriesType.SCATTER, '', Color.RED, ScatterMarkerSymbol.TRIANGLE_DOWN)) stock_plot.add_series(Series(f'Liquidate {symbol.value}', SeriesType.SCATTER, '', Color.BLUE, ScatterMarkerSymbol.DIAMOND)) self.add_chart(stock_plot) # YTD chart if charting_setup.PRINT_YTD: self._recent_year: int = -1 self._ytd_chart_name: str = "YTD Performance [%]" self._ytd_chart: Chart = Chart(self._ytd_chart_name) self._ytd_chart.add_series(Series("YTD", SeriesType.LINE, '')) self.add_chart(self._ytd_chart) self.set_warm_up(bb_period, Resolution.DAILY) self.settings.minimum_order_margin_portfolio_percentage = 0. self.settings.daily_precise_end_time = False def on_securities_changed(self, changes: SecurityChanges) -> None: for security in changes.added_securities: security.set_leverage(3) def on_end_of_day(self) -> None: # print benchmark in main equity plot if charting_setup.PRINT_BENCHMARK: mkt_price_df: DataFrame = self.History(self._market, 2, Resolution.DAILY) if not mkt_price_df.empty: benchmark_price: float = mkt_price_df['close'].unstack(level= 0).iloc[-1] if len(self._benchmark_values) == 2: self._benchmark_values[-1] = benchmark_price benchmark_perf: float = self._init_cash * (self._benchmark_values[-1] / self._benchmark_values[0]) self.plot('Strategy Equity', self._market, benchmark_perf) else: self._benchmark_values.append(benchmark_price) # print YTD performance if charting_setup.PRINT_YTD: if self.time.year != self._recent_year: self._recent_year = self.time.year self._start_of_year_portfolio_value = self.Portfolio.TotalPortfolioValue # calculate the YTD performance ytd_performance: float = ((self.Portfolio.TotalPortfolioValue / self._start_of_year_portfolio_value) - 1) * 100 self.Plot(self._ytd_chart_name, "YTD [%]", ytd_performance) def on_data(self, slice: Slice) -> None: if all(slice.contains_key(s) and slice[s] for s in list(self._historical_prices.keys())) and \ all(slice.contains_key(s) and slice[s] for s in self._signal_assets) and \ all(slice.contains_key(s) and slice[s] for s in self._symbols): # store prices for s in list(self._historical_prices.keys()): self._historical_prices[s].add(slice[s].value) if self.is_warming_up: return if not self._bb.is_ready: return iv_price: float = slice[self._iv].value market_price: float = slice[self._market].value if charting_setup.PRINT_TRADES: self.plot(self._bb_chart_name, "Market Price", market_price) self.plot(self._bb_chart_name, "IV Price", iv_price) self.plot(self._bb_chart_name, "MiddleBand", self._bb.middle_band.current.value) self.plot(self._bb_chart_name, "UpperBand", self._bb.upper_band.current.value) self.plot(self._bb_chart_name, "LowerBand", self._bb.lower_band.current.value) if move_to_safe_asset( self, self._vix_filter_type, self._vix_filter_value_threshold, self._signal_assets ): symbol_to_invest: Symbol|None = self._safe_asset else: bb_zone: BBZone = self.identify_bb_zone(iv_price, self._bb) if self._direction_asset == DirectionAsset.VIX: direction: int = 1 if self._historical_prices[self._iv][0] > self._historical_prices[self._iv][1] else -1 elif self._direction_asset == DirectionAsset.MARKET: direction: int = 1 if self._historical_prices[self._market][0] > self._historical_prices[self._market][1] else -1 symbol_to_invest: Symbol|None = self._traded_symbol_by_zone[bb_zone][direction] if direction in self._traded_symbol_by_zone[bb_zone] else None # rabalance held_symbols: List[Symbol] = [ x.key for x in self.portfolio if self.portfolio[x.key].invested ] if symbol_to_invest is None: # hold current holdings pass # for symbol in held_symbols: # self.trade_and_markup_chart(symbol, self._bb_chart_name, 0., slice[self._iv].value) else: if symbol_to_invest is not None and symbol_to_invest not in held_symbols: for s in held_symbols: self.trade_and_markup_chart(s, self._bb_chart_name, 0., iv_price) self.trade_and_markup_chart(symbol_to_invest, self._bb_chart_name, 1., iv_price) def trade_and_markup_chart( self, symbol: Symbol, chart_name: str, weight: float, chart_value: float) -> None: self.set_holdings(symbol, weight) if charting_setup.PRINT_TRADES: self.plot( chart_name, f"Buy {symbol.value}" if weight == 1. else (f"Sell {symbol.value}" if weight == -1. else (f"Liquidate {symbol.value}" if weight == 0. else f"Rebalance {symbol.value}")), chart_value ) def identify_bb_zone(self, price: float, bb: BollingerBands) -> BBZone: if price >= bb.upper_band.current.value: return BBZone.ZONE_1 elif price < bb.upper_band.current.value and price >= bb.middle_band.current.value: return BBZone.ZONE_2 elif price < bb.middle_band.current.value and price >= bb.lower_band.current.value: return BBZone.ZONE_3 elif price < bb.lower_band.current.value: return BBZone.ZONE_4
# region imports from AlgorithmImports import * from enum import Enum # endregion class VixFilterType(Enum): VVIX = 1 VIX_RANK = 2 VIX_RATIO = 3 def subscribe_assets( algo: QCAlgorithm, vix_filter_type: VixFilterType ) -> List[Symbol]: if vix_filter_type == VixFilterType.VVIX: iv: Symbol = algo.add_data(CBOE, "VVIX", Resolution.DAILY).symbol signal_assets = [iv] elif vix_filter_type == VixFilterType.VIX_RANK: iv: Symbol = algo.add_data(CBOE, "VIX", Resolution.DAILY).symbol signal_assets = [iv] elif vix_filter_type == VixFilterType.VIX_RATIO: iv: Symbol = algo.add_data(CBOE, "VIX", Resolution.DAILY).symbol iv_3m: Symbol = algo.add_data(CBOE, "VIX3M", Resolution.DAILY).symbol signal_assets = [iv, iv_3m] return signal_assets def move_to_safe_asset( algo: QCAlgorithm, vix_filter_type: VixFilterType, vix_filter_value_threshold: float, signal_assets: List[Symbol] ) -> bool: result: bool = False if vix_filter_type == VixFilterType.VVIX: vvix: float = algo.securities[signal_assets[0]].price if vvix > vix_filter_value_threshold: result = True elif vix_filter_type == VixFilterType.VIX_RANK: vix_rank: float = get_VIX_rank(algo, signal_assets[0]) if vix_rank > vix_filter_value_threshold: result = True elif vix_filter_type == VixFilterType.VIX_RATIO: vix_ratio: float = algo.securities[signal_assets[0]].price / algo.securities[signal_assets[1]].price if vix_ratio > vix_filter_value_threshold: result = True return result def get_VIX_rank( algo: QCAlgorithm, vix: Symbol, lookback: int = 150 ) -> float: history: DataFrame = algo.history( CBOE, vix, lookback, Resolution.DAILY ) rank: float = ((algo.securities[vix].price - min(history["low"])) / (max(history["high"]) - min(history["low"]))) return rank