Back to Community
Trailing Stop-Loss

I did some searches, but no dice. Does anyone have a UDF / stand-alone Trailing Stop-Loss function they created?

I want to compare it against mine for logic purposes.

Thanks

27 responses

I tossed together this simple version. The algo buys once at the beginning of each month and waits for a trailing stop signal set at 95% of the highest price seen since the position was taken.

Clone Algorithm
316
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a1bf0568222d090791cc3b
There was a runtime error.

Another take on a trailing stop exit. This one with a simple breakout entry logic.

This one uses the STD of recent close prices to buffer the trailing stop. This is a variation on the ATR trailing stop.

[Edit... If you swap in the following settings and the below security list you can get much better results.

STDMultiplier = 5.0  
Periods = 100  
def initialize(context):  
    context.Stocks = symbols("XLB","XLE","XLF","XLI","XLK","XLP","XLU","XLV","XLY","AGG")  

and below:

    record(Stop1=context.StopPrices[context.Stocks[0]], Price1=historic["close"][context.Stocks[0]][-1],  
           Stop2=context.StopPrices[context.Stocks[1]],  
           Stop3=context.StopPrices[context.Stocks[2]],  
           Stop4=context.StopPrices[context.Stocks[3]]  
          )  
Clone Algorithm
45
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a2f499d50f9d0911c84c4e
There was a runtime error.

Thanks a lot for this example. I often use trailing stop loss. I am new to Quantopian and this is my first attempt on modifying a code:) I changed to buying when the price > 30 days mavg and changed stop loss to 10%, because this resembles quite closely my 'manual' strategy, when investing in indices. Here, I hope for downside protection, while not missing too much of the rebound.

Clone Algorithm
470
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a566c153dbab28349edc2e
There was a runtime error.

This is the same algorithm run on a minute basis. Here, the performance is worse than the benchmark. On the daily basis it was selling 11 times, while selling 17 times on the minute basis, meaning that it was loosing a lot on selling on smaller corrections, due to wild intraday swings? Or is there something weird on the algo on the daily basis? Is the lesson here that intraday variation should be ignored??

Clone Algorithm
470
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a56b471a78061c2ac5ee05
There was a runtime error.

Klaus, in backtesting, orders are submitted in one bar and then filled in the next bar (subject to your fill logic and slippage). The difference between minute and daily mode is how often the data is queried.

In minute mode, the data is sampled every minute and closely simulates live trading. If you submit an order in one minute bar it will be filled in the next minute.

In daily mode, orders are submitted at the end of the day and are filled at the next day's close price. This was a conservative measure to avoid leaking bias into the algorithm. Though more conservative, it exposes more fluctuations in the fill price.

I'd suggest to code your algo in minute mode and use schedule_function to run things once per day or at another specified frequency. Try it out!

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

Thanks for the reply. The coding is new to me, so I will look into this and I saw that there are quite some posts on this subject. I still find it quite weird/intriguing if there is such a big difference in performance, by only considering the closing price and waiting until next day with selling. Maybe the price often bounds back the following day after a sharp decline, so the price is better??

Here is the same run using the schedule function in order to only consider the closing_price. It is probably best to avoid the intraday 'noise' for long term investors.

Clone Algorithm
470
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a7a6642bf144090f855087
There was a runtime error.

Here is the final attempt (hopefully not too over-optimized). It is of course difficult to make a strategy that works well both during the bear and the bull markets, as also mentioned by others. Therefore, the following example is a combination of two stop losses. When the market trend up (200 days mavg > 400 days mavg) then the entry point is for the closing price to go above the 50-day mavg. When the trend is down (200 days mavg < 400 days mavg) then the entry point is for the price to pass through the 100 days mavg. Hence the criteria for entering the market is more relaxed during a bull market than a bear market, which should make sense for a long-only strategy. The stop loss is 10% in both cases bases on the closing price. It seems to perform quite well for other of the large indices, as well. Max draw down was 16%.

Clone Algorithm
470
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a96885fbf274094ba0ae89
There was a runtime error.

Simple yet effective. Funny though that trading started before a full 400 trading days had transpired no? One would think that a full 400 days would be required to build a 400 day MA right?

Here's another test that's fun to do. Run a rolling year test and compare the results. Run 2004 only, then 2005, etc. And collect the individual years results. The issue with using order_target_percent, I've found, is that it might be unrealistic; compounding a good year on top of a a so-so year makes even the so-so year exceptional . But starting over fresh every year gives you a sense of what you could expect if you start trading right now -- and what you might see in a years time.

You can also look at the rolling 12 month returns in your full back test, try averaging those out to see a more realistic future return.

Thanks for the comments. Yes, it is a little strange at the beginning? It is a good idea to test a year at a time to get a more realistic picture for future returns, when starting from scratch.

I tested it on several indices. Something a little weird is that it works good on Russell 1000, but not so good on Russell 2000. Maybe Russell 2000 is too volatile?

