| Overall Statistics |
|
Total Orders 576 Average Win 0.62% Average Loss -0.39% Compounding Annual Return 26.543% Drawdown 16.200% Expectancy 1.150 Start Equity 100000 End Equity 324208.88 Net Profit 224.209% Sharpe Ratio 1.102 Sortino Ratio 1.204 Probabilistic Sharpe Ratio 79.108% Loss Rate 16% Win Rate 84% Profit-Loss Ratio 1.57 Alpha 0.121 Beta 0.409 Annual Standard Deviation 0.131 Annual Variance 0.017 Information Ratio 0.612 Tracking Error 0.144 Treynor Ratio 0.352 Total Fees $709.96 Estimated Strategy Capacity $200000000.00 Lowest Capacity Asset BIL TT1EBZ21QWKL Portfolio Turnover 4.07% Drawdown Recovery 514 |
# region imports
from AlgorithmImports import *
# endregion
class HybridModel(QCAlgorithm):
def initialize(self):
self.set_start_date(self.end_date - timedelta(5 * 365))
self.set_cash(100000)
self._qqq = self.add_equity("QQQ", Resolution.DAILY).symbol
self._smh = self.add_equity("SMH", Resolution.DAILY).symbol
self._gld = self.add_equity("GLD", Resolution.DAILY).symbol
self._bil = self.add_equity("BIL", Resolution.DAILY).symbol
self._risk_assets = [self._qqq, self._smh, self._gld]
self._symbols = [self._qqq, self._smh, self._gld, self._bil]
self._window_by_symbol = {symbol: RollingWindow[float](220) for symbol in self._symbols}
self.set_warm_up(timedelta(days=365), Resolution.DAILY)
self.schedule.on(self.date_rules.week_start(self._qqq), self.time_rules.at(8, 0), self._rebalance)
def on_data(self, data):
for symbol in self._symbols:
if symbol in data.bars:
self._window_by_symbol[symbol].add(float(data.bars[symbol].close))
def on_warmup_finished(self):
self._rebalance()
def _rebalance(self):
if self.is_warming_up:
return
# Score each risk asset by 6-month momentum divided by realized volatility.
score_by_symbol = {}
vol_by_symbol = {}
for symbol in self._risk_assets:
window = self._window_by_symbol[symbol]
if not window.is_ready:
continue
price = window[0]
old_price = window[126]
if old_price <= 0 or price <= sum(window[i] for i in range(200)) / 200:
continue
momentum = price / old_price - 1.0
if momentum <= 0:
continue
returns = [window[i] / window[i + 1] - 1.0 for i in range(63) if window[i + 1] > 0]
if not returns:
continue
volatility = float(np.std(returns))
if volatility > 0:
score_by_symbol[symbol] = momentum / volatility
vol_by_symbol[symbol] = volatility
# Allocate 75% of the book across the top two names by inverse volatility.
targets = {}
remaining_weight = 0.98
selected = sorted(score_by_symbol, key=lambda s: score_by_symbol[s])[-2:]
inverse_volatility_sum = sum(1.0 / vol_by_symbol[symbol] for symbol in selected)
if inverse_volatility_sum > 0:
for symbol in selected:
targets[symbol] = 0.75 * (1.0 / vol_by_symbol[symbol]) / inverse_volatility_sum
remaining_weight -= targets[symbol]
# Add a 20% boost to the deepest short-term pullback that is still above trend.
pullback_symbol = None
worst_pullback = 0.0
for symbol in self._risk_assets:
window = self._window_by_symbol[symbol]
if not window.is_ready:
continue
price = window[0]
five_day_price = window[5]
if five_day_price <= 0 or price <= sum(window[i] for i in range(200)) / 200:
continue
pullback = price / five_day_price - 1.0
if pullback < -0.02 and pullback < worst_pullback:
pullback_symbol = symbol
worst_pullback = pullback
if pullback_symbol is not None:
targets[pullback_symbol] = targets.get(pullback_symbol, 0) + 0.20
remaining_weight -= 0.20
if remaining_weight > 0:
targets[self._bil] = remaining_weight
for holding in list(self.portfolio.values()):
if holding.invested and holding.symbol not in targets:
self.set_holdings(holding.symbol, 0)
for symbol, weight in targets.items():
self.set_holdings(symbol, weight)