It seems the API is not correctly handling replacing of partially filled sell orders (buy orders work as expected).
When a buy order is partially filled and then gets replaced with an updated price, it will eventually fill the total quantity that was set initially. This shows that the API is internally keeping track of the partially filled quantity and only applying the new price to the rest of the quantity needed to fill the initially targeted qty.
However, when a sell order that was opened with qty=N is partially filled and then gets replaced with an updated price, the API is not correctly keeping track of the partially filled quantity. If updating the price, the API returns an error
insufficient qty available for order (requested: N, available: N-partially_filled_qty), showing that it thinks that the updated order which still has the initial qty set is meant to be executed for qty = N instead of qty = N - partially_filled_qty. It seems that whatever logic is checking whether enough qty is available for a regular sell order doesn’t account for fact the qty set in the replaced order may have already been partially filled and does not need to be <= to existing position.
On the other hand, if the client reduces the quantity in the replaced order request by the partially filled quantity the final filled qty (including the initial partial fill) will be less than the initially set qty, i.e. it will equal the newly set quantity. This shows that the actual execution of a replaced order does keep track of the partial fill quantity and only executes the difference between the newly set quantity and the initial partial fill for the replaced order.
This internal inconsistency in the way the API handles qty seems to be unintended behavior and prevents the client from replacing a partially filled sell order correctly.
Good catch. Yes, the
replace_order API doesn’t handle partial fills very well as described in the original post.
As a workaround, one can replace both the price AND the order quantity. Something like this (in Python but other SDKs would be similar)
# Get the order objects for the parent and two leg orders
# The parent order always stores the latest leg orders even if they have been replaced
# Always call this before replacing an order to ensure one has the current leg orders
parent_order = api.get_order('d143410f-871a-40a2-a1ef-58435be449f3')
limit_leg = parent_order.legs
stop_loss_leg = parent_order.legs
# Calculate the remaining shares to order
original_qty = float(parent_order.qty)
filled_qty = float(limit_leg.filled_qty)
replace_qty = original_qty - filled_qty
# Create whatever price logic is desired here
# Ensure the new price has just 2 decimal places
replace_limit_price = round(float(limit_leg.limit_price) * .995, 2)
# Call `replace_order` with both a new price and the remaining qty
api.replace_order(limit_leg.id, qty=replace_qty, limit_price=replace_limit_price)
This is an open item in the Alpaca API issues list (The API `PATCH/v2/orders/` incorrectly uses original qty and not updated qty if the order has a partial fill · Issue #165 · alpacahq/Alpaca-API · GitHub)
Thank you for the concise description of the problem. Hope to have this resolved soon but, in the interim, the code above should work.
Thanks for the workaround, Dan.
For more context, I have tried this kind of logic before and it doesn’t work as expected because if you replace the quantity as in
api.replace_order(limit_leg.id, qty=replace_qty, limit_price=replace_limit_price) the replaced order will only fill an additional
qty=replace_qty-limit_leg.filled_qty, i.e. it respects the
replace_qty as the intended total quantity for both parent and child. I think this is intended behavior and similar to replacing buy orders (but of course this intention is prevented by throwing an error initially when qty is not reduced, which is I think the problem).
In fact, this workaround can also raise an error if
replace_qty is less than
limit_leg.filled_qty because more than half the original qty is filled:
unable to replace order, qty must be > filled_qty.
Yes, the best is to fix the API. It’s on the active engineering task list so should be completed soon.