The moving average is built dynamically (and a recent feature upgrade!). On day 1 of the backtest you'll have a full mavg available because it queries for historical data before the backtest's start date. This way you won't have to wait for the 200 or 400 day window to be filled during the backtest.

That must mean that the Q has data prior to 1/3/2002 then? From Klaus's test where trading started 11/4/2002 minus 400 trading days... let's see 251 backwards would be 11/4/2001, and another 149 would be ~4/4/2001 yes? So there is data back to 4/4/2001 (or there abouts if we believe Klaus's strat traded at the soonest opportunity)?

I had missed the backtest start date! You're right that we don't have data before Jan 3 2002, this will be the first date that data is available in the algorithm. Looks like something else might be lurking in the beginning of the algorithm's backtest.

Sorry for being MIA work has been quite busy. I was thinking something more intraday for momentum strategies. I codded the following on my desktop not inside your API:

import sys as s  
############################  
#  Trailing Stop-loss Test #  
############################

l_price = c_price = 35.4  
s_loss_pct = 0.05  
t_s_l = c_price * ( 1 - s_loss_pct )

print "[+] Current stock price: %0.2f" % c_price  
print "[+] Trailing stop-loss percent set for: %0.2f%%" % (s_loss_pct * 100)

def trailing_stop_loss( price ):  
    global t_s_l, l_price, s_loss_pct  
    #check to see if price has already passed our stop-loss if so, sell.  
    if price <= t_s_l:  
        print "[!]Selling because: %0.2f <= %0.2f" % (price, t_s_l)  
        s.exit()  
    elif price <= l_price:  
        l_price = price  
        return  
    elif price >= l_price and price * (1 - s_loss_pct ) > t_s_l:  
        #update trailing  
        t_s_l = price * (1 - s_loss_pct )  
        l_price = price  
        return  

while 1:  
    print "[+] Current trailing stop-loss set to: %0.2f" % t_s_l  
    #manually simulate stock market price movements  
    price = raw_input( "Enter a new price >" )

    # have to type-cast input so not treated as string  
    trailing_stop_loss( float( price ) )  

Maybe something like the attached? Just threw it together very fast -- let me know if there is a more efficient way to code this and integrate it into different algos.

Clone Algorithm
6
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54b3112c1478204c64dcf961
There was a runtime error.

Trailing trades code for multiple securities. (Figured this might be the better place to post this)
These are geared to be able to easily replace order_target_percent() in existing code to try them out.

def handle_data(context, data):  
    for stock in data:  
        [ some conditions ]  
            #order_target_percent(stock, .5)  
            TrailingBuy(stock, .5, context, data)  
        else:  
            #order_target_percent(stock, 0)  
            TrailingSell(stock, 0, context, data)

def TrailingBuy(sec, percent, c, data):  # Trailing Stop for Buy on dn-then-up movement  
    b_stop_ratio = 1.06   # Ratio above low trigger to buy.  
    if sec not in data: return  
    if 'books' not in c: c.books = {}  
    log_it = 0  
    sym    = sec.symbol  
    if sym not in c.books:  
        c.books[sym] = {}  
    bsym   = c.books[sym]  
    price  = data[sec].price  
    keyy   = price   # Could act on moving average for example, or other  
    b_stop = b_stop_ratio * keyy  
    if 'trailingBuy' not in bsym:  
        bsym['trailingBuy'] = b_stop        # The marker line  
        return

    if keyy > bsym['trailingBuy']:  
        order_target_percent(sec, percent)  # Buy

    elif b_stop < bsym['trailingBuy']: # If lower  
        if log_it:  
            log.info('{} Lower trailingBuy from {} to {}'.format(  
                sym, bsym['trailingBuy'], b_stop))  
        bsym['trailingBuy'] = b_stop   # Adjust trailingBuy lower with dn movement.

def TrailingSell(sec, percent, c, data):  # Trailing Stop for Sell on up-then-dn movement  
    s_stop_ratio = .94    # Ratio below high trigger to sell.  
    if sec not in data: return  
    if 'books' not in c: c.books = {}  
    log_it = 0  
    sym    = sec.symbol  
    if sym not in c.books:  
        c.books[sym] = {}  
    bsym   = c.books[sym]  
    price  = data[sec].price  
    keyy   = price   # Could act on moving average for example, or other  
    s_stop = s_stop_ratio * keyy  
    if 'trailingSell' not in bsym:  
        bsym['trailingSell'] = s_stop        # The marker line  
        return

    if keyy < bsym['trailingSell']:  
        order_target_percent(sec, percent)   # Sell

    elif s_stop > bsym['trailingSell']: # If lower  
        if log_it:  
            log.info('{} Increase trailingSell from {} to {}'.format(  
                sym, bsym['trailingSell'], s_stop))  
        bsym['trailingSell'] = s_stop   # Adjust trailingSell higher with up movement.

This would fail if the stock split, right?

As anything relying on price would, more generally, no?

I posted a function that should help with drastic price changes including splits.
https://www.quantopian.com/posts/dealing-with-stock-splits

Only if one stores prices in context. More stateless algos so not have this problem...

If one has a way that everyone can be safe in splits always, perhaps they can share the knowledge, rather than just saying it is so.
Remember to address the instances reported here where adjustments fell thru the cracks.

Trailing stops are not possible to implement safely given the information our algos have access to; I don't have a secret way to do them better, I just avoid them now. Note that stops based on breaking below 'donchian' or bollinger bands can be implemented correctly, given correctly split adjusted data from Quantopian in history.

If there are data errors from Quantopian, then I don't believe anything can be implemented both safely and correctly, and we must just hope that the errors cancel out in the end.

Despite any strategy which I may have posted here in the past, I believe the only safe way to perform threshold trading is to forego retaining state of any sort and do "groundhog" style analysis and execution. The groundhog knows nothing about the environment outside of its burrow. It must pop its head up out of its hole and take a look around gathering all the data it may need to decide whether to forage or go back to bed. It remembers nothing. Its decision is completely based upon what it finds at the time of its query.

With this in mind one would have to recalculate all metrics required to trade -- on every periodic (or event based) opportunity. This means for a trailing stop, one would have to redetermine one's entry price (maybe it changed since the last period due to splits or other actions), re-perform the trailing % or ticks or time or threshold (Boll, Kelt, MA, etc.) calculations and then make ones execution decision. This seems counter intuitive at times since you're essentially redoing all this work that you just did a minute ago, over and over and over. But external circumstances force you to assume the world has changed while your algo was asleep and that you must reestablish your baseline metrics anew. I ran into this many times in my prior career and eventually learned that I must assume I knew nothing at the roll of the minute or day or at the tick or execution event; wake up, look around, gather info and make a decision.

Market Tech - I agree completely. Stateless is best.

It just occurred to me that you could do this sort of thing by calculating the split/dividend adjustment ratios by doing a Last and PrevLast pipeline every day, and dividing today's PrevLast by yesterday's Last (stored in context). Then you could adjust all your stops every mornings...

Simon - I think what you outline above, is similar to what I did here to identify when splits or dividends have happened. I was using it for debugging, but I expect it has other uses as well.

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

Ah yes precisely - this information could be used to adjust all price values that someone has stored in context, for re-entering trailing stops or profit target orders at the same spot as yesterday, despite any splits of dividends overnight.

So nobody is actually using stops, you just simulate them? My WIP version of trailing stops is below. I am actually modifying stop orders here, assuming that they are re-instated at market open (which is another thing I need to write). Note - this function assumes that all open orders are stops.

It uses the fixed percent of the price to trail, but I am working on storing the desired loss $ per entry order and using that instead.

Question - does Q update position.amount and and position.cost_basis during spits? (I mean for overnight positions of course)

# Returns the list of strings representing a log of trail events  
def trail_stops(context, close_prices):  
    trail_log = []  
    open_orders_by_security = get_open_orders()  
    for security, open_orders_for_security in open_orders_by_security.iteritems():  
        assert len(open_orders_for_security) <= 1, "Expected 0 or 1 open orders for security " + security.symbol + " but got: " + str(len(open_orders_for_security)) + ", orders: " + print_orders(open_orders_for_security)  
        for open_order in open_orders_for_security:  
            if open_order.status == 0: # 0 = Open  
                current_price = close_prices[open_order.sid][-1]  
                assert open_order.amount != 0, "open_order amount is not expected to be zero"  
                isProtectingLong = open_order.amount < 0 # stop = sell  
                optimum_stop_price = current_price * (1 + context.stop_loss_trailing_fraction * (-1 if isProtectingLong else 1))

                need_to_move_stop_order = optimum_stop_price > open_order.stop if isProtectingLong else optimum_stop_price < open_order.stop  
                if need_to_move_stop_order:  
                    o_sid = open_order.sid  
                    o_amount = open_order.amount  
                    trail_log.append("Moving %d/%d %s stop from %s to %s" % (open_order.filled, open_order.amount, open_order.sid.symbol, print_nullable_money(open_order.stop), print_nullable_money(optimum_stop_price)))  
                    cancel_order(open_order) # order is not callable after cancelling  
                    order(o_sid, o_amount, style=StopOrder(stop_price = optimum_stop_price))  
                else:  
                    trail_log.append("Not moving stop: %s, opt price %s, curr price %s" % (print_nullable_money(open_order.stop), print_nullable_money(optimum_stop_price), print_nullable_money(current_price)))  
    return trail_log

def print_nullable_money(number):  
    return ("%.2f" % number) if number is not None else "null"

def print_order(order):  
   return "Status: %d, filled: %d out of %d, stop: %s, limit: %s, created: %s" % (order.status, order.filled, order.amount, print_nullable_money(order.stop), print_nullable_money(order.limit), order.created)

def print_orders(orders):  
    result = ""  
    for o in orders:  
        result += print_order(o) + ", "  
    return result