| Overall Statistics |
|
Total Orders 12436 Average Win 0.11% Average Loss -0.17% Compounding Annual Return 34.083% Drawdown 30.500% Expectancy 0.138 Start Equity 200000 End Equity 866067.35 Net Profit 333.034% Sharpe Ratio 0.816 Sortino Ratio 0.929 Probabilistic Sharpe Ratio 33.065% Loss Rate 30% Win Rate 70% Profit-Loss Ratio 0.63 Alpha 0.161 Beta 1.295 Annual Standard Deviation 0.295 Annual Variance 0.087 Information Ratio 0.759 Tracking Error 0.236 Treynor Ratio 0.186 Total Fees $15570.54 Estimated Strategy Capacity $93000000.00 Lowest Capacity Asset NVS RULY784EQ6AT Portfolio Turnover 25.48% Drawdown Recovery 559 |
from AlgorithmImports import *
from datetime import timedelta
from typing import List
import numpy as np
class EliteDipBuyer_MOC(QCAlgorithm):
"""
Long-only momentum strategy selecting high-Sharpe, large-cap equities
above their 200-SMA, rebalanced at 8 AM (orders fill at open).
"""
MAX_POSITIONS = 10
TARGET_LEVERAGE = 1.5
SHARPE_LOOKBACK = 252
SHARPE_FLOOR = 0.6
COARSE_SIZE = 300
FINE_SIZE = 200
MIN_PRICE = 50
MIN_DOLLAR_VOL = 25e6
MIN_MARKET_CAP = 10e9
STOP_ATR_MULT = 4.0
def initialize(self):
self.set_start_date(self.end_date - timedelta(5 * 365))
self.set_cash(200_000)
self.settings.free_portfolio_value_percentage = 0.02
self.settings.minimum_order_margin_portfolio_percentage = 0
self.settings.seed_initial_prices = True
self.settings.automatic_indicator_warm_up = True
self.universe_settings.resolution = Resolution.DAILY
self.universe_settings.leverage = 2.0
self._universe = self.add_universe(self._fundamental_filter)
self.schedule.on(
self.date_rules.every_day("SPY"),
self.time_rules.at(8, 0),
self._rebalance,
)
def _fundamental_filter(self, fundamental: List[Fundamental]) -> List[Symbol]:
pool = [
f for f in fundamental
if f.has_fundamental_data
and f.price > self.MIN_PRICE
and f.dollar_volume > self.MIN_DOLLAR_VOL
]
pool = sorted(pool, key=lambda f: f.dollar_volume, reverse=True)[:self.COARSE_SIZE]
pool = [f for f in pool if f.market_cap >= self.MIN_MARKET_CAP]
pool = sorted(pool, key=lambda f: f.market_cap, reverse=True)[:self.FINE_SIZE]
return [f.symbol for f in pool]
def on_securities_changed(self, changes: SecurityChanges) -> None:
for security in changes.added_securities:
security.ichimoku = self.ichimoku(security, 9, 26, 17, 52, 26, 26)
security.atr = self.atr(security, 50, MovingAverageType.WILDERS)
security.sma = self.sma(security, 200)
base_sharpe = self.sr(security, self.SHARPE_LOOKBACK, 0.0)
security.sharpe_ratio = IndicatorExtensions.times(base_sharpe, float(np.sqrt(252)))
security.stop_flag = False
def on_data(self, data: Slice) -> None:
if not data.bars:
return
for symbol in self._universe.selected:
security = self.securities[symbol]
if not security.ichimoku.is_ready or not security.atr.is_ready:
continue
atr_val = security.atr.current.value
if atr_val <= 0:
continue
kijun = security.ichimoku.kijun.current.value
if security.price < kijun - self.STOP_ATR_MULT * atr_val:
security.stop_flag = True
def _select_elite(self) -> List[Security]:
selected = []
for symbol in self._universe.selected:
security = self.securities[symbol]
if (not security.sma.is_ready or
not security.sharpe_ratio.is_ready or
security.price < security.sma.current.value or
security.sharpe_ratio.current.value < self.SHARPE_FLOOR):
continue
selected.append(security)
return sorted(selected, key=lambda security: security.sharpe_ratio)[-self.MAX_POSITIONS:]
def _rebalance(self) -> None:
elite = self._select_elite()
stopped = set()
for symbol in self._universe.selected:
security = self.securities[symbol]
if security.stop_flag and security.invested:
security.stop_flag = False
stopped.add(security)
elite = [s for s in elite if s not in stopped]
if not elite:
self.liquidate()
return
target_weight = self.TARGET_LEVERAGE / len(elite)
targets = [PortfolioTarget(security, target_weight) for security in elite]
self.set_holdings(targets, True)