stop-limit order - how does it work?

I'm trying to sort out how a stop-limit order works, by example (see attached). No matter how I set the stop & limit prices, I can't seem to trigger the order. The order object indicates that the stop was never reached? What am I missing? Or is the stop-limit order not working properly?

Grant

14
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
def initialize(context):
pass

def handle_data(context, data):

print data[sid(24)].price

order_id = order(sid(24), 50, style=StopLimitOrder(limit_price=100, stop_price=99.99))

if get_open_orders():
print get_order(order_id)


There was a runtime error.
15 responses

Grant,

Stop-limit orders can definitely be a little confusing at first glance. What's happening here is that your algorithm does actually place orders on the 25th for a 100 shares of AAPL (2 separate orders). The order object is indicating that the stop was never reached when it actually was because in this case, the stop-limit order causes the order to immediately be converted into a limit order when the stop price is reached(this also means that the 'stop_reached' boolean will always be False but the 'limit_reached' will change according to the appropriate conditions). And because in backtesting, orders aren't cancelled at the end-of-day, the now converted limit order is carrying through day-over-day; hence the orders executing on the 25th, a few days after you first placed the order.

Basically, this means that your first two orders have automatically been converted into limit orders with a limit price of $100.00 and that's why the 'stop_reached' boolean is always False. In the backtest that I've attached we can see that on the 25th, the first two orders placed on the 22nd and 23rd have reached their respective limit prices and so the orders have been filled on that day. Also, because orders are filled on the next bar, the current order (and the stop condition, given this was a regular stop order) won't be filled until the next day so you could, if you wanted, store the id in 'context' like def initialize(context): context.past_id = None def handle_data(context, data): if context.past_id: print get_order(context.past_id) order_id = order(sid(24), 50, style=StopLimitOrder(limit_price=100, stop_price=99.99)) if get_open_orders(): context.past_id = order_id  6 Loading... 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 def initialize(context): context.past_ids = [] def handle_data(context, data): print "These are all the orders up till %s" % get_datetime() for i in context.past_ids: print get_order(i) order_id = order(sid(24), 50, style=StopLimitOrder(limit_price=100, stop_price=99.99)) if get_open_orders(): context.past_ids.append(order_id)  There was a runtime error. 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. Hi Seong, I guess I don't yet understand completely. Are you using OHLC values to determine if the stop is reached? I can't understand why the limit order resulted when the closing price stayed above the stop price of 99.99? Also, what is the point of the 'stop_reached' flag if it is not used? Shouldn't you be retaining the stop price and setting the stop_reached flag to True? I've attached an update to my backtest. Grant The log output: 2014-09-22PRINT101.07 2014-09-22PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': 99.99, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 100.0, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': '9bc9b28c2d9647518168ca4e9aa8da43', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) 2014-09-23PRINT102.64 2014-09-23PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 100.0, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': '9bc9b28c2d9647518168ca4e9aa8da43', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) 2014-09-24PRINT101.74 2014-09-24PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 100.0, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': '9bc9b28c2d9647518168ca4e9aa8da43', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) 2014-09-25PRINT97.87 2014-09-25PRINTEvent({'status': 1, 'limit_reached': True, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': 1.5, 'amount': 50, 'limit': 100.0, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': '9bc9b28c2d9647518168ca4e9aa8da43', 'dt': datetime.datetime(2014, 9, 25, 0, 0, tzinfo=), 'filled': 50}) 2014-09-26PRINT100.73 2014-09-26PRINTEvent({'status': 1, 'limit_reached': True, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': 1.5, 'amount': 50, 'limit': 100.0, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': '9bc9b28c2d9647518168ca4e9aa8da43', 'dt': datetime.datetime(2014, 9, 25, 0, 0, tzinfo=), 'filled': 50}) End of logs.  14 Loading... 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 def initialize(context): context.order_placed = False context.order_id = None def handle_data(context, data): print data[sid(24)].price if not context.order_placed: context.order_id = order(sid(24), 50, style=StopLimitOrder(limit_price=100, stop_price=99.99)) context.order_placed = True if context.order_id != None: print get_order(context.order_id)  There was a runtime error. Seong, Here's a variant--still confusing. The stop is reached, but then there is no buy at the limit. It appears that only closing prices are used, correct? Grant 2014-09-22PRINTOHLC 2014-09-22PRINT101.8 2014-09-22PRINT102.14 2014-09-22PRINT100.58 2014-09-22PRINT101.07 2014-09-22PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': 101.0, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 97.72, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': 'b7eca32b0bba43d4b3baf9c2807a6668', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) 2014-09-23PRINTOHLC 2014-09-23PRINT100.62 2014-09-23PRINT102.9474 2014-09-23PRINT100.54 2014-09-23PRINT102.64 2014-09-23PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 97.72, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': 'b7eca32b0bba43d4b3baf9c2807a6668', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) 2014-09-24PRINTOHLC 2014-09-24PRINT102.16 2014-09-24PRINT102.85 2014-09-24PRINT101.2 2014-09-24PRINT101.74 2014-09-24PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 97.72, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': 'b7eca32b0bba43d4b3baf9c2807a6668', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) 2014-09-25PRINTOHLC 2014-09-25PRINT100.51 2014-09-25PRINT100.71 2014-09-25PRINT97.72 2014-09-25PRINT97.87 2014-09-25PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 97.72, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': 'b7eca32b0bba43d4b3baf9c2807a6668', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) 2014-09-26PRINTOHLC 2014-09-26PRINT98.53 2014-09-26PRINT100.74 2014-09-26PRINT98.4 2014-09-26PRINT100.73 2014-09-26PRINTEvent({'status': 0, 'limit_reached': False, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': None, 'amount': 50, 'limit': 97.72, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': 'b7eca32b0bba43d4b3baf9c2807a6668', 'dt': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'filled': 0}) End of logs.  14 Loading... 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 def initialize(context): context.order_placed = False context.order_id = None def handle_data(context, data): print 'OHLC' print data[sid(24)].open_price print data[sid(24)].high print data[sid(24)].low print data[sid(24)].close_price if not context.order_placed: context.order_id = order(sid(24), 50, style=StopLimitOrder(limit_price=97.72, stop_price=101)) context.order_placed = True if context.order_id != None: print get_order(context.order_id)  There was a runtime error. Grant, Great questions! To answer your first one: Yes, the close_price is being used to calculate a security has reached its stop/limit price. As for the rest of the questions, I'll try to clear up everything but I'm sure this is going to be an ongoing discussion so feel free to keep asking more. I can't understand why the limit order resulted when the closing price stayed above the stop price of 99.99?  I can totally see why you're confused. At first glance it seems like the stop and limit prices should create upper and lower limits of sorts, but when you look at how stop-limit orders really work you see that that's not the case. Stop-limit orders behave like this: let's say a stock is at$40.00 and you set the stop price to be $45.00 and the limit price to be$50.00. Once the stock crosses the $45.00 stop price, it's converted into a limit order. This means that as long as the stock's price is below$50.00, the limit order is good to go. So it's in two steps, each independent of each other where the limit order doesn't know about the stop order of $45.00; it only knows that as long as the stock is below$50.00, the limit order can be executed. This is why the trades were executed on the 25th since 97.87 is well below the limit price, and the stop price (99) was crossed on multiple days (101, 102, etc.,). And the logs show that the only orders that were executed were orders that were placed on the 25th, not the 26th (both orders have the same id and the creation date is the 25th). Ideally in the above example, the stock's price would remain above 45 and below 50, giving you the butterfly bounds that you originally hoped for, but in some cases that doesn't happen. 2014-09-25PRINT97.87 2014-09-25PRINTEvent({'status': 1, 'limit_reached': True, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': 1.5, 'amount': 50, 'limit': 100.0, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': '9bc9b28c2d9647518168ca4e9aa8da43', 'dt': datetime.datetime(2014, 9, 25, 0, 0, tzinfo=), 'filled': 50}) 2014-09-26PRINT100.73 2014-09-26PRINTEvent({'status': 1, 'limit_reached': True, 'created': Timestamp('2014-09-22 00:00:00+0000', tz='UTC'), 'stop': None, 'reason': None, 'stop_reached': False, 'commission': 1.5, 'amount': 50, 'limit': 100.0, 'sid': Security(24, symbol='AAPL', security_name='APPLE INC', exchange='NASDAQ GLOBAL SELECT MARKET', start_date=datetime.datetime(1993, 1, 4, 0, 0, tzinfo=), end_date=datetime.datetime(2014, 9, 30, 0, 0, tzinfo=), first_traded=None), 'id': '9bc9b28c2d9647518168ca4e9aa8da43', 'dt': datetime.datetime(2014, 9, 25, 0, 0, tzinfo=), 'filled': 50}) End of logs.  And to answer this question:  Also, what is the point of the 'stop_reached' flag if it is not used? Shouldn't you be retaining the stop price and setting the stop_reached flag to True?  Because the behavior is to convert the stop-limit order directly into a limit_order, the backtester treats the order as a strict limit order and not a hybrid order. This is why the 'stop' quickly becomes 'None' in many of the logs. This is definitely an ongoing discussion and the backtester's behavior is very different from the live tester's in this case because all orders in live testing are cancelled at the end of the day, reducing the probability that something like this could happen. And because the ticks of each bar are day over day, the big fluctuations in the price might have led to this as well. I hope I've answered your questions, but if not, feel free to keep on asking more! Seong Thanks Seong, I found the relevant zipline code: The logic for the stop-limit order is: if order_type == BUY | STOP | LIMIT: if event.price >= order.stop: sl_stop_reached = True if event.price <= order.limit: limit_reached = True elif order_type == SELL | STOP | LIMIT: if event.price <= order.stop: sl_stop_reached = True if event.price >= order.limit: limit_reached = True  I also sorted out that there are two separate boolean flags, stop_reached and sl_stop_reached. As best I can tell, stop_reached applies to stop orders, while sl_stop_reached is used for stop-limit orders. A call to get_order(order_id) only shows stop_reached; sl_stop_reached is not accessible. The code that converts the stop order into a limit order is: if sl_stop_reached: # Change the STOP LIMIT order into a LIMIT order self.stop = None  The problem I see is that the stop-limit order literally converts into a limit order, with no record of the stop value and no indication that the stop was reached. My sense is that rather than converting to a limit order, the stop-limit order should remain (with a stop value and stop_reached = True), and just become executable as a limit order. Grant Grant, That's awesome you found the code! 'sl_stop_reached' and 'stop_reached' are independent in order to preserve distinct 'stop' and 'limit' orders but you bring up a good point; I'm looking IB's description of the stop-limit order and they seem to convert the stop-limit order directly into a limit order but it doesn't specify whether it executes within the boundaries of both the stop and limit order instead of just the limit order. We'll definitely have to dig a little deeper into it. Thank you for bringing it up! Seong Song, The logic in the zipline code I posted above seems correct (see http://financial-dictionary.thefreedictionary.com/Stop-Limit+Order, for example). Grant Grant, Yeah, we double checked with IB to make sure and everything checks out... and it does!! Good to go! Seong Updated track_orders for StopLimit orders. It can help turn the lights on. 2016-01-14 06:31 _orders:65 INFO 1 Sell -1 SPY now 189.67 stop 189.63 limit 191.19 cash 1000 2c31 2016-01-14 07:36 _orders:65 INFO 66 Sold -1 SPY limit at 191.24 cash 1190 2c31 2016-01-15 06:31 _orders:65 INFO 1 Sell -1 SPY now 186.80 stop 186.75 limit 188.29 cash 1190 ea33 2016-01-15 13:00 WARN Your order for -1 shares of SPY failed to fill by the end of day and was canceled. 2016-01-19 06:31 _orders:65 INFO 1 Canceled Sell -1 SPY limit at 189.95 2016-01-19 06:31 _orders:65 INFO 1 Sell -1 SPY now 189.95 stop 189.91 limit 191.47 cash 1190 3781 2016-01-19 13:00 WARN Your order for -1 shares of SPY failed to fill by the end of day and was canceled. 2016-01-20 06:31 _orders:65 INFO 1 Canceled Sell -1 SPY limit at 185.13 2016-01-20 06:31 _orders:65 INFO 1 Sell -1 SPY now 185.13 stop 185.09 limit 186.62 cash 1190 ed1b 2016-01-20 12:21 _orders:65 INFO 351 Sold -1 SPY limit at 187.00 cash 1376 ed1b 2016-01-21 06:31 _orders:65 INFO 1 Sell -1 SPY now 185.82 stop 185.78 limit 187.31 cash 1376 c6dd 2016-01-21 07:21 _orders:65 INFO 51 Sold -1 SPY limit at 187.32 cash 1562 c6dd  20 Loading... 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 ''' StopLimit order examples using track_orders, https://www.quantopian.com/posts/track-orders For https://www.quantopian.com/posts/limit-slash-stop-orders-for-short-selling ''' def initialize(context): context.spy = sid(8554) schedule_function(short, date_rules.every_day(), time_rules.market_open()) def short(context, data): price = data.current(context.spy, 'price') if 1: order(context.spy, -1, style = StopLimitOrder( limit_price = price * 1.008, # Selling including short stop_price = price * 0.9998 ) ) if 0: order(context.spy, 1, style = StopLimitOrder( limit_price = price * 0.9998, # Buying incuding short buyback stop_price = price * 1.001 ) ) track_orders(context, data) def handle_data(context, data): track_orders(context, data) def track_orders(context, data): # Log orders created, filled, unfilled or canceled. ''' Show orders when made and filled. Info: https://www.quantopian.com/posts/track-orders ''' c = context if 't_orders' not in c: # Move these to initialize() for better efficiency. c.t_options = { # O P T I O N S 'log_cash' : 1, # Show cash values in logging window or not. 'log_ids' : 1, # Include order id's in logging window or not. 'log_unfilled': 0, # When orders are unfilled. } c.t_orders = {} c.t_dates = { 'active': 0, 'start' : [], # Start dates, option like ['2007-05-07', '2010-04-26'] 'stop' : [] # Stop dates, option like ['2008-02-13', '2010-11-15'] } # To not overwhelm the logging window, start/stop dates can be entered above. from pytz import timezone # Python only does once, makes this portable. # Move to top of algo for better efficiency. # If the dates 'start' or 'stop' lists have something in them, sets them ... if c.t_dates['start'] or c.t_dates['stop']: date = str(get_datetime().date()) if date in c.t_dates['start']: # See if there's a match to start c.t_dates['active'] = 1 elif date in c.t_dates['stop']: # ... or to stop c.t_dates['active'] = 0 else: c.t_dates['active'] = 1 # Set to active b/c no conditions if c.t_dates['active'] == 0: return # Skip if off def _minute(): # To preface each line with the minute of the day. bar_dt = get_datetime().astimezone(timezone('US/Eastern')) return str((bar_dt.hour * 60) + bar_dt.minute - 570).rjust(3) # (-570 = 9:31a) def _orders(to_log): # So all logging comes from the same line number, log.info(to_log) # for vertical alignment in the logging window. for id in c.t_orders.copy(): # Independent copy to allow deletes o = get_order(id) if o.dt == get_datetime() and not o.limit_reached: continue # Same minute as order, no chance of fill yet. if o.filled: # Filled at least some. filled = '{} '.format(o.amount) filled_this = '' if o.filled == o.amount: # Complete if 0 < c.t_orders[o.id] < o.amount: filled = 'all {}/{}'.format(o.filled - c.t_orders[o.id], o.amount) else: filled = '{}'.format(o.amount) filled_this = 1 del c.t_orders[o.id] else: # c.t_orders[o.id] is previously filled total filled_this = o.filled - c.t_orders[o.id] # filled this time, can be 0 c.t_orders[o.id] = o.filled # save for increments math filled = '{}/{}'.format(filled_this, o.amount) if filled_this: prc = '%.2f' % data.current(o.sid, 'price') if data.can_trade(o.sid) else 'unknwn' cash = 'cash {}'.format(int(c.portfolio.cash)) if c.t_options['log_cash'] else '' _orders(' {} {} {} {}{} at {} {} {}'.format(_minute(), 'Bot' if o.amount > 0 else 'Sold', filled, o.sid.symbol, ' limit' if o.limit else '', prc, cash, str(o.id)[-4:] if c.t_options['log_ids'] else '')) elif c.t_options['log_unfilled']: _orders(' {} {} {}{} unfilled {}'.format(_minute(), o.sid.symbol, o.amount, ' limit' if o.limit else '', str(o.id)[-4:] if c.t_options['log_ids'] else '')) if o.status == 2: # Canceled prc = '%.2f' % data.current(o.sid, 'price') if data.can_trade(o.sid) else 'unknwn' cash = 'cash {}'.format(int(c.portfolio.cash)) if c.t_options['log_cash'] else '' trade = 'Buy' if o.amount > 0 else 'Sell' style = ' stop' if o.stop else '' ; style = ' limit' if o.limit else '' _orders(' {} Canceled {} {} {}{} at {}'.format(_minute(), trade, o.amount, o.sid.symbol, style, prc, cash, str(o.id)[-4:] if c.t_options['log_ids'] else '')) del c.t_orders[o.id] for oo_list in get_open_orders().values(): # Open orders list for o in oo_list: if o.id in c.t_orders: continue # New orders beyond this point cash = 'cash {}'.format(int(c.portfolio.cash)) if c.t_options['log_cash'] else '' prc = '%.2f' % data.current(o.sid, 'price') if data.can_trade(o.sid) else 'unknwn' trade = 'Buy' if o.amount > 0 else 'Sell' c.t_orders[o.id] = 0 if o.stop: # Stop order if o.limit: # StopLimit order _orders(' {} {} {} {} now {} stop {} limit {} {} {}'.format(_minute(), trade, o.amount, o.sid.symbol, prc, o.stop, o.limit, cash, str(o.id)[-4:] if c.t_options['log_ids'] else '')) else: _orders(' {} {} {} {} now {} stop {} {} {}'.format(_minute(), trade, o.amount, o.sid.symbol, prc, o.stop, cash, str(o.id)[-4:] if c.t_options['log_ids'] else '')) elif o.limit: # Limit order _orders(' {} {} {} {} now {} limit {} {} {}'.format(_minute(), trade, o.amount, o.sid.symbol, prc, o.limit, cash, str(o.id)[-4:] if c.t_options['log_ids'] else '')) else: # Market order _orders(' {} {} {} {} at {} {} {}'.format(_minute(), trade, o.amount, o.sid.symbol, prc, cash, str(o.id)[-4:] if c.t_options['log_ids'] else ''))  There was a runtime error. causes the order to immediately be converted into a limit order when the stop price is reached Rather than a new order though, everything remains the same (order id, dt, status etc) except stop price is removed, the only change. In the code I imagine this is happening: if stop price: if stop price met: # Treat it as a stop order Set stop price to None <=== whoa elif limit price and limit_reached is False: Check whether price met etc # Treat it as a limit order else: # Treat it as a market order  Instead could be: if stop price and stop_reached is False: <== added condition if stop price met: # Treat it as a stop order Set stop_reached to True <=== yes elif limit price and limit_reached is False: Check whether price met etc # Treat it as a limit order else: # Treat it as a market order  That's the only change needed to make the behavior consistent and user could maybe use stop_reached for decisions, as long as the accurate stop_reached set to True would be ok with IB. Right now when limit price is reached, limit_reached is set to True while stop_reached remains False when stop price is met. I know this is an old thread, but I'm confused by the thread itself. What I would like to do is trail a price on the way up, and if it reverses, get the stop price or better. Based on what I'm reading, that wouldn't happen. If I have stock at100 and it goes up say, to 106. I want to place the stop order below the current price of 106, and the and the limit order to be triggered on reversal for 105. Would the function below accomplish that, or not?

             def trail_stop(context,data,*limit):
if context.trail_counter < TRAIL_INTERVAL:
context.trail_counter += 1
return
else:
context.trail_counter = 0
for s in context.portfolio.positions:
if s not in context.closed:
cp = data.current(s,'price')
cb = context.portfolio.positions[s].cost_basis
amt = context.portfolio.positions[s].amount

if cp > (cb * TRAIL_STOP):
if (cp * TRAIL_STOP) > context.trail[s]:
context.trail[s] = cp
if limit == True:
tc(sl,[s,0,cp,cp,True],["SL - Trail Price Moved => " + s.symbol + " => " + str(cp),'StopLimit Error'])
else:
tc(so,[s,0,cp,True],["SO - Trail Price Moved => " + s.symbol + " => " + str(cp),'StopOrder Error'])

def sl(stock,amount,priceA,priceB,*cancel):
co(stock,cancel)
order_target(stock,amount,style=StopLimitOrder(priceA,priceB))

def co(stock,*cancel):
if cancel == True:
for s in get_open_orders(stock):
cancel_order(s)
print s

# TRY CATCH WAPPER - ALLOWS FOR MESSAGES ON SUCCESS AND FAILURE
def tc(fn,prms,msg):
try:
fn(*prms)
print prms[0].symbol + ' => ' +  msg[0]
except:
print prms[0].symbol + ' => ' + msg[1]


Anyone?

One use for StopLimit (for long): Sell if price falls to the stop but only if it doesn't drop even further below the limit set even lower.
Imagine a LimitStop order that says: If price rises 9% then don't sell yet, let it skyrocket, but set a stop now, at 8% to sell if it drops that far.

What you're referring to, Blue, is called a 'trailing stop'. Trailing stops can be percentage or dollar-amount based. These are a common tool among intermediate-advanced traders, and are available on most of the major (ie Etrade, Thinkorswim, etc) trading platforms.

I don't believe I've actually seen one successfully implemented on Q yet, because the logic behind it... ie "if ABC stock starts to fall, place a stop-limit order X% below it" ...would result in soooo many orders being placed (and subsequently having to be retracted/cancelled as the stock regained its footing) that the algo would presumably eventually go haywire and fail.

Canceling and resetting stops can be done, just lots of work, Quantopian's ordering system is built well enough to take it. By LimitStop I meant just a single order and I think the trailing stop you refer moves automatically, wouldn't that be sweet.

I hope it is (robust enough).. have only done paper-trading so far on Q, no live-trading.

Upon rethinking, though, the algo could just as easily track the price minute-by-minute and then place a market order once said threshold (X%) is reached. It may not sell at precisely X%, but it would be close enough (assuming you set the threshold at, say, 6.8% when you're really aiming for ~7%).