| Overall Statistics |
|
Total Orders 9848 Average Win 0.91% Average Loss -0.87% Compounding Annual Return 19.616% Drawdown 58.700% Expectancy 0.057 Start Equity 1000000 End Equity 2449554.11 Net Profit 144.955% Sharpe Ratio 0.466 Sortino Ratio 0.485 Probabilistic Sharpe Ratio 9.020% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.04 Alpha 0.164 Beta 0.632 Annual Standard Deviation 0.433 Annual Variance 0.187 Information Ratio 0.333 Tracking Error 0.427 Treynor Ratio 0.319 Total Fees $899343.57 Estimated Strategy Capacity $340000.00 Lowest Capacity Asset IAIC XTU2YXLMT085 Portfolio Turnover 55.84% Drawdown Recovery 313 |
# region imports
from operator import lt, gt
from AlgorithmImports import *
# endregion
class ReversalFakeoutAlpha(AlphaModel):
def __init__(self, algorithm, date_rule, assets_per_side):
self._universe = []
self._assets_per_side = assets_per_side
self._insights = []
self._adx_threshold = 25
# Add a Scheduled Event to create insights matching the universe cadence.
algorithm.schedule.on(date_rule, algorithm.time_rules.at(8, 0), self._create_insights)
def _create_insights(self):
# Check if valid asset prices and all windows/indicators are ready.
securities = [s for s in self._universe if s.price and s.daily_returns.is_ready and s.monthly_window.is_ready]
if not securities:
return
# Calculate and sort by the momentum scores for months 1-12 skipping the first month.
sorted_by_monthly_return = sorted(securities, key=lambda s: math.prod(s.monthly_window[i].close / s.monthly_window[i].open for i in range(1, s.monthly_window.size)))
# Long the top 100 performers (strongest momentum) where positive DI > negative DI.
self._insights += self._long_short_picks(sorted_by_monthly_return[-100:], False, lt, InsightDirection.UP)
# Short the bottom 100 performers (weakest momentum) where negative DI > positive DI triggers reversal.
self._insights += self._long_short_picks(sorted_by_monthly_return[:100], True, gt, InsightDirection.DOWN)
def _long_short_picks(self, candidates, reverse, compare, direction):
# Filter candidates by ADX > threshold and positive DI > negative DI (for long) or negative DI > positive DI (for short).
candidates = [
candidate for candidate in candidates
if candidate.adx.current.value > self._adx_threshold and compare(candidate.adx.positive_directional_index, candidate.adx.negative_directional_index)
]
# Calculate continuity score [-1, 1] for volatility/reversal: (2 * down_days - total_days) / total_days.
filtered = sorted(candidates, key=lambda s: (2 * sum(1 for r in s.daily_returns if r <= 0) - s.daily_returns.count) / s.daily_returns.count, reverse=reverse)
return [Insight.price(s, timedelta(5), direction) for s in filtered[:self._assets_per_side]]
def update(self, algorithm, data):
insights = self._insights.copy()
self._insights.clear()
return insights
def on_securities_changed(self, algorithm, changes):
for security in changes.added_securities:
# Create up two consolidators + two consolidators: monthly for momentum ranking and daily for intraday (open-to-close) returns.
security.daily_returns = RollingWindow(280)
security.monthly_window = RollingWindow[TradeBar](13)
security.consolidators = [algorithm.consolidate(security, Calendar.MONTHLY, lambda bar: algorithm.securities[bar.symbol].monthly_window.add(bar)),
algorithm.consolidate(security, Resolution.DAILY, lambda bar: algorithm.securities[bar.symbol].daily_returns.add((bar.close - bar.open) / bar.open))]
security.adx = algorithm.adx(security, 20)
self._universe.append(security)
# Warm up the daily and monthly windows.
history = algorithm.history[TradeBar](security, 600)
for bar in history:
for consolidator in security.consolidators:
consolidator.update(bar)
for security in changes.removed_securities:
if security in self._universe:
for consolidator in security.consolidators:
algorithm.subscription_manager.remove_consolidator(security, consolidator)
self._universe.remove(security)# region imports
from AlgorithmImports import *
from alpha import ReversalFakeoutAlpha
# endregion
class CrossSectionalMomentum(QCAlgorithm):
def initialize(self):
self.set_start_date(self.end_date - timedelta(5*365))
self.settings.seed_initial_prices = True
self.settings.automatic_indicator_warm_up = True
self.set_cash(1_000_000)
self.universe_settings.resolution = Resolution.DAILY
date_rule = self.date_rules.week_start("SPY")
self.universe_settings.schedule.on(date_rule)
self.universe_settings.asynchronous = True
self.add_universe_selection(FundamentalUniverseSelectionModel(
lambda fundamentals: [
f.symbol for f in sorted(fundamentals, key=lambda f: f.dollar_volume)[-1000:]
]
))
self.add_alpha(ReversalFakeoutAlpha(self, date_rule, 5))
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
self.set_risk_management(TrailingStopRiskManagementModel(0.02))
self.set_warm_up(timedelta(10))