| Overall Statistics |
|
Total Orders 15202 Average Win 0.03% Average Loss -0.03% Compounding Annual Return 3.653% Drawdown 12.000% Expectancy 0.064 Start Equity 100000 End Equity 119658.98 Net Profit 19.659% Sharpe Ratio -0.14 Sortino Ratio -0.158 Probabilistic Sharpe Ratio 4.828% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.15 Alpha -0.005 Beta -0.086 Annual Standard Deviation 0.073 Annual Variance 0.005 Information Ratio -0.44 Tracking Error 0.17 Treynor Ratio 0.119 Total Fees $15095.00 Estimated Strategy Capacity $39000.00 Lowest Capacity Asset LBTYB SZC2UFSQNK9X Portfolio Turnover 0.80% Drawdown Recovery 1005 |
#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(self.end_date - timedelta(5*365))
self.set_cash(100_000)
self.settings.seed_initial_prices = True
# Define some parameters.
self._num_portfolios = 6
self._lookback = 20*6
# Add a universe of US Equities.
self._date_rule = self.date_rules.month_start('SPY')
self.universe_settings.schedule.on(self._date_rule)
self.universe_settings.resolution = Resolution.DAILY
self._universe = self.add_universe(self._select_assets)
# We'll churn 1/6 of the portfolio during each rebalance.
# Let's add a deque to help us keep track.
self._portfolios = deque(maxlen=self._num_portfolios)
# Add a warm-up period so the algorithm trades on deployment
# instead of waiting for the next month.
self.set_warm_up(timedelta(self._lookback))
def _select_assets(self, fundamentals):
# Select stocks trading above $5.
filtered = [
f for f in fundamentals
if f.has_fundamental_data and f.price > 5 and not np.isnan(f.market_cap)
]
# Take the 50% of stocks with that have higher market caps.
filtered = sorted(filtered, key=lambda f: f.market_cap)[-int(len(filtered)/2):]
return [f.symbol for f in filtered]
def on_securities_changed(self, changes):
# As assets enter the universe, warm up their trailing price data.
for security in changes.added_securities:
security.session.size = self._lookback
for bar in self.history[TradeBar](security, self._lookback):
security.session.update(bar)
def on_warmup_finished(self):
# Add a Scheduled Event to rebalance the portfolio each month.
time_rule = self.time_rules.at(8, 0)
self.schedule.on(self._date_rule, time_rule, self._rebalance)
# Rebalance today too.
if self.live_mode:
self._rebalance()
else:
self.schedule.on(self.date_rules.today, time_rule, self._rebalance)
def _rebalance(self):
# Get the securities in the universe that have sufficient
# trailing data.
securities = []
for symbol in self._universe.selected:
security = self.securities[symbol]
if security.session.is_ready and security.price:
securities.append(security)
if len(securities) < 100:
return
# Select the most volatile subset of stocks.
volatile_stocks = sorted(securities, key=self._volaility)[-int(0.2*len(securities)):]
# Sort stocks by their trailing returns and then split into long/short sides.
sorted_by_return = sorted(volatile_stocks, key=self._return)
longs = sorted_by_return[-int(0.2*len(sorted_by_return)):]
shorts = sorted_by_return[:int(0.2*len(sorted_by_return))]
# If the portfolio is full, liquidate the oldest 1/6th of holdings.
if len(self._portfolios) == self._portfolios.maxlen:
for security, quantity in list(self._portfolios)[0].items():
if security.is_tradable and security.price:
self.market_on_open_order(security, -quantity)
# For this new 1/6th of the portfoilo, form a long-short portfolio.
quantity_by_security = (
self._get_target_quantities(longs, 1)
| self._get_target_quantities(shorts, -1)
)
self._portfolios.append(quantity_by_security)
for security, quantity in quantity_by_security.items():
self.market_on_open_order(security, quantity)
def _get_target_quantities(self, securities, trade_direction):
# Calculate the trade quantity of each asset for this rebalance.
quantity_by_security = {}
for security in securities:
quantity = trade_direction * int(
self.portfolio.total_portfolio_value
/ 2
/ len(securities)
/ security.price
/ self._num_portfolios
)
if quantity:
quantity_by_security[security] = quantity
return quantity_by_security
def _get_history(self, security):
# Get the trailing bars from the security.session.
return [bar.close for bar in list(security.session)[::-1]][:-1]
def _volaility(self, security):
history = self._get_history(security)
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(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, security):
history = self._get_history(security)
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(history)[:-5]
# calculate the annualized realized return
return (prices[-1]-prices[0])/prices[0]