| Overall Statistics |
|
Total Orders 493 Average Win 1.61% Average Loss -1.25% Compounding Annual Return 26.119% Drawdown 37.100% Expectancy 0.617 Start Equity 100000 End Equity 635225.36 Net Profit 535.225% Sharpe Ratio 0.82 Sortino Ratio 0.911 Probabilistic Sharpe Ratio 34.414% Loss Rate 29% Win Rate 71% Profit-Loss Ratio 1.29 Alpha 0.09 Beta 0.994 Annual Standard Deviation 0.206 Annual Variance 0.042 Information Ratio 0.697 Tracking Error 0.129 Treynor Ratio 0.17 Total Fees $1358.65 Estimated Strategy Capacity $250000000.00 Lowest Capacity Asset ORCL R735QTJ8XC9X Portfolio Turnover 1.77% Drawdown Recovery 543 |
# region imports
from AlgorithmImports import *
# endregion
class MeasuredRedOrangeBat(QCAlgorithm):
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_cash(100000)
self.settings.seed_initial_prices = True
# Warm up to pre-load historical data
self.set_warm_up(100, Resolution.DAILY)
# Per-position stop loss
self._stop_loss_pct = 0.02 # 2% of portfolio value loss per position
# Gold for cash allocation
self._hedge_etf = "GLD" # SPDR Gold Shares
self.add_equity(self._hedge_etf)
# Momentum parameters
self._lookback_period = 90
self._num_positions = 10
self._momentum_indicators = {}
self._volatility_indicators = {} # Track volatility for position sizing
# Add tech universe
self._universe = self.add_universe(self._select_tech_stocks)
# Schedule monthly rebalancing on first day of month at market open
self.schedule.on(self.date_rules.month_start(),
self.time_rules.after_market_open("SPY", 1),
self._rebalance)
# Check drawdown daily
self.schedule.on(self.date_rules.every_day(),
self.time_rules.after_market_open("SPY", 30),
self._check_drawdown)
# Allocate excess cash to bonds daily
self.schedule.on(self.date_rules.every_day(),
self.time_rules.after_market_open("SPY", 31),
self._allocate_excess_cash)
def _select_tech_stocks(self, fundamentals: List[Fundamental]) -> List[Symbol]:
# Filter for technology sector stocks
tech_stocks = [f for f in fundamentals
if f.has_fundamental_data
and f.asset_classification.morningstar_sector_code == MorningstarSectorCode.TECHNOLOGY
and f.market_cap > 1e9 # Market cap > $1B
and f.volume > 500000] # Minimum liquidity
# Sort by market cap and take top 20 for selection
tech_stocks = sorted(tech_stocks, key=lambda f: f.market_cap, reverse=True)[:20]
return [f.symbol for f in tech_stocks]
def on_securities_changed(self, changes: SecurityChanges):
# Initialize momentum and volatility indicators for new securities
for security in changes.added_securities:
symbol = security.symbol
if symbol not in self._momentum_indicators:
self._momentum_indicators[symbol] = self.momp(symbol, self._lookback_period, Resolution.DAILY)
self._volatility_indicators[symbol] = self.std(symbol, 20, Resolution.DAILY)
# Clean up removed securities
for security in changes.removed_securities:
symbol = security.symbol
if symbol in self._momentum_indicators:
del self._momentum_indicators[symbol]
if symbol in self._volatility_indicators:
del self._volatility_indicators[symbol]
def _rebalance(self):
# Skip if warming up or no universe selections
if self.is_warming_up or not self._universe.selected:
return
# Calculate momentum scores for active universe
momentum_scores = {}
for symbol in self._universe.selected:
if (symbol in self._momentum_indicators and
self._momentum_indicators[symbol].is_ready and
symbol in self._volatility_indicators and
self._volatility_indicators[symbol].is_ready):
# Filter out negative momentum stocks
momentum = self._momentum_indicators[symbol].current.value
if momentum > 0:
momentum_scores[symbol] = momentum
# Skip if not enough data
if len(momentum_scores) < self._num_positions:
return
# Select top momentum stocks
sorted_by_momentum = sorted(momentum_scores.items(), key=lambda x: x[1], reverse=True)
selected_symbols = [symbol for symbol, _ in sorted_by_momentum[:self._num_positions]]
# Volatility-adjusted position sizing
volatilities = {symbol: self._volatility_indicators[symbol].current.value
for symbol in selected_symbols}
# Inverse volatility weighting
inv_vols = {symbol: 1.0 / max(vol, 0.01) for symbol, vol in volatilities.items()}
total_inv_vol = sum(inv_vols.values())
# Calculate weights with 100% allocation
max_allocation = 1.0
weights = {symbol: max_allocation * (inv_vol / total_inv_vol)
for symbol, inv_vol in inv_vols.items()}
# Calculate total weight and allocate remainder to leveraged bonds
total_weight = sum(weights.values())
remaining_weight = 1.0 - total_weight
# Debug logging
self.log(f"Date: {self.time} | Momentum stocks selected: {len(selected_symbols)} | Total weight: {total_weight:.2%} | Remaining: {remaining_weight:.2%}")
targets = [PortfolioTarget(symbol, weight) for symbol, weight in weights.items()]
# Add gold allocation if there's remaining weight
if remaining_weight > 0.01: # Only allocate if meaningful amount
targets.append(PortfolioTarget(self.symbol(self._hedge_etf), remaining_weight))
self.log(f"GOLD ALLOCATION: {remaining_weight:.2%} to {self._hedge_etf}")
else:
self.log(f"No gold allocation needed (remaining: {remaining_weight:.2%})")
# Set holdings and liquidate positions not in targets
self.set_holdings(targets, liquidate_existing_holdings=True)
self.log(f"Rebalanced to {len(selected_symbols)} positions | Portfolio value: ${self.portfolio.total_portfolio_value:,.0f}")
def _check_drawdown(self):
# Check per-position stop losses based on PnL
portfolio_value = self.portfolio.total_portfolio_value
max_loss_per_position = portfolio_value * self._stop_loss_pct
symbols_to_liquidate = []
# Debug: log current portfolio allocation
allocated_value = sum(holding.absolute_holdings_value for holding in self.portfolio.values() if holding.quantity != 0)
cash_value = self.portfolio.cash
allocation_pct = (allocated_value / portfolio_value * 100) if portfolio_value > 0 else 0
# Check all positions in portfolio
for symbol in list(self.portfolio.keys()):
holding = self.portfolio[symbol]
if holding.quantity > 0:
# Get unrealized loss in dollars
unrealized_profit = holding.unrealized_profit
# Liquidate if position loss exceeds 2% of portfolio value
if unrealized_profit <= -max_loss_per_position:
symbols_to_liquidate.append(symbol)
self.log(f"STOP LOSS: {symbol} unrealized loss ${abs(unrealized_profit):.2f}, max allowed loss: ${max_loss_per_position:.2f}")
# Liquidate positions that hit stop loss and log portfolio state
if symbols_to_liquidate:
self.log(f"Portfolio before liquidation - Allocated: {allocation_pct:.1f}% (${allocated_value:,.0f}) | Cash: ${cash_value:,.0f}")
for symbol in symbols_to_liquidate:
self.liquidate(symbol)
self.log(f"Liquidated {len(symbols_to_liquidate)} positions due to stop loss")
def _allocate_excess_cash(self):
# Allocate excess cash to gold between rebalances
portfolio_value = self.portfolio.total_portfolio_value
if portfolio_value <= 0:
return
# Calculate current allocation excluding cash and gold
equity_value = sum(holding.absolute_holdings_value for symbol, holding in self.portfolio.items()
if holding.quantity > 0 and symbol.value != self._hedge_etf)
equity_allocation = equity_value / portfolio_value
# If equity allocation is below 100%, allocate excess to gold
excess_allocation = 1.0 - equity_allocation
if excess_allocation > 0.05: # Only rebalance if more than 5% excess
gold_symbol = self.symbol(self._hedge_etf)
current_gold_value = self.portfolio[gold_symbol].absolute_holdings_value
current_gold_allocation = current_gold_value / portfolio_value if portfolio_value > 0 else 0
if excess_allocation > current_gold_allocation + 0.02: # More than 2% opportunity
self.set_holdings([PortfolioTarget(gold_symbol, excess_allocation)], liquidate_existing_holdings=False)
self.log(f"Allocated {excess_allocation:.2%} to {self._hedge_etf} (excess cash reallocation)")