| Overall Statistics |
|
Total Orders 6872 Average Win 0.05% Average Loss -0.05% Compounding Annual Return -0.739% Drawdown 6.400% Expectancy -0.016 Start Equity 100000 End Equity 96653.29 Net Profit -3.347% Sharpe Ratio -0.54 Sortino Ratio -0.637 Probabilistic Sharpe Ratio 0.147% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 0.99 Alpha -0.017 Beta 0.01 Annual Standard Deviation 0.03 Annual Variance 0.001 Information Ratio -0.837 Tracking Error 0.108 Treynor Ratio -1.705 Total Fees $6796.41 Estimated Strategy Capacity $1000.00 Lowest Capacity Asset FMI VK7WZY1YHPB9 Portfolio Turnover 1.05% |
#region imports
from AlgorithmImports import *
from collections import deque
#endregion
# https://quantpedia.com/Screener/Details/155
class MomentumReversalCombinedWithVolatility(QCAlgorithm):
def initialize(self):
self.set_start_date(2014, 1, 1) # Set Start Date
self.set_end_date(2018, 8, 1) # Set Start Date
self.set_cash(100000) # Set Strategy Cash
self.set_security_initializer(BrokerageModelSecurityInitializer(
self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
self.universe_settings.resolution = Resolution.DAILY
self.add_universe(self._coarse_selection_function, self._fine_selection_function)
self._data_dict = {}
# 1/6 of the portfolio is rebalanced every month
self._portfolios = deque(maxlen=6)
spy = Symbol.create("SPY", SecurityType.EQUITY, Market.USA)
self.schedule.on(self.date_rules.month_start(spy), self.time_rules.after_market_open(spy), self._rebalance)
# the lookback period for volatility and return is six months
self._lookback = 20*6
self._filtered_fine = None
self._monthly_rebalance = False
self.set_warm_up(self._lookback)
def _coarse_selection_function(self, coarse):
# update the price of stocks in universe everyday
for i in coarse:
if i.symbol not in self._data_dict:
self._data_dict[i.symbol] = SymbolData(self._lookback)
self._data_dict[i.symbol].update(i.adjusted_price)
if self._monthly_rebalance:
# drop stocks which have no fundamental data or have too low prices
return [x.symbol for x in coarse if (x.has_fundamental_data) and (float(x.price) > 5)]
else:
return []
def _fine_selection_function(self, fine):
if self._monthly_rebalance:
sorted_fine = sorted(fine, key=lambda x: x.earning_reports.basic_average_shares.value * self._data_dict[x.symbol].price, reverse=True)
# select stocks with large size
top_fine = sorted_fine[:int(0.5*len(sorted_fine))]
self._filtered_fine = [x.symbol for x in top_fine]
return self._filtered_fine
else:
return []
def _rebalance(self):
self._monthly_rebalance = True
def on_data(self, data):
if self._monthly_rebalance and self._filtered_fine and not self.is_warming_up:
filtered_data = {symbol: symbolData for (symbol, symbolData) in self._data_dict.items() if symbol in self._filtered_fine and symbolData.is_ready() and symbol in data.bars}
self._filtered_fine = None
self._monthly_rebalance = False
# if the dictionary is empty, then return
if len(filtered_data) < 100:
return
# sort the universe by volatility and select stocks in the top high volatility quintile
sorted_by_vol = sorted(filtered_data.items(), key=lambda x: x[1].volatility(), reverse=True)[:int(0.2*len(filtered_data))]
sorted_by_vol = dict(sorted_by_vol)
# sort the stocks in top-quintile by realized return
sorted_by_return = sorted(sorted_by_vol, key=lambda x: sorted_by_vol[x].return_(), reverse=True)
long_ = sorted_by_return[:int(0.2*len(sorted_by_return))]
short = sorted_by_return[-int(0.2*len(sorted_by_return)):]
self._portfolios.append(short + long_)
# 1/6 of the portfolio is rebalanced every month
if len(self._portfolios) == self._portfolios.maxlen:
for i in list(self._portfolios)[0]:
self.liquidate(i)
# stocks are equally weighted and held for 6 months
short_weight = 1/len(short)
for i in short:
self.set_holdings(i, -1/6*short_weight)
long_weight = 1/len(long_)
for i in long_:
self.set_holdings(i, 1/6*long_weight)
class SymbolData:
def __init__(self, lookback):
self.price = None
self._history = deque(maxlen=lookback)
def update(self, value):
# update yesterday's close price
self.price = value
# update the history price series
self._history.append(float(value))
def is_ready(self):
return len(self._history) == self._history.maxlen
def volatility(self):
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(self._history)[:-5]
returns = (prices[1:]-prices[:-1])/prices[:-1]
# calculate the annualized realized volatility
return np.std(returns)*np.sqrt(250/len(returns))
def return_(self):
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(self._history)[:-5]
# calculate the annualized realized return
return (prices[-1]-prices[0])/prices[0]