Back to Community
Time Management for Orders

A common inconsistency with backtesting vs. live trading is the length of time that orders remain valid. IB's default "time in force" model is a DAY order, which means that IB cancels all open orders at the end of the day unless explicitly told not to do so. However, in backtesting all orders persist until they are filled or explicitly cancelled.

I wrote an "OrderWrapper" class and some time-in-force models to more accurately model the time-in-force options IB offers. I implemented the following ones.

  • DAY: this is the default time in force. It is cancelled at the end of the day
  • GTC Good-Till-Cancelled: This order remains valid until the end of the following business quarter. IB does the same thing except the order is also cancelled if there are any corporate actions on the stock.
  • IOC Immediate-Or-Cancel: this order is given one minute to fill, any unfilled portion is cancelled.
  • GBD Good-Between-Dates: The order only becomes valid between two dates. If an end date is specified with no start, the order is valid right away. Allows orders to be scheduled in the future which is pretty cool.

The algo just blindly buys 5000 shares of two illiquid biotechs all the time to demo what's going on. You will see that the full 5000 shares rarely gets through due to slippage. I used the IOC and default DAY orders here.

I hope you find this useful.

David

Clone Algorithm
15
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
import pandas as pd
from zipline.utils import tradingcalendar


def initialize(context):
    set_commission(commission.PerShare(0.0))
    context.x = symbol('PBMD')
    context.y = symbol('ALDR')
    context.order_manager = OrderManager()
    
    schedule_function(buy_before_close,
                      time_rule=time_rules.market_close(minutes=3))

    
def handle_data(context, data):
    context.order_manager.update(context, data)
    now = get_datetime()
    
    # Trys to buy 5000 shares every 30 min
    # Any unexecuted portion is cancelled after one minute. 
    if now.minute % 30:
        return
    context.order_manager.place_order(order, context.x, 5000, tif='IOC')
    
def buy_before_close(context, data):
    # Defaults to a day order which is cancelled in at the end of the day.
    context.order_manager.place_order(order, context.y, 5000)
    
    
    
class OrderManager(object):
    
    def __init__(self):
        self.orders = []
        
    def update(self, context, data):
        orders = []
        for order_ in self.orders:
            order_.update(context, data)
            if not order_.expired:
                orders.append(order_)
        self.orders = orders
        
    def place_order(self, order_func, sid, amount, 
                    limit_price=None, stop_price=None,style=None,
                    tif=None, first_valid_dt=None, last_valid_dt=None):
        order_ = OrderWrapper(order_func, sid, amount, 
                              limit_price=limit_price, 
                              stop_price=stop_price, 
                              style=style, 
                              tif=tif, 
                              first_valid_dt=first_valid_dt,  
                              last_valid_dt=last_valid_dt)
        self.orders.append(order_)
            
    

class OrderWrapper(object):

    def __init__(self, order_func, sid, amount, 
                 limit_price=None, stop_price=None,style=None,
                 tif=None, first_valid_dt=None, last_valid_dt=None):
        self.func = order_func
        self.sid = sid
        self.amount = amount
        self.limit_price = limit_price
        self.stop_price = stop_price
        self.style = style
        if first_valid_dt is None:
            first_valid_dt = get_datetime()
        self.first_valid_dt = first_valid_dt
        self.last_valid_dt = last_valid_dt
        self.tif = self._get_tif_model(tif)
        self.active = False
        self.expired = False
        self.oid = None

    def _get_tif_model(self, tif):
        if isinstance(tif, TimeInForceModel):
            return tif
        if tif == 'GTC':
            return GoodTillCancelled()
        if tif == 'IOC':
            return ImmediateOrCancel()
        if self.last_valid_dt is not None:
            return GoodBetweenDates(self.first_valid_dt, self.last_valid_dt)
        return DayOrder()
    
    def update(self, context, data):
        now = get_datetime()
        isafter, isbefore = self.tif.valid_date_bools(now)
        if not isafter:
            return
        if not isbefore:
            self.cancel()
            self.expired = True
            return
        if not self.active:
            self.place_order()
            self.active = True
            
    def place_order(self):
        self.oid = self.func(self.sid, self.amount, 
                             limit_price=self.limit_price, 
                             stop_price=self.stop_price,
                             style=self.style)
        
    def cancel(self):
        if self.oid is not None:
            cancel_order(self.oid)
            
        


class TimeInForceModel(object):
    """
    Base class representing when an order is valid
    """
    _tif = None

    def get_first_valid_dt(self):
        """
        Get the first valid datetime for this order
        """
        return self.first_valid_dt

    def get_last_valid_dt(self):
        """
        Get the last valid datetime for this order
        """
        return self.last_valid_dt

    def set_last_valid_dt(self, dt):
        """
        Alters the last valid datetime for this order
        """
        self.last_valid_dt = dt

    def valid_date_bools(self, dt):
        """
        Returns a tuple of (f(dt) ==> bool) outputs.
        """
        return (dt >= self.get_first_valid_dt(),
                dt <= self.get_last_valid_dt())


class DayOrder(TimeInForceModel):
    """
    Represents an order that only remains open for
    the trading day in which it was submitted.
    """

    def __init__(self):
        """
        Set the last valid dt the market_close on the day
        the order was placed. 
        """
        self._tif = 'DAY'
        self.first_valid_dt = get_datetime()
        open_and_closes = tradingcalendar.open_and_closes
        dt = tradingcalendar.canonicalize_datetime(self.first_valid_dt)
        idx = open_and_closes.index.searchsorted(dt)
        self.last_valid_dt = open_and_closes.iloc[idx]['market_close']
        # Subtract one minute so the order is cancelled on the last bar of the day
        self.last_valid_dt -= pd.offsets.Minute()



class GoodTillCancelled(TimeInForceModel):
    """
    Represents an order that remains open
    until the end of the following business quarter
    unless explicitly cancelled.
    """

    def __init__(self):
        self._tif = 'GTC'
        self.first_valid_dt = get_datetime()
        self.last_valid_dt = self.first_valid_dt + pd.offsets.BQuarterEnd(2)


class ImmediateOrCancel(TimeInForceModel):
    """
    Represents an order that must fill immediatley,
    any amount still open after the first bar is cancelled.
    """

    def __init__(self):
        """
        Set the last dt one minute later because
        zipline orders process on the bar following
        the bar the order was placed. 
        """
        self._tif = 'IOC'
        self.first_valid_dt = get_datetime()
        self.last_valid_dt = get_datetime() + pd.offsets.Minute()


class GoodBetweenDates(TimeInForceModel):
    """
    Represents an order that only remains open
    between two specified dates (inclusive).
    """
    def __init__(self, first_valid_dt, last_valid_dt):
        self._tif = 'GBD'
        self.first_valid_dt = first_valid_dt
        self.last_valid_dt = last_valid_dt


There was a runtime error.
2 responses

This is nice, but why not just implement proper order types in the IB interface as well as zipline? IOC is quite different from allowing an order a whole minute to execute.

It would be nice to have "one cancels another" pairs of orders.
For example, a strategy that exits with either a stop-loss or stop-profit.
When either order is triggered it cancels the other order.

[If this feature exists please comment here]