Overall Statistics |
Total Trades
12565
Average Win
0.07%
Average Loss
-0.10%
Compounding Annual Return
9.130%
Drawdown
38.600%
Expectancy
0.294
Net Profit
527.215%
Sharpe Ratio
0.657
Probabilistic Sharpe Ratio
3.288%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
0.73
Alpha
0.088
Beta
-0.064
Annual Standard Deviation
0.127
Annual Variance
0.016
Information Ratio
0.059
Tracking Error
0.227
Treynor Ratio
-1.301
Total Fees
$1791.66
|
# https://quantpedia.com/strategies/low-volatility-factor-effect-in-stocks-long-only-version/ # # The investment universe consists of global large-cap stocks (or US large-cap stocks). At the end of each month, the investor constructs # equally weighted decile portfolios by ranking the stocks on the past three-year volatility of weekly returns. The investor goes long # stocks in the top decile (stocks with the lowest volatility). # # QC implementation changes: # - Instead of all listed stock, we select 500 most liquid US stocks. import numpy as np class LowVolatilityFactorEffectStocksLongOnlyVersion(QCAlgorithm): def Initialize(self): self.SetStartDate(2000, 1, 1) self.SetCash(100000) self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol self.period = 3 * 12 * 21 self.coarse_count = 500 self.last_coarse = [] self.data = {} self.selection_flag = False self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.CoarseSelectionFunction) self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: security.SetFeeModel(CustomFeeModel(self)) security.SetLeverage(10) def CoarseSelectionFunction(self, coarse): # Update the rolling window every day. for stock in coarse: 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 = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] selected = [x.Symbol for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'], key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] # Warmup price rolling windows. for symbol in selected: if symbol in self.data: continue self.data[symbol] = SymbolData(symbol, self.period) history = self.History(symbol, self.period, Resolution.Daily) if history.empty: self.Log(f"Not enough data for {symbol} yet.") continue closes = history.loc[symbol].close for time, close in closes.iteritems(): self.data[symbol].update(close) self.last_coarse = [x for x in selected if self.data[x].is_ready()] return self.last_coarse def OnData(self, data): if not self.selection_flag: return self.selection_flag = False weekly_vol = {x : self.data[x].volatility() for x in self.last_coarse} # Volatility sorting. sorted_by_vol = sorted(weekly_vol.items(), key = lambda x: x[1], reverse = True) decile = int(len(sorted_by_vol) / 10) long = [x[0] for x in sorted_by_vol[-decile:]] # Trade execution. stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] for symbol in stocks_invested: if symbol not in long: self.Liquidate(symbol) for symbol in long: if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable: self.SetHoldings(symbol, 1 / len(long)) def Selection(self): self.selection_flag = True class SymbolData(): def __init__(self, symbol, period): self.Symbol = symbol self.Price = RollingWindow[float](period) def update(self, value): self.Price.Add(value) def is_ready(self) -> bool: return self.Price.IsReady def volatility(self) -> float: closes = [x for x in self.Price] # Weekly volatility calc. separete_weeks = [closes[x:x+5] for x in range(0, len(closes), 5)] weekly_returns = [x[0] / x[-1] - 1 for x in separete_weeks] return np.std(weekly_returns) # Custom fee model. class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))