| Overall Statistics |
|
Total Orders 4 Average Win 0.14% Average Loss 0% Compounding Annual Return 21.924% Drawdown 8.800% Expectancy 0 Start Equity 1000000 End Equity 1161082.31 Net Profit 16.108% Sharpe Ratio 0.671 Sortino Ratio 0.454 Probabilistic Sharpe Ratio 47.306% Loss Rate 0% Win Rate 100% Profit-Loss Ratio 0 Alpha 0.062 Beta 0.697 Annual Standard Deviation 0.158 Annual Variance 0.025 Information Ratio 0.39 Tracking Error 0.11 Treynor Ratio 0.152 Total Fees $15.09 Estimated Strategy Capacity $0 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 0.45% Drawdown Recovery 18 |
# region imports
from AlgorithmImports import *
# endregion
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
class RevisedBuyOnDip(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetEndDate(2025, 1, 1)
self.SetCash(100_000)
self.spy = self.AddEquity("SPY", Resolution.DAILY).Symbol
# Dictionaries to track lots and total allocation per symbol:
# self.lots: key = symbol, value = list of tuples (lot_allocation, entry_price)
self.lots = {}
# self.totalAllocated: key = symbol, value = cumulative allocation percentage
self.totalAllocated = {}
self.UniverseSettings.Resolution = Resolution.DAILY
self._universe = BiggestMarketCapUniverse(self)
self.SetUniverseSelection(self._universe)
# Schedule our daily rebalancing call
self.Schedule.On(self.DateRules.EveryDay(self.spy),
self.TimeRules.AfterMarketOpen(self.spy, 1),
self.Rebalance)
def Rebalance(self):
# 1. Check for profit taking on any held positions
for symbol in list(self.lots.keys()):
# Ensure we have a valid price
if symbol not in self.Securities or self.Securities[symbol].Price == 0:
continue
currentPrice = self.Securities[symbol].Price
lotsToKeep = []
totalReduction = 0.0
# Check each lot for a 5% gain over its entry price
for lotAllocation, entryPrice in self.lots[symbol]:
if (currentPrice - entryPrice) / entryPrice >= 0.05:
totalReduction += lotAllocation
self.Log(f"Liquidating {lotAllocation*100:.1f}% of {symbol} at {currentPrice} (entry was {entryPrice})")
else:
lotsToKeep.append((lotAllocation, entryPrice))
# If any lot qualifies, update the holdings for that symbol
if totalReduction > 0:
newAllocation = self.totalAllocated[symbol] - totalReduction
self.totalAllocated[symbol] = newAllocation
self.SetHoldings(symbol, newAllocation)
if newAllocation == 0:
# Remove symbol entirely if fully liquidated
del self.lots[symbol]
del self.totalAllocated[symbol]
else:
self.lots[symbol] = lotsToKeep
# 2. Universe selection and buying on dip
selectedSymbol = self._universe.last_selected_symbol
if not selectedSymbol:
return
# Add the symbol if it is not already in our Securities
if selectedSymbol not in self.Securities:
self.AddEquity(selectedSymbol, Resolution.DAILY)
# Retrieve recent price history to calculate the price drop
history = self.History(selectedSymbol, 2, Resolution.DAILY)
if history.empty or len(history) < 2:
return
prevClose = history.iloc[-2]['close']
currClose = history.iloc[-1]['close']
priceDrop = (prevClose - currClose) / prevClose
# Check if price drop is significant (>= 5%) and we have capacity to add more allocation.
# We assume a maximum of 99% allocation for any given stock.
currentAlloc = self.totalAllocated.get(selectedSymbol, 0)
if priceDrop >= 0.05 and currentAlloc + 0.09 <= 0.99:
# Reduce our SPY allocation from 100% to 90%
self.SetHoldings(self.spy, 0.9)
# Increase the allocation for the selected stock by 9%
newAllocation = currentAlloc + 0.09
self.SetHoldings(selectedSymbol, newAllocation)
self.totalAllocated[selectedSymbol] = newAllocation
# Record the new purchase as a lot
if selectedSymbol not in self.lots:
self.lots[selectedSymbol] = []
self.lots[selectedSymbol].append((0.09, currClose))
self.Log(f"Bought additional 9% of {selectedSymbol} at {currClose}, total allocation now {newAllocation*100:.1f}%")
class BiggestMarketCapUniverse(FundamentalUniverseSelectionModel):
def __init__(self, algorithm):
super().__init__(True) # True = use dynamic universe selection
self.algorithm = algorithm
self.last_selected_symbol = None
def SelectCoarse(self, algorithm, coarse):
filtered = [x for x in coarse if x.HasFundamentalData and x.Market == Market.USA]
return [x.Symbol for x in filtered]
def SelectFine(self, algorithm, fine):
if not fine:
return []
fine = sorted(fine, key=lambda x: x.MarketCap, reverse=True)
self.last_selected_symbol = fine[0].Symbol
return [self.last_selected_symbol]