| Overall Statistics |
|
Total Trades 25 Average Win 0% Average Loss -2.29% Compounding Annual Return 26.729% Drawdown 31.400% Expectancy -1 Net Profit 50.117% Sharpe Ratio 0.744 Probabilistic Sharpe Ratio 30.070% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha 0.111 Beta 1.457 Annual Standard Deviation 0.326 Annual Variance 0.106 Information Ratio 0.647 Tracking Error 0.235 Treynor Ratio 0.166 Total Fees $118.71 Estimated Strategy Capacity $1900000.00 Lowest Capacity Asset MYGN R735QTJ8XC9X Portfolio Turnover 0.32% |
# region imports
from AlgorithmImports import *
# endregion
# Filter:
# Average daily dollar volume greater than $50 million over the last twenty days.
# Minimum price $5.00. I don't like trading stocks that have a price below $5
# because I believe they can be too volatile, but I do believe I have an
# edge trading stocks below $10, because the institutionals usually are not involved.
# Setup:
# Close of the SPY is above the 100-day simple moving average (SMA).
# This indicates a trend in the overall index.
# The close of the 25-day simple moving average crosses above the close of
# the 50-day simple moving average.
# Ranking:
# In case we have more setups than our position sizing allows,
# we rank by the highest rate of change over the last 200 trading days.
# This means the highest percentage price increase over the last 200 trading days.
# Entry:
# Next day market order on open. I'm not worried about slippage for a long trade,
# and I definitely want to be in.
# Stop-Loss:
# The day after entering the trade, place a stop-loss below the execution price
# of five times the average true range (ATR) of the last twenty days. That will definitely
# keep me outside the daily noise and give the trade room to develop.
# Reentry:
# If stopped out, renter the next day if all entry conditions apply again.
# Profit Protection:
# A trailing stop of 25 percent. This is in addition to the initial stop-loss.
# Eventually, as the stock rises, the trailing stop will move up above the stop-loss price.
# Profit taking:
# No profit target; the goal is to ride this as high as it will go.
# Position sizing:
# 2 percent risk and 10 percent maximum percentage size, with a maximum of ten positions.
class LongTrendHighMomentum(QCAlgorithm):
def Initialize(self):
# self.Debug("Start Init algo")
self.SetStartDate(1999, 1, 2) # Set Start Date
# self.SetStartDate(2023, 1, 2) # Set Start Date
self.SetEndDate(2002, 7, 10) # Set End Date
self.SetCash(100000) # Set Strategy Cash
self.UniverseSettings.Resolution = Resolution.Daily
self.universe = self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.spy = self.AddEquity("SPY", Resolution.Daily)
self.spySma = self.SMA("SPY", 100, Resolution.Daily)
self.WarmUpIndicator("SPY", self.spySma)
self.SetBenchmark("SPY")
self.securities = []
self.coarse_averages = {}
self.market_order_tickets = []
self.AddRiskManagement(TrailingStopRiskManagementModel(0.25))
# self.Schedule.On(self.DateRules.EveryDay("SPY"),
# self.TimeRules.AfterMarketOpen("SPY", 0.0001, False),
# self.DailyCheck)
# self.Debug("Finish Init algo")
def OnData(self, slice):
if self.spy.Close < self.spySma.Current.Value:
return
self.Debug(f"OnData Fired at: {self.Time}")
todays_setups = []
for security in self.securities:
twenty_five_sma = security.twentyFiveSma.Current.Value
fifty_sma = security.fiftySma.Current.Value
if twenty_five_sma > fifty_sma:
todays_setups.append(security)
todays_setups.sort(
key=lambda s: s.twoHundredRoc.Current.Value, reverse=True)
# filter securities in our portfolio we hold only if x.Invested
current_open_positions = [x for x in self.Portfolio.Values if x.Invested]
current_open_positions_symbols = [x.Symbol.Value for x in current_open_positions]
num_setups_to_buy = 10 - len(current_open_positions)
if (num_setups_to_buy) < 1:
return
# log number of setups
self.Log(f"Number of setups: {num_setups_to_buy}")
setups_passed = todays_setups[:num_setups_to_buy]
setups_to_buy = []
for setup_passed in setups_passed:
symbol_value = setup_passed.Symbol.Value
if symbol_value not in current_open_positions_symbols:
setups_to_buy.append(setup_passed)
for setup in setups_to_buy:
current_price_of_setup = setup.Price
stop_price = current_price_of_setup - \
(setup.twentyAtr.Current.Value * 5)
risk_per_share = current_price_of_setup - stop_price
# Calculate portfolio risk per trade
portfolio_risk_per_trade = self.Portfolio.TotalPortfolioValue * 0.02
# Calculate shares to buy, round down to nearest integer
total_shares = portfolio_risk_per_trade / risk_per_share // 1
# Check if total_shares exceeds 10% of portfolio, if so, adjust total_shares
if total_shares * current_price_of_setup > self.Portfolio.TotalPortfolioValue * 0.1:
total_shares = (self.Portfolio.TotalPortfolioValue * 0.1) // current_price_of_setup
if total_shares > 0:
ticket = self.MarketOrder(setup.Symbol, total_shares)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
# self.Log("OnSecuritiesChanged:")
# self.Debug(f"Start OnSecuritiesChanged")
for security in changes.AddedSecurities:
# self.Debug(f"Adding {security.Symbol.Value} to securities")
# Don't add spy to tradeable securities
if (security != self.spy):
# check if security exists in averages
security.twentyFiveSma = self.SMA(security.Symbol, 25)
security.fiftySma = self.SMA(security.Symbol, 50)
security.twoHundredRoc = self.ROC(security.Symbol, 200)
security.twentyAtr = self.ATR(security.Symbol, 20)
self.WarmUpIndicator(
security.Symbol, security.twentyFiveSma)
self.WarmUpIndicator(
security.Symbol, security.fiftySma)
self.WarmUpIndicator(
security.Symbol, security.twoHundredRoc)
self.WarmUpIndicator(
security.Symbol, security.twentyAtr)
self.securities.append(security)
for security in changes.RemovedSecurities:
if security in self.securities:
self.DeregisterIndicator(security.twentyFiveSma)
self.DeregisterIndicator(security.fiftySma)
self.DeregisterIndicator(security.twoHundredRoc)
self.DeregisterIndicator(security.twentyAtr)
self.securities.remove(security)
self.securities = [s for s in self.securities if not s.IsDelisted]
# self.Debug(f"End OnSecuritiesChanged")
def OnOrderEvent(self, orderEvent: OrderEvent) -> None:
order = self.Transactions.GetOrderById(orderEvent.OrderId)
if orderEvent.Status == OrderStatus.Invalid:
x = 1
if orderEvent.Status == OrderStatus.Filled:
# self.pending_tickets = [x for x in self.pending_tickets if x.Symbol.Value != order.Symbol.Value]
# Stop market order is 5*atr below execution price
holdings_quantity = self.Portfolio[order.Symbol].AbsoluteQuantity
self.Log(f"portfolio size = ")
if holdings_quantity > 0:
average_price = self.Portfolio[order.Symbol].AveragePrice
# self.Debug(f"OrderStatus.Filled: {order.Symbol.Value} holdings_quantity {holdings_quantity} average_price {average_price}")
twenty_day_atr = self.ATR(order.Symbol, 20)
self.WarmUpIndicator(order.Symbol, twenty_day_atr)
twenty_day_atr_value = twenty_day_atr.Current.Value
stop_price = average_price - 5 * twenty_day_atr_value
self.StopMarketOrder(
order.Symbol, -holdings_quantity, stop_price)
def CoarseSelectionFunction(self, coarse):
selected = []
# some initial screening filter
universe = [x for x in coarse if x.Price > 5]
# self.Debug("start coarse selection function")
for coarse in universe:
symbol = coarse.Symbol
if symbol not in self.coarse_averages:
history = self.History(symbol, 20, Resolution.Daily)
self.coarse_averages[symbol] = SelectionData(history)
self.coarse_averages[symbol].update(self.Time, coarse.DollarVolume)
dollarVolumeAvg = self.coarse_averages[symbol].dollarVolumeAvg.Current.Value
if self.coarse_averages[symbol].is_ready() and dollarVolumeAvg >= 50_000_000 and coarse.HasFundamentalData:
selected.append(symbol)
# self.Debug("end coarse selection function")
return selected
def FineSelectionFunction(self, fine):
# self.Debug(f"Start FineSelectionFunction")
filtered = []
for security in fine:
if security.SecurityReference.ExchangeId == "NAS" or security.SecurityReference.ExchangeId == "NYS" or security.SecurityReference.ExchangeId == "ASE":
filtered.append(security.Symbol)
# self.Debug(f"End FineSelectionFunction")
return filtered
class SelectionData():
# 3. Update the constructor to accept a history array
def __init__(self, history):
self.dollarVolumeAvg = SimpleMovingAverage(20)
# 4. Loop over the history data and update the indicators
for bar in history.itertuples():
volume = getattr(bar, 'volume', 0)
close = getattr(bar, 'close', 0)
dollar_volume = volume * close if volume and close else 0
self.dollarVolumeAvg.Update(bar.Index[1], dollar_volume)
def is_ready(self):
return self.dollarVolumeAvg.IsReady
def update(self, time, value):
self.dollarVolumeAvg.Update(time, value)