| Overall Statistics |
|
Total Orders 1772 Average Win 0.53% Average Loss -0.59% Compounding Annual Return 33.789% Drawdown 32.700% Expectancy 0.057 Start Equity 100000 End Equity 125916.34 Net Profit 25.916% Sharpe Ratio 0.677 Sortino Ratio 0.741 Probabilistic Sharpe Ratio 37.773% Loss Rate 44% Win Rate 56% Profit-Loss Ratio 0.90 Alpha 0.159 Beta 1.332 Annual Standard Deviation 0.393 Annual Variance 0.155 Information Ratio 0.575 Tracking Error 0.323 Treynor Ratio 0.2 Total Fees $2805.18 Estimated Strategy Capacity $11000000.00 Lowest Capacity Asset CRML YGDFBZ0PUOH1 Portfolio Turnover 120.20% Drawdown Recovery 176 |
from AlgorithmImports import *
import numpy as np
class ShortTermCompetitionAlgo(QCAlgorithm):
def Initialize(self):
self.set_start_date(2025, 1, 1)
self.set_cash(100000)
self.spy = self.add_equity("SPY", Resolution.MINUTE)
self.set_benchmark("SPY")
self.universe_settings.resolution = Resolution.MINUTE
self.universe_settings.leverage = 2.0
# --- Parameters ---
self.rsi_period = 2
self.sma_period = 5
# Increased to 25 to ensure we get enough signals to rotate capital
self.buy_threshold = 25
self.max_positions = 10
self.active_symbols = set()
self.indicators = {}
self.add_universe(self.coarse_selection)
self.schedule.on(self.date_rules.every_day("SPY"),
self.time_rules.before_market_close("SPY", 15),
self.run_strategy)
def coarse_selection(self, coarse):
# 1. Filter
filtered = [x for x in coarse if x.price > 10 and x.has_fundamental_data]
# 2. Sort by Dollar Volume
sorted_by_volume = sorted(filtered, key=lambda x: x.dollar_volume, reverse=True)
# 3. Take Top 100
return [x.symbol for x in sorted_by_volume[:100]]
def OnSecuritiesChanged(self, changes):
# Handle Removals
for security in changes.removed_securities:
symbol = security.symbol
# --- BUG FIX: Zombie Prevention ---
# If we currently own this stock, DO NOT remove it from our tracker.
# We need to keep updating its indicators so we know when to sell it.
if self.portfolio[symbol].invested:
continue
if symbol in self.active_symbols:
self.active_symbols.remove(symbol)
if symbol in self.indicators:
self.unregister_indicator(self.indicators[symbol]['rsi'])
self.unregister_indicator(self.indicators[symbol]['sma'])
del self.indicators[symbol]
# Handle Additions
for security in changes.added_securities:
symbol = security.symbol
if symbol not in self.active_symbols and symbol.value != "SPY":
self.active_symbols.add(symbol)
# Indicators
rsi = self.rsi(symbol, self.rsi_period, MovingAverageType.WILDERS, Resolution.HOUR)
sma = self.sma(symbol, self.sma_period, Resolution.HOUR)
# Warmup Loop
try:
history = self.history(symbol, 40, Resolution.HOUR)
if not history.empty:
# Fix for MultiIndex issues
if 'symbol' in history.index.names:
history = history.droplevel('symbol')
for time, row in history.iterrows():
# Fix for Series vs Scalar issues
close_price = row.get('close') if isinstance(row, pd.Series) else row['close']
if isinstance(close_price, pd.Series):
close_price = close_price.iloc[0]
if pd.notna(close_price):
rsi.update(time, float(close_price))
sma.update(time, float(close_price))
except Exception as e:
self.debug(f"Warmup Failed for {symbol.value}: {str(e)}")
self.indicators[symbol] = {'rsi': rsi, 'sma': sma}
def run_strategy(self):
if self.is_warming_up:
return
targets = {}
candidates = []
# 1. Identify Candidates & Manage Positions
# We iterate a copy (list(...)) so we can modify the set safely if needed
for symbol in list(self.active_symbols):
if symbol not in self.indicators: continue
inds = self.indicators[symbol]
if not inds['rsi'].is_ready or not inds['sma'].is_ready:
continue
rsi_val = inds['rsi'].current.value
sma_val = inds['sma'].current.value
if not self.securities[symbol].has_data: continue
price = self.securities[symbol].price
# --- EXIT LOGIC ---
if self.portfolio[symbol].invested:
# 1. Profit Take / Standard Exit
if price > sma_val:
targets[symbol] = 0.0
self.log(f"EXIT PROFIT {symbol.value}: Price {price:.2f} > SMA {sma_val:.2f}")
# 2. Stop Loss (New Feature to free up slots)
elif self.portfolio[symbol].unrealized_profit_percent < -0.10:
targets[symbol] = 0.0
self.log(f"EXIT STOP LOSS {symbol.value}: Loss < -10%")
else:
# Hold position
continue
# --- ENTRY LOGIC ---
else:
if rsi_val < self.buy_threshold:
candidates.append((symbol, rsi_val))
# 2. Select Top Candidates
candidates.sort(key=lambda x: x[1])
# Calculate available slots
# Note: We re-check portfolio keys because 'active_symbols' might include non-invested stuff
current_invested = [s for s in self.portfolio.keys() if self.portfolio[s].invested]
# Count pending exits to free up slots immediately
pending_exits = 0
for sym, weight in targets.items():
if weight == 0.0 and self.portfolio[sym].invested:
pending_exits += 1
slots_available = self.max_positions - len(current_invested) + pending_exits
for i in range(min(len(candidates), slots_available)):
symbol = candidates[i][0]
targets[symbol] = 2.0 / self.max_positions # Also controls leverage
self.log(f"ENTRY {symbol.value}: RSI {candidates[i][1]:.2f} < {self.buy_threshold}")
# 3. Execution
for symbol, weight in targets.items():
if weight == 0:
self.liquidate(symbol)
# Clean up: If we sold it, and it's NOT in the top 100 anymore,
# we can finally remove it from memory.
# (Simple check: if we just liquidated it, we can try to remove it
# if the universe doesn't want it. For simplicity, we leave it until next universe refresh).
else:
self.set_holdings(symbol, weight)