| Overall Statistics |
|
Total Trades 575 Average Win 1.61% Average Loss -1.77% Compounding Annual Return 31.576% Drawdown 24.600% Expectancy 0.498 Net Profit 1395.550% Sharpe Ratio 1.026 Probabilistic Sharpe Ratio 47.826% Loss Rate 22% Win Rate 78% Profit-Loss Ratio 0.91 Alpha 0.181 Beta 0.565 Annual Standard Deviation 0.216 Annual Variance 0.047 Information Ratio 0.716 Tracking Error 0.209 Treynor Ratio 0.391 Total Fees $4854.09 Estimated Strategy Capacity $110000000.00 Lowest Capacity Asset LLY R735QTJ8XC9X Portfolio Turnover 6.07% |
from AlgorithmImports import *
class BBandsAlgorithm(QCAlgorithm):
def Initialize(self) -> None:
# backtest settings
self.SetCash(100000)
self.SetStartDate(2014, 1, 1)
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash)
self.lastSellTime = datetime.min
self.BBstd = float(self.GetParameter("stdBB")) # 4
self.BBvalue = float(self.GetParameter("BB")) # 15
self.ROCvalue = float(self.GetParameter("ROC")) # 79
# Initialize an empty dictionary to store last trade times (new addition)
self.lastTradeTimes = {}
# settings
self.EnableAutomaticIndicatorWarmUp = True
self.Settings.FreePortfolioValuePercentage = 0.05
self.UniverseSettings.Resolution = Resolution.Daily
# ETF universe
self.etf = self.AddEquity("SPY", self.UniverseSettings.Resolution).Symbol
self.AddUniverseSelection(
ETFConstituentsUniverseSelectionModel(self.etf, self.UniverseSettings, self.ETFConstituentsFilter))
# Alternative investments
self.alternatives = {
'UUP': self.AddEquity('UUP', self.UniverseSettings.Resolution).Symbol,
'TLT': self.AddEquity('TLT', self.UniverseSettings.Resolution).Symbol,
'GLD': self.AddEquity('GLD', self.UniverseSettings.Resolution).Symbol
}
self.SetBenchmark(self.etf)
self.symbolData = {}
self.universe = []
self.buy_prices = {}
# initialize flag for stop loss triggered
self.stop_loss_triggered = False
def ETFConstituentsFilter(self, constituents: List[ETFConstituentData]) -> List[Symbol]:
# validate in-data
if constituents is None:
return Universe.Unchanged
selected = [c for c in constituents if c.Weight]
sorted_selected = sorted([c for c in selected], key=lambda c: c.Weight, reverse=True)[:(int(self.GetParameter("selected")))] #20
self.universe = [c.Symbol for c in sorted_selected]
return self.universe
def OnOrderEvent(self, orderEvent):
if orderEvent.Status == OrderStatus.Filled:
order = self.Transactions.GetOrderById(orderEvent.OrderId)
symbol = order.Symbol # Extract the symbol from the order
fillPrice = orderEvent.FillPrice # Extract the fill price of the order
# Check if it's a sell order and tag accordingly
if order.Direction == OrderDirection.Sell:
# Tagging for stop loss
if fillPrice <= 0.95 * self.buy_prices.get(symbol, float('inf')):
order.Tag = "STOP LOSS"
# Tagging for profit sell
elif fillPrice >= 1.05 * self.buy_prices.get(symbol, 0):
order.Tag = "5%+ PROFIT"
# Tagging if held for more than 10 days
elif (self.Time - self.lastTradeTimes.get(symbol, datetime.min)).days >= 10:
order.Tag = "HELD FOR 10 DAYS"
# Check if it's a buy order and tag accordingly
if order.Direction == OrderDirection.Buy:
order.Tag = "BUY CONDITIONS MET"
# After tagging, here you may want to update your records such as `lastTradeTimes` or `buy_prices`
if order.Direction == OrderDirection.Buy:
self.buy_prices[symbol] = fillPrice
self.lastTradeTimes[symbol] = self.Time # Record the time of the purchase
elif order.Direction == OrderDirection.Sell and symbol in self.lastTradeTimes:
del self.lastTradeTimes[symbol]
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
# validate in-data
if changes is None:
return
for security in changes.AddedSecurities:
self.symbolData[security.Symbol] = SymbolData(self, security.Symbol)
for security in changes.RemovedSecurities:
self.Liquidate(security.Symbol)
symbolData = self.symbolData.pop(security.Symbol, None)
if symbolData:
symbolData.dispose()
def OnData(self, data: Slice) -> None:
# Check if we are still warming up
if self.IsWarmingUp:
return
if self.Time - self.lastSellTime < timedelta(days=1):
return
# liquidate assets that we should not trade
for symbol in [x.Key for x in self.Portfolio if x.Value.Invested]:
if symbol not in self.universe or not self.Securities[symbol].IsTradable:
self.Liquidate(symbol)
# Time-based Exit
if symbol in self.lastTradeTimes:
holding_period = (self.Time - self.lastTradeTimes[symbol]).days
if holding_period >= 10:
self.Liquidate(symbol, "HELD FOR 10 DAYS")
# After liquidation, it's a good practice to remove the symbol from lastTradeTimes
del self.lastTradeTimes[symbol]
continue
# Calculate the ROC for all symbols and select the max ROC symbol involving only positive ROC values
roc_values = {symbol: self.symbolData[symbol].roc.Current.Value for symbol in self.universe if
symbol in self.symbolData and self.symbolData[symbol].roc.Current.Value > 0}
if roc_values:
max_roc_symbol = max(roc_values, key=roc_values.get)
max_roc = roc_values[max_roc_symbol]
else:
max_roc_symbol = None
max_roc = None
# Check if all ROCs are negative and liquidate/reallocate if necessary
if all(value < 0 for value in roc_values.values()):
self.Liquidate(tag = "Liquidate for New Max ROC")
self.Reallocate()
return
# Implement the Buy Conditions
if max_roc_symbol and self.CanInvest() and max_roc > 0:
price = self.Securities[max_roc_symbol].Price
symbolData = self.symbolData[max_roc_symbol]
if symbolData.bb.MiddleBand.Current.Value < price < symbolData.bb.UpperBand.Current.Value:
quantity = self.CalculateOrderQuantity(max_roc_symbol, 1)
orderTicket = self.MarketOrder(max_roc_symbol, quantity)
orderTicket.UpdateTag(f"BUY CONDITIONS MET")
self.buy_prices[max_roc_symbol] = price
self.lastTradeTimes[max_roc_symbol] = self.Time # Update the last trade time when a new holding is set (new addition)
# Implement the Sell Conditions
for symbol in self.Portfolio.Keys:
price = self.Securities[symbol].Price
if self.Portfolio[symbol].Invested:
# Profit taking
if price >= 1.05 * self.buy_prices.get(symbol, 0):
self.Liquidate(symbol,"5%+ PROFIT")
# It's a good practice to remove the symbol from lastTradeTimes after liquidation
if symbol in self.lastTradeTimes:
del self.lastTradeTimes[symbol]
continue
# Stop Loss
if price <= 0.95 * self.buy_prices.get(symbol, self.Portfolio[symbol].AveragePrice):
self.Liquidate(symbol, "STOP LOSS")
# After liquidation, it's a good practice to remove the symbol from lastTradeTimes
if symbol in self.lastTradeTimes:
del self.lastTradeTimes[symbol]
continue
def CanInvest(self) -> bool:
return sum(1 for x in self.Portfolio if x.Value.Invested) < 10
def Reallocate(self) -> None:
# Calculate ROC for UUP and TLT
roc_uup = self.symbolData[self.alternatives['UUP']].roc.Current.Value
roc_tlt = self.symbolData[self.alternatives['TLT']].roc.Current.Value
# Decide which alternative to buy based on ROC
if roc_uup > 0 or roc_tlt > 0:
if roc_uup > roc_tlt:
quantity = self.CalculateOrderQuantity(self.alternatives['UUP'], 1)
orderTicket = self.MarketOrder(self.alternatives['UUP'], quantity)
orderTicket.UpdateTag(f"UUP ROC > 0")
else:
quantity = self.CalculateOrderQuantity(self.alternatives['TLT'], 1)
orderTicket = self.MarketOrder(self.alternatives['TLT'], quantity)
orderTicket.UpdateTag(f"TLT ROC > 0")
else:
# If both UUP and TLT have negative ROC, invest in GLD
quantity = self.CalculateOrderQuantity(self.alternatives['GLD'], 1)
orderTicket = self.MarketOrder(self.alternatives['GLD'], quantity)
orderTicket.UpdateTag(f"GLD ROC > 0")
# If any equities meet buy conditions, liquidate alternatives
for symbol in self.universe:
if self.Securities[symbol].Invested:
symbol_data = self.symbolData[symbol]
price = self.Securities[symbol].Price
if symbol_data.roc.Current.Value > 0 and symbol_data.bb.MiddleBand.Current.Value < price < symbol_data.bb.UpperBand.Current.Value:
# Liquidate UUP, TLT, and GLD
self.Liquidate(self.alternatives['UUP'], "Liquidating for New Max ROC")
self.Liquidate(self.alternatives['TLT'], "Liquidating for New Max ROC")
self.Liquidate(self.alternatives['GLD'], "Liquidating for New Max ROC")
break # Exit the loop as we only need to liquidate once
class SymbolData(object):
def __init__(self, algorithm, symbol):
self.algorithm = algorithm
self.symbol = symbol
self.stdBB = float(algorithm.BBstd)
self.BBi = int(algorithm.BBvalue)
self.ROCi = int(algorithm.ROCvalue)
# Assuming 'BB' and 'ROC' are methods provided by QuantConnect's QCAlgorithm class
self.bb = algorithm.BB(symbol, self.BBi, self.stdBB, MovingAverageType.Simple, Resolution.Daily)
self.roc = algorithm.ROC(symbol, self.ROCi, Resolution.Daily)
def dispose(self):
# deregister indicators and remove consolidator
pass