Overall Statistics |
Total Orders 509 Average Win 0.30% Average Loss -0.15% Compounding Annual Return 3.627% Drawdown 18.100% Expectancy 1.047 Start Equity 100000 End Equity 169486.95 Net Profit 69.487% Sharpe Ratio 0.18 Sortino Ratio 0.171 Probabilistic Sharpe Ratio 0.885% Loss Rate 32% Win Rate 68% Profit-Loss Ratio 2.03 Alpha -0.01 Beta 0.223 Annual Standard Deviation 0.054 Annual Variance 0.003 Information Ratio -0.664 Tracking Error 0.119 Treynor Ratio 0.044 Total Fees $108.22 Estimated Strategy Capacity $0 Lowest Capacity Asset FAMA_FRENCH_5_MARKET_EQ.QuantpediaFamaFrenchEquity 2S Portfolio Turnover 0.31% |
from AlgorithmImports import * # Quantpedia data. # NOTE: IMPORTANT: Data order must be ascending (datewise) class QuantpediaFamaFrench(PythonData): def GetSource(self, config, date, isLiveMode): return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/equity/fama_french/{config.Symbol.Value.lower()}.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) def Reader(self, config, line, date, isLiveMode): data = QuantpediaFamaFrench() data.Symbol = config.Symbol if not line[0].isdigit(): return None split = line.split(';') data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) data['market'] = float(split[1]) data['size'] = float(split[2]) data['value'] = float(split[3]) data['profitability'] = float(split[4]) data['investment'] = float(split[5]) return data class QuantpediaFamaFrenchEquity(PythonData): def GetSource(self, config, date, isLiveMode): return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/equity/fama_french/{config.Symbol.Value.lower()}.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) def Reader(self, config, line, date, isLiveMode): data = QuantpediaFamaFrenchEquity() data.Symbol = config.Symbol if not line[0].isdigit(): return None split = line.split(';') data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) data.Value = float(split[1]) return data # custom fee model class CustomFeeModel: def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))
# https://quantpedia.com/strategies/mean-variance-factor-timing/ # # The investment universe consists of all AMEX, NYSE, and NASDAQ-listed U.S. stocks. The data come from Kenneth French’s website. Create factor portfolios based on five factors: # size, value, momentum, investment, and profitability. # Using the Markowitz model, construct a long-short efficient portfolio maximizing the Sharpe ratio. Each month run out-of-sample estimation using previous 60-month data. # # QC Implementation changes: #region imports from AlgorithmImports import * from scipy.optimize import minimize import data_tools #endregion class MeanVarianceFactorTiming(QCAlgorithm): def Initialize(self): self.SetStartDate(2010, 1, 1) self.SetCash(100000) self.period:int = 60 * 21 # warm up fama french values for idiosyncratic volatility self.SetWarmup(self.period, Resolution.Daily) self.data:dict = {} self.fama_french_symbol:Symbol = self.AddData(data_tools.QuantpediaFamaFrench, 'fama_french_5_factor', Resolution.Daily).Symbol self.ff_factor_names:list[str] = ['market', 'size', 'value', 'profitability', 'investment'] # ff performance data self.fama_french_data:dict = { ff_factor_name : RollingWindow[float](self.period) for ff_factor_name in self.ff_factor_names } # ff traded symbols for factor_name in self.ff_factor_names: data:Security = self.AddData(data_tools.QuantpediaFamaFrenchEquity, f'fama_french_5_{factor_name}_eq', Resolution.Daily) data.SetLeverage(3) data.SetFeeModel(data_tools.CustomFeeModel()) self.recent_month:int = -1 self.settings.minimum_order_margin_portfolio_percentage = 0. def OnData(self, data): # update fama french values on daily basis if self.fama_french_symbol in data and data[self.fama_french_symbol]: for ff_factor_name in self.ff_factor_names: self.fama_french_data[ff_factor_name].Add(data[self.fama_french_symbol].GetProperty(ff_factor_name)) if self.recent_month == self.Time.month: return self.recent_month = self.Time.month # optimization if all(x[1].IsReady for x in self.fama_french_data.items()): perf_df:pd.DataFrame = pd.DataFrame(columns=self.ff_factor_names) for ff_factor_name in self.ff_factor_names: perf_df[ff_factor_name] = np.array([x for x in self.fama_french_data[ff_factor_name]][::-1]) opt, weights = self.optimization_method(perf_df) for ff_factor_symbol, w in weights.items(): traded_symbol:str = f'fama_french_5_{ff_factor_symbol}_eq' if abs(w) > 0.001: self.SetHoldings(traded_symbol, w) else: self.Liquidate(traded_symbol) def optimization_method(self, returns:pd.DataFrame): '''Maximize sharpe ratio method''' # objective function fun = lambda weights: - np.sum(returns.mean() * weights) * 252 / np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights))) # Constraint #1: The weights can be negative, which means investors can short a security. constraints = [{'type': 'eq', 'fun': lambda w: 1 - np.sum(w)}] size = returns.columns.size x0 = np.array(size * [1. / size]) # bounds = tuple((self.minimum_weight, self.maximum_weight) for x in range(size)) bounds = tuple((0, 1) for x in range(size)) opt = minimize(fun, # Objective function x0, # Initial guess method='SLSQP', # Optimization method: Sequential Least SQuares Programming bounds = bounds, # Bounds for variables constraints = constraints) # Constraints definition return opt, pd.Series(opt['x'], index = returns.columns)