Overall Statistics |
Total Orders 4881 Average Win 0.33% Average Loss -0.30% Compounding Annual Return 6.998% Drawdown 24.000% Expectancy 0.142 Start Equity 100000 End Equity 272266.72 Net Profit 172.267% Sharpe Ratio 0.368 Sortino Ratio 0.394 Probabilistic Sharpe Ratio 1.443% Loss Rate 45% Win Rate 55% Profit-Loss Ratio 1.08 Alpha -0.012 Beta 0.545 Annual Standard Deviation 0.099 Annual Variance 0.01 Information Ratio -0.591 Tracking Error 0.089 Treynor Ratio 0.067 Total Fees $3141.50 Estimated Strategy Capacity $44000000.00 Lowest Capacity Asset AFL R735QTJ8XC9X Portfolio Turnover 6.50% |
# https://quantpedia.com/strategies/switching-between-value-and-momentum-in-stocks/ # # The investment universe consists of global equities. Two portfolios are created out of these stocks each month – value and momentum portfolios. # Exact methodology to form value/momentum portfolios is explained in Hsieh, Hodnett second paper (link in a “Related Research”) however classical # methodologies could be probably easily used too (momentum portfolio consists of stocks with the highest 6-12 month return and value portfolio # consists of stocks with he highest E/P ratios). # Overall investment portfolio consists of 3 parts – value, momentum and cash (invested in T-Bills) part.The weighted least squares (WLS) technique # is used in a next step. A series of rolling 36-month (month t-36 through month t) WLS regressions are performed monthly to calculate optimal # weights for value, momentum and cash component of a portfolio (investor is optimizing to have the highest Sharpe ratio). The optimal style # allocations estimated by WLS regressions are subsequently employed to perform style allocations for the next month. Calculation is performed # monthly and whole portfolio is rebalanced accordingly. # # QC implementation changes: # - Universe consists of 200 most liquid US stocks. from math import isnan import pandas as pd import numpy as np from scipy.optimize import minimize from AlgorithmImports import * class SwitchingbetweenValueMomentum(QCAlgorithm): def Initialize(self): self.SetStartDate(2010, 1, 1) self.SetCash(100000) self.data:Dict[Symbol, SymbolData] = {} self.period:int = 36 * 21 self.leverage:int = 5 self.quantile:int = 10 self.symbol:Symbol = self.AddEquity('IEF', Resolution.Daily).Symbol self.data[self.symbol] = SymbolData(self, self.symbol, self.period) # T-note prices self.portfolio_symbols:Set[Symbol] = set() # Selected sorted symbols self.fundamental_count:int = 200 self.fundamental_sorting_key = lambda x: x.DollarVolume self.selection_flag:bool = False self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.FundamentalSelectionFunction) self.Settings.MinimumOrderMarginPortfolioPercentage = 0. self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) self.settings.daily_precise_end_time = False def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: for security in changes.AddedSecurities: security.SetFeeModel(CustomFeeModel()) security.SetLeverage(self.leverage) def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]: # Update the rolling window every day. for stock in fundamental: symbol:Symbol = stock.Symbol # Store monthly price. if symbol in self.data: self.data[symbol].update(stock.AdjustedPrice) if not self.selection_flag: return Universe.Unchanged selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and \ not isnan(x.ValuationRatios.PERatio) and x.ValuationRatios.PERatio != 0 ] if len(selected) > self.fundamental_count: selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]] ep:Dict[Symbol, float] = {} performance:Dict[Symbol, float] = {} # Warmup price rolling windows. for stock in selected: symbol:Symbol = stock.Symbol if symbol not in self.data: self.data[symbol] = SymbolData(self, symbol, self.period) if self.data[symbol].is_ready(): ep[symbol] = (1 / stock.ValuationRatios.PERatio) performance[symbol] = self.data[symbol].performance() if len(ep) >= self.quantile: # Sorting by return and EP. sorted_by_ret:List = sorted(performance.items(), key = lambda x: x[1], reverse = True) quantile:int = int(len(sorted_by_ret) / self.quantile) high_by_ret:List[Symbol] = [x[0] for x in sorted_by_ret[:quantile]] sorted_by_ep:List = sorted(ep.items(), key = lambda x: x[1], reverse = True) quantile = int(len(sorted_by_ep) / self.quantile) high_by_ep:List[Symbol] = [x[0] for x in sorted_by_ep[:quantile]] self.portfolio_symbols = set(high_by_ret + high_by_ep) # portfolios with no duplication return list(self.portfolio_symbols) def OnData(self, data: Slice) -> None: if not self.selection_flag: return self.selection_flag = False self.Liquidate() if len(self.portfolio_symbols) == 0: return # Optimalization. data:Dict = { symbol : [x for x in self.data[symbol].Price] for symbol in self.portfolio_symbols if symbol in data and data[symbol]} data[self.symbol] = [x for x in self.data[self.symbol].Price] df_price = pd.DataFrame(data, columns=data.keys()) daily_return = (df_price / df_price.shift(1)).dropna() a = PortfolioOptimization(daily_return, 0, len(data)) opt_weight = a.opt_portfolio() if isnan(sum(opt_weight)): return for i in range(len(data)): w = opt_weight[i] if w >= 0.001: self.SetHoldings(df_price.columns[i], w) def Selection(self) -> None: if not self.data[self.symbol].is_ready(): return self.selection_flag = True class PortfolioOptimization(object): def __init__(self, df_return, risk_free_rate, num_assets): self.daily_return = df_return self.risk_free_rate = risk_free_rate self.n = num_assets # numbers of risk assets in portfolio self.target_vol = 0.08 def annual_port_return(self, weights): # calculate the annual return of portfolio return np.sum(self.daily_return.mean() * weights) * 252 def annual_port_vol(self, weights): # calculate the annual volatility of portfolio return np.sqrt(np.dot(weights.T, np.dot(self.daily_return.cov() * 252, weights))) def min_func(self, weights): # method 1: maximize sharp ratio return - self.annual_port_return(weights) / self.annual_port_vol(weights) # method 2: maximize the return with target volatility # return - self.annual_port_return(weights) / self.target_vol # method 3: minimize variance with target volatility # return (1 / self.annual_port_vol(weights)) / self.target_vol def opt_portfolio(self): # maximize the sharpe ratio to find the optimal weights cons = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1}) bnds = tuple((0, 1) for x in range(2)) + tuple((0, 0.25) for x in range(self.n - 2)) opt = minimize(self.min_func, # object function np.array(self.n * [1. / self.n]), # initial value method='SLSQP', # optimization method bounds=bnds, # bounds for variables constraints=cons) # constraint conditions opt_weights = opt['x'] #opt_sharpe = - opt['fun'] #opt_weights = opt['x'] #opt_return = self.annual_port_return(opt_weights) #opt_volatility = self.annual_port_vol(opt_weights) return opt_weights class SymbolData(): def __init__(self, algorithm, symbol, period): self.Symbol = symbol self.Price = RollingWindow[float](period) self.Algorithm = algorithm # Warmup. history = algorithm.History(algorithm.Symbol(symbol), period, Resolution.Daily) if not history.empty: closes = history.loc[symbol].close for time, close in closes.items(): self.Price.Add(close) def update(self, value): self.Price.Add(value) def is_ready(self) -> bool: return self.Price.IsReady # performance t12-t6. def performance(self) -> float: closes = [x for x in self.Price][:12*21][-6*21:] return (closes[0] / closes[-1] - 1) # Custom fee model. class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))