Overall Statistics |
Total Orders 4869 Average Win 0.09% Average Loss -0.09% Compounding Annual Return 5.329% Drawdown 29.900% Expectancy 0.057 Start Equity 10000000 End Equity 15546759.61 Net Profit 55.468% Sharpe Ratio 0.273 Sortino Ratio 0.295 Probabilistic Sharpe Ratio 1.033% Loss Rate 49% Win Rate 51% Profit-Loss Ratio 1.06 Alpha 0.008 Beta 0.316 Annual Standard Deviation 0.132 Annual Variance 0.017 Information Ratio -0.348 Tracking Error 0.151 Treynor Ratio 0.114 Total Fees $120995.15 Estimated Strategy Capacity $4500000.00 Lowest Capacity Asset MHC R735QTJ8XC9X Portfolio Turnover 1.27% |
#region imports from AlgorithmImports import * from decimal import Decimal from math import floor #endregion # https://quantpedia.com/Screener/Details/53 class SentimentAndStyleRotationAlgorithm(QCAlgorithm): def initialize(self): self.set_start_date(2010, 1, 1) self.set_end_date(2018, 7, 1) self.set_cash(10000000) self.set_security_initializer(BrokerageModelSecurityInitializer( self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))) self.add_data(CBOE, "VIX", Resolution.DAILY) self.add_data(PutCallRatio, "PutCallRatio", Resolution.DAILY) self._vix_sma_1 = SimpleMovingAverage(21) self._vix_sma_6 = SimpleMovingAverage(21*6) self._pc_ratio_sma_1 = SimpleMovingAverage(21) self._pc_ratio_sma_6 = SimpleMovingAverage(21*6) # initialize the indicator with the history request pc_ratio_history = self.history(["PutCallRatio"], 21*10, Resolution.DAILY) vix_history = self.history(["VIX"], 21*10, Resolution.DAILY) for t, value in vix_history.loc['VIX']['value'].items(): self._vix_sma_6.update(t, value) self._vix_sma_1.update(t, value) for t, value in pc_ratio_history.loc['PutCallRatio']['value'].items(): self._pc_ratio_sma_1.update(t, value) self._pc_ratio_sma_6.update(t, value) self.add_universe(self._coarse_selection_function, self._fine_selection_function) self.add_equity("SPY", Resolution.DAILY) self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.at(0, 0), self._rebalance) self._month_start = False self._selection = False self._months = -1 def _coarse_selection_function(self, coarse): if self._month_start: # drop stocks which have no fundamental data or have low price return [x.symbol for x in coarse if (x.has_fundamental_data)] else: return Universe.UNCHANGED def _fine_selection_function(self, fine): if self._month_start: self._selection = True fine = [i for i in fine if i.earning_reports.basic_average_shares.three_months>0 and i.earning_reports.basic_eps.twelve_months>0 and i.valuation_ratios.pe_ratio>0 and i.valuation_ratios.pb_ratio>0] sorted_market_cap = sorted(fine, key=lambda x: x.market_cap, reverse=True) decile_top1 = sorted_market_cap[:floor(len(sorted_market_cap)/10)] decile_top2 = sorted_market_cap[floor(len(sorted_market_cap)/10):floor(len(sorted_market_cap)*2/10)] decile_top3 = sorted_market_cap[floor(len(sorted_market_cap)*2/10):floor(len(sorted_market_cap)*3/10)] sorted_pb1 = sorted(decile_top1, key=lambda x: x.valuation_ratios.pb_ratio) sorted_pb2 = sorted(decile_top2, key=lambda x: x.valuation_ratios.pb_ratio) sorted_pb3 = sorted(decile_top3, key=lambda x: x.valuation_ratios.pb_ratio) # The value portfolio consists of all firms included in the quintile with the lowest P/B ratio pb_bottom1 = sorted_pb1[:floor(len(decile_top1)/5)] pb_bottom2 = sorted_pb2[:floor(len(decile_top2)/5)] pb_bottom3 = sorted_pb3[:floor(len(decile_top3)/5)] self._value_portfolio = [i.symbol for i in pb_bottom1 + pb_bottom2 + pb_bottom3] # The growth portfolio consists of all firms included in the quintile with the highest P/B ratio pb_top1 = sorted_pb1[-floor(len(decile_top1)/5):] pb_top2 = sorted_pb2[-floor(len(decile_top2)/5):] pb_top3 = sorted_pb3[-floor(len(decile_top3)/5):] self._growth_portfolio = [i.symbol for i in pb_top1 + pb_top2 + pb_top3] return self._value_portfolio + self._growth_portfolio else: return Universe.UNCHANGED def _rebalance(self): # rebalance every three months self._months += 1 if self._months%3 == 0: self._month_start = True def _get_tradable_assets(self, symbols): tradable_assets = [] for symbol in symbols: security = self.securities[symbol] if security.price and security.is_tradable: tradable_assets.append(symbol) return tradable_assets def on_data(self, data): if (not data.contains_key("VIX") or not data.contains_key("PutCallRatio") or data["VIX"].value == 0 or data["PutCallRatio"].value == 0): return self._vix_sma_1.update(self.time, data["VIX"].value) self._vix_sma_6.update(self.time, data["VIX"].value) self._pc_ratio_sma_1.update(self.time, data["PutCallRatio"].value) self._pc_ratio_sma_6.update(self.time, data["PutCallRatio"].value) if self._month_start and self._selection: self._month_start = False self._selection = False self._value_portfolio = self._get_tradable_assets(self._value_portfolio) self._growth_portfolio = self._get_tradable_assets(self._growth_portfolio) stocks_invested = [x.key for x in self.portfolio if x.value.invested] for i in stocks_invested: if i not in self._value_portfolio+self._growth_portfolio: self.liquidate(i) if self._vix_sma_1.current.value > self._vix_sma_6.current.value: if self._pc_ratio_sma_1.current.value < self._pc_ratio_sma_6.current.value: long_weight = 1/len(self._value_portfolio) for long_ in self._value_portfolio: self.set_holdings(long_, long_weight) elif self._pc_ratio_sma_1.current.value > self._pc_ratio_sma_6.current.value: short_weight = 1/len(self._value_portfolio) for short in self._value_portfolio: self.set_holdings(short, -short_weight) else: long_weight = 1/len(self._value_portfolio+self._growth_portfolio) for long_ in self._value_portfolio+self._growth_portfolio: self.set_holdings(long_, long_weight) class PutCallRatio(PythonData): def get_source(self, config, date, is_live_mode): return SubscriptionDataSource( "https://cdn.cboe.com/resources/options/volume_and_call_put_ratios/totalpc.csv", SubscriptionTransportMedium.REMOTE_FILE) def reader(self, config, line, date, is_live_mode): if not (line.strip() and line[0].isdigit()): return None index = CBOE() index.symbol = config.symbol try: # Example File Format: # DATE CALL PUT TOTAL P/C Ratio # 11/1/06 976510 623929 1600439 0.64 data = line.split(',') index.time = datetime.strptime(data[0], "%m/%d/%Y").strftime("%Y-%m-%d") index.value = Decimal(data[4]) except ValueError: return None return index