Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Bug in my code?

Hello,

I would like to ask for your help in finding the bug in my code (or Quantopian's) in the Halloween code that I previously shared. Here is a cleaner version:

def initialize(context):
#context.stocks = [sid(24)] #AAPL
context.stocks = [sid(22169)] #SNP
#context.stocks = [sid(8554)] #S&P500

# Setting our maximum position size  
context.max_notional = 1000000.1  
context.min_notional = -1000000.0  

# Don't forget to change this number to match the number of stocks in your portfolio  
context.max_stock = len(context.stocks)  

log.info(' SID-- Price-- Shares Value------- Order')

def handle_data(context, data):
max_notional = int(context.max_notional / context.max_stock)

for stock in context.stocks:  
    trade_date = data[stock].datetime  

    # buy if not already in trade and if we are in November  
    if (context.portfolio.positions[stock].amount == 0) and (trade_date.month == 10):  
        price = data[stock].price  
        shares = int(max_notional / price)  
        value = price * shares  
        order(stock, shares)  
        log.info('{:>5} {:>7.3f} {:>6} {:> 12.3f} BUY'.format(stock, price, shares, -value))  

    # sell if already in trade and if we are in March  
    elif (context.portfolio.positions[stock].amount != 0) and (trade_date.month == 5):  
        price = data[stock].price  
        shares = context.portfolio.positions[stock].amount  
        value = price * shares  
        order(stock, -shares)  
        log.info('{:>5} {:>7.3f} {:>6} {:> 12.3f} SELL'.format(stock, price, shares, value))

What happens is that if I run the code with AAPL or S&P500 everything works, but if I run the same code with SNP it shows the following output (with too many sells):

1970-01-01initialize:13INFO SID-- Price-- Shares Value------- Order
2008-10-01handle_data:28INFO22169 78.350 12763 -999981.050 BUY
2009-05-01handle_data:36INFO22169 78.560 12763 1002661.280 SELL
2009-10-01handle_data:28INFO22169 82.590 12108 -999999.720 BUY
2010-05-03handle_data:36INFO22169 80.040 12108 969124.320 SELL
2010-10-01handle_data:28INFO22169 88.940 11243 -999952.420 BUY
2011-05-02handle_data:36INFO22169 99.940 11243 1123625.420 SELL
2011-10-03handle_data:28INFO22169 95.650 10454 -999925.100 BUY
2012-05-01handle_data:36INFO22169 107.450 10454 1123282.300 SELL
2012-10-01handle_data:28INFO22169 92.550 10804 -999910.200 BUY
2013-05-01handle_data:36INFO22169 108.720 10804 1174610.880 SELL
2013-05-02handle_data:36INFO22169 108.680 5268 572526.240 SELL
2013-05-03handle_data:36INFO22169 109.130 -370 -40378.100 SELL
2013-05-06handle_data:36INFO22169 109.290 -4898 -535302.420 SELL

If you analyze the code, there should be a sell for every buy, but with this specific sid it shows 3 extra sells.

Thanks in advance,

JM

8 responses

Hello Joao,

I took a quick look and couldn't sort it out. In any case, I've attached your algorithm in case someone wants to clone it. I added the line:

record(num_shares = context.portfolio.positions[context.stocks[0]].amount)  

Grant

Hello,

I can't see any error in my code either.
Maybe it's something with the portfolio updating?

Thanks,

JM

Here's what's going on. On May 1, 2013, your algo places an order to sell 10804 shares of SNP. The backtester tries to fill that order on May 2. But, only 5536 shares are actually sold on May 2, not the full 10804. You can see that by running a "full backtest" and then looking at the "transactions" tab.

The question next is, why wasn't the order filled completely? The answer is that the slippage model didn't let you. Google Finance says the volume of SNP on that day was just under 23,000 shares. The default slippage model limits you to 25% of the trading volume - the 5536 shares that you actually got.

Now back to your algo. The algo assumes that all orders are completely filled, and it's not robustly handling when an order is only partially filled. Line 34 of the code Grant put together tells the story. The amount of the stock held is not equal to zero, and the trade month is five, so the algo blithely places another order, even though there is a partially filled order still open, and now we've really left the rails!

Options:

  • Best is probably to both check your orders and your positions in your conditional logic
  • You can override the slippage model so that orders are always filled by the backtester - easily done, but not good if you're planning on using this with real money someday
  • Avoid the problem entirely by using more liquid stocks and/or smaller order sizes.

I think this exercise is pretty interesting, actually. Your testing found a situation where your strategy would probably have problems in the real world because the stock is thinly traded. It didn't show up the way you expected, but you found it nonetheless.

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 Dan,

I thought I'd try to fix the logic with get_open_orders, but it spits out a lot more than I need:

2008-01-04PRINT<bound method AlgorithmProxy.get_open_orders of AlgorithmProxy( captial_base=100000.0 sim_params= SimulationParameters( period_start=2008-01-04 00:00:00+00:00, period_end=2013-06-11 23:59:00+00:00, capital_base=1000000.0, emission_rate=daily, first_open=2008-01-04 14:31:00+00:00, last_close=2013-06-11 20:00:00+00:00), initialized=True, slippage=VolumeShareSlippage( volume_limit=0.25, price_impact=0.1), commission=PerShare(cost=0.03), blotter=Blotter( transact_partial=(VolumeShareSlippage( volume_limit=0.25, price_impact=0.1), PerShare(cost=0.03)), open_orders=defaultdict(<type 'list'>, {}), orders={}, new_orders=[], current_dt=2008-01-04 00:00:00+00:00), recorded_vars={'num_shares': 0})>  

Is there an example of how to use get_open_orders? Or perhaps it is not the right approach?

I've attached the backtest I used to generate the log output above.

Thanks,

Grant

Grant it's a great question, and one that I've been working on. Well. . . when I say I was working on it. . . I mean that I used it as an interview question for our summer intern interviews. I liked this solution.

It's not yet a universal solution, but it gets you most of the way.

# define a new function to deal with orders so total amount of longs should never be able to exceed maxl  
def limorder(context, sid, amount, maxl, limit_price=False, stop_price=False):  
    # count how many shares are ordered where the order hasn't yet been filled  
    tot = 0  
    for o in get_open_orders(sid=sid):  
        tot += o.amount  
    # if unfilled order shares + owned shares + amount ordering is greater than maxl, don't order  
    if tot + context.portfolio.positions[sid].amount + amount > maxl:  
        return False  
    else:  
        return order(sid, amount, limit_price, stop_price)  
def handle_data(context, data):

    # if we can't order, say so  
    context.order_id = limorder(context, sid(24), 25, 1000)  
    if not context.order_id:  
        log.debug("couldn't order")  

@Grant, for a simpler result you can ask for orders related to a single sid:

get_open_orders(sid=sid)  

And you can get some useful values from the orders list this way:

orders = get_open_orders(sid=stock)  
amount = sum(o.amount for o in orders)  
pending = amount - sum(o.filled for o in orders)  

I have attached a backtest where in day one I try to order more SNP than is available (based on trading volume). The log shows how these orders are partially filled the next day and continue to be filled over the next week.

2013-06-03 PRINT Number of orders pending: 4  
2013-06-03 PRINT Size of original orders: 84848  
2013-06-03 PRINT Pending shares to order: 84848  
2013-06-03 PRINT Current position amount: 0  
2013-06-04 PRINT Number of orders pending: 4  
2013-06-04 PRINT Size of original orders: 84848  
2013-06-04 PRINT Pending shares to order: 75544  
2013-06-04 PRINT Current position amount: 9304  
2013-06-05 PRINT Number of orders pending: 4  
2013-06-05 PRINT Size of original orders: 84848  
2013-06-05 PRINT Pending shares to order: 60628  
2013-06-05 PRINT Current position amount: 24220  
2013-06-06 PRINT Number of orders pending: 3  
2013-06-06 PRINT Size of original orders: 63636  
2013-06-06 PRINT Pending shares to order: 39286  
2013-06-06 PRINT Current position amount: 45562  
2013-06-07 PRINT Number of orders pending: 2  
2013-06-07 PRINT Size of original orders: 42424  
2013-06-07 PRINT Pending shares to order: 24927  
2013-06-07 PRINT Current position amount: 59921  
2013-06-10 PRINT Number of orders pending: 2  
2013-06-10 PRINT Size of original orders: 42424  
2013-06-10 PRINT Pending shares to order: 8629  
2013-06-10 PRINT Current position amount: 76219  
2013-06-11 PRINT Number of orders pending: 0  
2013-06-11 PRINT Size of original orders: 0  
2013-06-11 PRINT Pending shares to order: 0  
2013-06-11 PRINT Current position amount: 84848  

Hello Joao,

I used the solution from Dennis above to fix your algorithm (see attached). Note that I was lazy and did not generalize to multiple sids (a homework exercise).

I check for pending orders and set a boolean flag. New orders can be submitted only if there are no pending orders:

        if (context.portfolio.positions[stock].amount == 0) and (trade_date.month == 10) and (pending_orders == False):  
            price = data[stock].price  
            shares = int(max_notional / price)  
            value = price * shares  
            order(stock, shares)  
            log.info('{:>5} {:>7.3f} {:>6} {:> 12.3f} BUY'.format(stock, price, shares, -value))  
        # sell if already in trade and if we are in March  
        elif (context.portfolio.positions[stock].amount != 0) and (trade_date.month == 5) and (pending_orders == False):  
            price = data[stock].price  
            shares = context.portfolio.positions[stock].amount  
            value = price * shares  
            order(stock, -shares)  
            log.info('{:>5} {:>7.3f} {:>6} {:> 12.3f} SELL'.format(stock, price, shares, value))  

If you clone the backtest, run it, and then look at the "Transaction Details" you'll see that the slippage model results in the final sell order being spread over two days.

Grant

I will be interested to see how quantopian adapts to trading live with IB, when the trader is placing discretionary orders for the same stocks. That's a fun problem!