Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Dealing with Partial Fills

Partial fills happen with limits on the number of shares that can be traded in a particular minute based on the slippage model in use, in the effort for backtesting to match the real world (to whatever degree possible). They are based on each minute's volume for each stock.

Sometimes when a stock is ordered, its volume throughout that day is so low (and the order so high) that partial fills run all the way to the end of the day. That produces log messages like this, that most are familiar with (from lecture 43, basic pairs):

2014-01-02 13:00 WARN Your order for -346741 shares of ABGB failed to fill by the end of day and was canceled.  
2014-01-02 13:00 WARN Your order for 87260 shares of FSLR has been partially filled. 17663 shares were successfully purchased. 69597 shares were not filled by the end of day and were canceled.  
2014-01-03 13:00 WARN Your order for -338361 shares of ABGB has been partially filled. 2 shares were successfully sold. 338359 shares were not filled by the end of day and were canceled.  

In orders from get_open_orders(), .amount is the total number of shares ordered originally and .filled is the total number of shares filled so far.
Cost basis is updated with each partial fill. context.portfolio.positions[stock].cost_basis

My impression so far is that drawdown (and all metrics) can be improved by handling partial fills. If the price is moving against us while fills are happening, it contributes to drawdowns. When there are a lot of partial fills on opening an order, it's a hint that closing the order can also be difficult, so a cutoff point on opening a position is one route. Many strategies for addressing partial fills can be employed.

This is an invite for ideas because I worry that the majority of us are oblivious to the magnitude of partial fills and just hoping for the best. I'll have more to add to this.

4 responses

I'd suggest that you think of partial fills as the symptom, not the problem. If you're hitting partial fills, what's happening in the real world is that you're a big fraction of the market, and you're pushing the market price against you. The slippage model doesn't do a good job of showing that level of market impact; it's even worse in the real world.

If you're hitting partial fills regularly, you're regularly trying to open or close a position that is big relative to the volume that is traded in that stock. That's the problem I suggest you focus on. You need to avoid that problem before it happens rather than try to manage the problem once you've found it.

Ways to avoid the problem:

  • Use the QTradableStocksUS to limit your universe up front. That universe is designed to hold only relatively liquid stocks.
  • Use the Optimize API to constrain your max position size so you never go too long or short into any given name
  • Use a custom factor that looks at historical traded volume and the optimize API to constrain the individual positions in your portfolio to never exceed a specified fraction of the average daily volume.
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.

The problem is more prevalent than one might think. For example, the picture below traces trades as they are filled for 3 quite tradable stocks. They trade, on average, over 2 million shares per day. More than enough to fill orders for 1,000 to 3,000 shares.

The extent of the slippage can be estimated by the number of vertical ticks. The stocks trade every minute taking from 10 to 30 minutes to fill, some take even longer. This is a lot of slippage. It is not just a penny here or a penny there. It is a major problem.

It is very simple. The 2.5% rule does not reflect reality that well. Nor does the no-slippage testing condition criteria or the no-minimum fee per trade.

To Dan's point I've been doing those along with something like this regarding the custom factor he mentioned.

class VolumeMin(CustomFactor):  
    inputs = [USEquityPricing.volume] ; window_length = 42  
    def compute(self, today, assets, out, volume):  
        out[:] = np.min(volume, axis=0)    # & VolumeMin().top(600)  

To quantify partial fills, and if anyone is interested in seeing how their own code is doing, in track_orders, can fairly easily add a counter to be incremented each minute that an order is not yet complete, and log the total at the end of a run. Can even keep track per stock using like context.partials = pd.Series({}) with context.partials[stock] += 1. (and even make trading decisions with it too).

The fine code at Quantopian Risk Model In Algorithms is throwing me for a loop to some degree while I'm playing around attempting to find a way to reduce partial counts while improving some metrics, it is breaking some of my preconceived notions actually. But for now, just in terms of scale, essentially this is what I seem to be seeing with the original:

Buys: 650743 (406134 partial)
Sells: 669625 (409757 partial)
Average partial fills throughout the run for top 30 highest counts: 2988

I'm trying something that reduces the number of partial fills, there must be 50 ways to go about it, and have hit a surprising drawdown, the opposite of what I expected.

Regarding partial fills... I try to address that by limiting my order amount to less than 2.5% of the average dollar-volume for each security. In practice I use 2% as a max. This is a bit artificial and really based upon the Q slippage model limiting trades to 2.5% of minutely volume (I believe that's still correct?). While this helps eliminate partial fills from my algorithms, the real motive is to increase trades in smaller dollar-volume stocks. I don't filter by dollar-volume but rather simply enforce a constraint that I can't order more than 2% of the average dollar-volume of any security. This is easily done by adding a 'PositionConcentration' constraint.

First, create an average dollar volume factor in a pipeline. Not sure what the best window length is (let me know if anyone has thoughts on this) but I've been using 10.

def make_pipeline(context, data):  
    # Make the pipeline and just add the following factor  
    avg_dollar_vol = AverageDollarVolume(window_length=DOLLAR_VOL_DAYS)

Next, run the pipeline. Add a new column in the dataframe output which represents the maximum percent we want to hold of the current portfolio for each security. If one never wants to hold more than 2% of the typical dollar-volume of a stock, then this percent is simply 2% x avg_dollar_volume / portfolio_value. Note that this percent could be greater than 100% for some big securities. That's no problem. It's a max value.

def before_trading_start(context, data):  
   # Run the pipeline and then add a column for the max percent of the portfolio value each security can have  
   context.output = pipeline_output('pipe')

   context.output['max_position'] = context.output.avg_dollar_vol * MAX_DOL_VOL_PERCENT / context.portfolio.portfolio_value

Finally, add this 'max_position' percent as an ordering constraint. Add it as both a positive (long) and a negative (short) value.

def my_order(context, data):  
    # Set up the ordering objective and any constraints.  
    # Include the PositionConcentration constraint to limit the amount held/ordered  
    objective = opt.MaximizeAlpha(pipeline_data.alpha)  
    max_position = PositionConcentration(min_weights = -context.output[['max_position']],  
                                         max_weights = context.output[['max_position']])

    order_optimal_portfolio(  
        objective=objective,  
        constraints=[  
            max_position,  
        ],  
    )

Note there is a subtle issue here with 'PositionConcentration'. This approach limits the total amount held and not the total amount traded. Indirectly it will typically limit the amount traded since one wouldn't place a trade for more than one holds/wants to hold. But odd things can happen.

Anyway, using the 'PositionConcentration' constraint is one way to limit partial fills while at the same time allowing exposure to smaller dollar-volume securities.