Quant BuffetRelax, Not Over Thinking

All-Time High Breakout with ATR Trailing Stop

Log in to collect

Academic paper

Strategy in a nutshell

The investment universe consists of US-listed companies. A minimum stock price filter is used to avoid penny stocks, and a minimum daily liquidity filter is used to avoid stocks that are not liquid enough. The entry signal occurs if today’s close is greater than or equal to the highest close during the stock’s entire history. A 10-period average true range trailing stop is used as an exit signal. The investor holds all stocks which satisfy the entry criterion and are not stopped out. The portfolio is equally weighted and rebalanced daily. Transaction costs of 0.5% round-turn are deducted from each trade to account for estimated commission and slippage.

Economic rationale

Behavioral biases (investors herding, under- and over-reaction, etc.) create a non-normal return distribution on financial markets. Trend-following systems cut the left tail of the long-tail distribution. This characteristic creates improved risk/return characteristics of trend-following systems when compared to a diversified buy&hold approach.

Backtest performance

Annualised return19.3%
Volatility15.6%
Beta0.881
Sharpe ratio0.625
Sortino ratio0.638
Maximum drawdown34.1%
Win rate59%

Full Python code

import numpy as np
from AlgoLib import *

class TrendFollowingEffectinStocks(XXX):

def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)

self.fundamental_count:int = 100
self.fundamental_sorting_key = lambda x: x.DollarVolume

self.long:List[Symbol] = []

self.max_close:Dict[Symbol, float] = {}
self.atr:Dict[Symbol, AverageTrueRange] = {}
self.atr_period:int = 10

self.sl_order:Dict[Symbol, OrderTicket] = {}
self.sl_price:Dict[Symbol, float] = {}

self.selection:List[Symbol] = []
self.period:int = 10*12*21
self.min_share_price:float = 5.

self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.

def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    
    symbol = security.Symbol
    if symbol not in self.atr:
        self.atr[symbol] = self.ATR(symbol, self.atr_period, Resolution.Daily)
        
    if symbol not in self.max_close:
        hist = self.History([self.Symbol(symbol)], self.period, Resolution.Daily)
        if 'close' in hist.columns:
            closes:pd.Series = hist['close']
            self.max_close[symbol] = max(closes)
    
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> None:
selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.AdjustedPrice >= self.min_share_price]

if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]

self.selection = list(map(lambda x: x.Symbol, selected))

return self.selection

def OnData(self, data: Slice) -> None:
if self.IsWarmingUp:
    return

for symbol in self.selection:
    if symbol in data.Bars:
        price:float = data[symbol].Value
    
        if symbol not in self.max_close: continue
    
        if price >= self.max_close[symbol]:
            self.max_close[symbol] = price
            self.long.append(symbol)

stocks_invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
count:int = len(self.long) + len(stocks_invested)
if count == 0: return

# Update stoploss orders
for symbol in stocks_invested:
    if not self.Securities[symbol].IsTradable:
        self.Liquidate(symbol)
        
    if self.atr[symbol].Current.Value == 0: continue
    
    # Move SL
    if symbol not in self.sl_price: continue
    
    self.SetHoldings(symbol, 1 / count)
    
    new_sl = self.Securities[symbol].Price - self.atr[symbol].Current.Value
    if new_sl > self.sl_price[symbol]:
        update_order_fields = UpdateOrderFields()
        update_order_fields.StopPrice = new_sl      # Update SL price
        
        quantity:float = self.CalculateOrderQuantity(symbol, (1 / count))
        update_order_fields.Quantity = quantity     # Update SL quantity

        self.sl_price[symbol] = new_sl
        self.sl_order[symbol].Update(update_order_fields)

# Open new trades
for symbol in self.long:
    if not self.Portfolio[symbol].Invested and self.atr[symbol].Current.Value != 0:
        price:float = data[symbol].Value
        if self.Securities[symbol].IsTradable:
            unit_size:float = self.CalculateOrderQuantity(symbol, (1 / count))
            
            self.MarketOrder(symbol, unit_size)
            
            sl_price:float = price - self.atr[symbol].Current.Value
            self.sl_price[symbol] = sl_price
            if unit_size != 0:
                self.sl_order[symbol] = self.StopMarketOrder(symbol, -unit_size, sl_price, 'SL')

self.long.clear()

# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))