Overall Statistics |
Total Orders 1744 Average Win 0.68% Average Loss -0.69% Compounding Annual Return 1.283% Drawdown 48.600% Expectancy 0.012 Start Equity 100000 End Equity 113001.97 Net Profit 13.002% Sharpe Ratio 0.034 Sortino Ratio 0.038 Probabilistic Sharpe Ratio 0.062% Loss Rate 49% Win Rate 51% Profit-Loss Ratio 0.98 Alpha -0 Beta 0.047 Annual Standard Deviation 0.116 Annual Variance 0.014 Information Ratio -0.485 Tracking Error 0.165 Treynor Ratio 0.083 Total Fees $2893.83 Estimated Strategy Capacity $14000000.00 Lowest Capacity Asset SGMO RTNHJYBI1ROL Portfolio Turnover 3.78% |
#region imports from AlgorithmImports import * #endregion class FamaFrenchFiveFactorsAlgorithm(QCAlgorithm): ''' Stocks Selecting Strategy based on Fama French 5 Factors Model Reference: https://tevgeniou.github.io/EquityRiskFactors/bibliography/FiveFactor.pdf ''' def initialize(self): self.set_start_date(2010, 1, 1) # Set Start Date self.set_end_date(2019, 8, 1) # Set End Date self.set_cash(100000) # Set Strategy Cash self.universe_settings.resolution = Resolution.DAILY self.add_universe(self._coarse_selection_function, self._fine_selection_function) self._num_coarse = 200 # Number of symbols selected at Coarse Selection self._num_long = 5 # Number of stocks to long self._num_short = 5 # Number of stocks to short self._long_symbols = [] # Contains the stocks we'd like to long self._short_symbols = [] # Contains the stocks we'd like to short self._next_liquidate = self.time # Initialize last trade time self._rebalance_days = 30 # Set the weights of each factor self._beta_m = 1 self._beta_s = 1 self._beta_h = 1 self._beta_r = 1 self._beta_c = 1 def _coarse_selection_function(self, coarse): '''Drop securities which have no fundamental data or have too low prices. Select those with highest by dollar volume''' if self.time < self._next_liquidate: return Universe.UNCHANGED selected = sorted([x for x in coarse if x.has_fundamental_data and x.price > 5], key=lambda x: x.dollar_volume, reverse=True) return [x.symbol for x in selected[:self._num_coarse]] def _fine_selection_function(self, fine): '''Select securities with highest score on Fama French 5 factors''' # Select stocks with these 5 factors: # MKT -- Book value per share: Value # SMB -- TotalEquity: Size # HML -- Operation profit margin: Quality # RMW -- ROE: Profitability # CMA -- TotalAssetsGrowth: Investment Pattern filtered = [x for x in fine if x.valuation_ratios.book_value_per_share and x.financial_statements.balance_sheet.total_equity and x.operation_ratios.operation_margin.value and x.operation_ratios.ROE and x.operation_ratios.total_assets_growth] # Sort by factors sorted_by_mkt = sorted(filtered, key=lambda x: x.valuation_ratios.book_value_per_share, reverse=True) sorted_by_smb = sorted(filtered, key=lambda x: x.financial_statements.balance_sheet.total_equity.value, reverse=True) sorted_by_hml = sorted(filtered, key=lambda x: x.operation_ratios.operation_margin.value, reverse=True) sorted_by_rmw = sorted(filtered, key=lambda x: x.operation_ratios.ROE.value, reverse=True) sorted_by_cma = sorted(filtered, key=lambda x: x.operation_ratios.total_assets_growth.value, reverse=False) stock_by_symbol = {} # Get the rank based on 5 factors for every stock for index, stock in enumerate(sorted_by_mkt): mkt_rank = self._beta_m * index smb_rank = self._beta_s * sorted_by_smb.index(stock) hml_rank = self._beta_h * sorted_by_hml.index(stock) rmw_rank = self._beta_r * sorted_by_rmw.index(stock) cma_rank = self._beta_c * sorted_by_cma.index(stock) avg_rank = np.mean([mkt_rank,smb_rank,hml_rank,rmw_rank,cma_rank]) stock_by_symbol[stock.symbol] = avg_rank sorted_dict = sorted(stock_by_symbol.items(), key = lambda x: x[1], reverse = True) symbols = [x[0] for x in sorted_dict] # Pick the stocks with the highest scores to long self._long_symbols= symbols[:self._num_long] # Pick the stocks with the lowest scores to short self._short_symbols = symbols[-self._num_short:] return self._long_symbols + self._short_symbols def on_data(self, data): '''Rebalance Every self._rebalance_days''' # Liquidate stocks in the end of every month if self.time >= self._next_liquidate: for holding in self.portfolio.values(): # If the holding is in the long/short list for the next month, don't liquidate if holding.symbol in self._long_symbols or holding.symbol in self._short_symbols: continue # If the holding is not in the list, liquidate if holding.invested: self.liquidate(holding.symbol) count = len(self._long_symbols + self._short_symbols) # It means the long & short lists for the month have been cleared if count == 0: return # Open long position at the start of every month for symbol in self._long_symbols: self.set_holdings(symbol, 1/count) # Open short position at the start of every month for symbol in self._short_symbols: self.set_holdings(symbol, -1/count) # Set the Liquidate Date self._next_liquidate = self.time + timedelta(self._rebalance_days) # After opening positions, clear the long & short symbol lists until next universe selection self._long_symbols.clear() self._short_symbols.clear()