Overall Statistics |
Total Orders 776 Average Win 0.37% Average Loss -0.55% Compounding Annual Return 21.848% Drawdown 38.700% Expectancy 0.282 Start Equity 100000 End Equity 239731.17 Net Profit 139.731% Sharpe Ratio 0.716 Sortino Ratio 0.74 Probabilistic Sharpe Ratio 25.900% Loss Rate 23% Win Rate 77% Profit-Loss Ratio 0.66 Alpha 0.055 Beta 1.017 Annual Standard Deviation 0.213 Annual Variance 0.046 Information Ratio 0.398 Tracking Error 0.141 Treynor Ratio 0.15 Total Fees $1538.19 Estimated Strategy Capacity $180000.00 Lowest Capacity Asset ATCO RM5P2C3931PH Portfolio Turnover 0.73% |
#region imports from AlgorithmImports import * import statistics as stat import pickle from collections import deque #endregion class DynamicCalibratedGearbox(QCAlgorithm): def initialize(self): ### IMPORTANT: FOR USERS RUNNING THIS ALGORITHM IN LIVE TRADING, ### RUN THE BACKTEST ONCE self._tech_ROA_key = 'TECH_ROA' # we need 3 extra years to warmup our ROA values self.set_start_date(2016, 4, 1) self.set_end_date(2020, 9, 1) self.set_warm_up(timedelta(3*365)) self.set_cash(100000) # Set Strategy Cash self.set_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(days=31))) self.set_execution(ImmediateExecutionModel()) self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(lambda time:None)) self.add_universe_selection( FineFundamentalUniverseSelectionModel(self._coarse_filter, self._fine_filter) ) self.universe_settings.resolution = Resolution.DAILY self._curr_month = -1 # store ROA of tech stocks self._tech_ROA = {} self._symbols = None if self.live_mode and not self.object_store.contains_key(self._tech_ROA_key): self.quit('QUITTING: USING LIVE MOVE WITHOUT TECH_ROA VALUES IN OBJECT STORE') self._quarters = 0 def on_end_of_algorithm(self): self.log('Algorithm End') self._save_data() def _save_data(self): ''' Saves the tech ROA data to ObjectStore ''' # Symbol objects aren't picklable, hence why we use the ticker string tech_ROA = {symbol.value: roa for symbol, roa in self._tech_ROA.items()} self.object_store.save_bytes(self._tech_ROA_key, pickle.dumps(tech_ROA)) def _coarse_filter(self, coarse): # load data from ObjectStore if len(self._tech_ROA) == 0 and self.object_store.contains_key(self._tech_ROA_key): tech_ROA = self.object_store.read_bytes(self._tech_ROA_key) tech_ROA = pickle.loads(bytearray(tech_ROA)) self._tech_ROA = {Symbol.create(ticker, SecurityType.EQUITY, Market.USA): roa for ticker, roa in tech_ROA.items()} return list(self._tech_ROA.keys()) if self._curr_month == self.time.month: return Universe.UNCHANGED self._curr_month = self.time.month # we only want to update our ROA values every three months if self.time.month % 3 != 1: return Universe.UNCHANGED self._quarters += 1 return [c.symbol for c in coarse if c.has_fundamental_data] def _fine_filter(self, fine): # book value == FinancialStatements.BalanceSheet.NetTangibleAssets (book value and NTA are synonyms) # BM (Book-to-Market) == book value / MarketCap # ROA == OperationRatios.ROA # CFROA == FinancialStatements.CashFlowStatement.OperatingCashFlow / FinancialStatements.BalanceSheet.TotalAssets # R&D to MktCap == FinancialStatements.IncomeStatement.ResearchAndDevelopment / MarketCap # CapEx to MktCap == FinancialStatements.CashFlowStatement.CapExReported / MarketCap # Advertising to MktCap == FinancialStatements.IncomeStatement.SellingGeneralAndAdministration / MarketCap # note: this parameter may be slightly higher than pure advertising costs tech_securities = [f for f in fine if f.asset_classification.morningstar_sector_code == MorningstarSectorCode.TECHNOLOGY and not np.isnan(f.operation_ratios.roa.three_months)] for security in tech_securities: # we use deques instead of RWs since deques are picklable symbol = security.symbol if symbol not in self._tech_ROA: # 3 years * 4 quarters = 12 quarters of data self._tech_ROA[symbol] = deque(maxlen=12) self._tech_ROA[symbol].append(security.operation_ratios.roa.three_months) if self.live_mode: # this ensures we don't lose new data from an algorithm outage self.save_data() # we want to rebalance in the fourth month after the (fiscal) year ends # so that we have the most recent quarter's data if self.time.month != 4 or (self._quarters < 12 and not self.live_mode): return Universe.UNCHANGED # make sure our stocks has these fundamentals tech_securities = [ x for x in tech_securities if ( not np.isnan(x.operation_ratios.roa.one_year) and x.operation_ratios.roa.one_year and not np.isnan(x.financial_statements.cash_flow_statement.operating_cash_flow.twelve_months) and x.financial_statements.cash_flow_statement.operating_cash_flow.twelve_months and not np.isnan(x.financial_statements.balance_sheet.total_assets.twelve_months) and x.financial_statements.balance_sheet.total_assets.twelve_months and not np.isnan(x.financial_statements.income_statement.research_and_development.twelve_months) and x.financial_statements.income_statement.research_and_development.twelve_months and not np.isnan(x.financial_statements.cash_flow_statement.cap_ex_reported.twelve_months) and x.financial_statements.cash_flow_statement.cap_ex_reported.twelve_months and not np.isnan(x.financial_statements.income_statement.selling_general_and_administration.twelve_months) and x.financial_statements.income_statement.selling_general_and_administration.twelve_months and not np.isnan(x.market_cap) and x.market_cap ) ] # compute the variance of the ROA for each tech stock tech_VARROA = {symbol:stat.variance(roa) for symbol, roa in self._tech_ROA.items() if len(roa) == roa.maxlen} if len(tech_VARROA) < 2: return Universe.UNCHANGED tech_VARROA_median = stat.median(tech_VARROA.values()) # we will now map tech Symbols to various fundamental ratios, # and compute the median for each ratio # ROA 1-year tech_ROA1Y = {x.symbol:x.operation_ratios.roa.OneYear for x in tech_securities} tech_ROA1Y_median = stat.median(tech_ROA1Y.values()) # Cash Flow ROA tech_CFROA = {x.symbol: ( x.financial_statements.cash_flow_statement.operating_cash_flow.twelve_months / x.financial_statements.balance_sheet.total_assets.twelve_months ) for x in tech_securities} tech_CFROA_median = stat.median(tech_CFROA.values()) # R&D to MktCap tech_RD2MktCap = {x.symbol: ( x.financial_statements.income_statement.research_and_development.twelve_months / x.market_cap ) for x in tech_securities} tech_RD2MktCap_median = stat.median(tech_RD2MktCap.values()) # CapEx to MktCap tech_CaPex2MktCap = {x.symbol: ( x.financial_statements.cash_flow_statement.cap_ex_reported.twelve_months / x.market_cap ) for x in tech_securities} tech_CaPex2MktCap_median = stat.median(tech_CaPex2MktCap.values()) # Advertising to MktCap tech_Ad2MktCap = {x.symbol: ( x.financial_statements.income_statement.selling_general_and_administration.twelve_months / x.market_cap ) for x in tech_securities} tech_Ad2MktCap_median = stat.median(tech_Ad2MktCap.values()) # sort fine by book-to-market ratio, get lower quintile has_book = [f for f in fine if f.financial_statements.balance_sheet.net_tangible_assets.twelve_months and f.market_cap] sorted_by_BM = sorted(has_book, key=lambda x: x.financial_statements.balance_sheet.net_tangible_assets.twelve_months / x.market_cap)[:len(has_book)//4] # choose tech stocks from lower quintile tech_symbols = [f.symbol for f in sorted_by_BM if f in tech_securities] ratioDicts_medians = [(tech_ROA1Y, tech_ROA1Y_median), (tech_CFROA, tech_CFROA_median), (tech_RD2MktCap, tech_RD2MktCap_median), (tech_CaPex2MktCap, tech_CaPex2MktCap_median), (tech_Ad2MktCap, tech_Ad2MktCap_median)] def compute_g_score(symbol): g_score = 0 if tech_CFROA[symbol] > tech_ROA1Y[symbol]: g_score += 1 if symbol in tech_VARROA and tech_VARROA[symbol] < tech_VARROA_median: g_score += 1 for ratio_dict, median in ratioDicts_medians: if symbol in ratio_dict and ratio_dict[symbol] > median: g_score += 1 return g_score # compute g-scores for each symbol g_scores = {symbol:compute_g_score(symbol) for symbol in tech_symbols} return [symbol for symbol, g_score in g_scores.items() if g_score >= 5]