Alpaca Pro limit orders rejected - can't figure out why

I run some scripts at market open for my Alpaca Pro account that basically picks UP TO 4 stocks, buys them individually via a separate algorithm, then I have another algorithm that scans my current Alpaca positions currently every second (easy to modify) and ‘chases’ each position with a stop limit order.

Currently, the only calls being made to Alpaca every second are:

  • Initial query to get ALL open orders (to process locally)
  • Current price of each stock that it’s currently holding
  • Updating an existing order IF certain criteria are met (I.E. price increases by X percent or a new candle posts, etc)

I really don’t think I should be hitting API rate limits, but I’m seeing a small pattern of some INITIAL stop limit orders being ‘rejected’ or ‘cancelled’ for up to a couple iterations/seconds without any errors in the request.post response (to “v2/orders”). I put some logic in my script to handle this and usually they stick after a couple iterations. Annoying, but no biggie. However, today, I had one particular stop limit UPDATE that simply was “rejected” every iteration for about 2 minutes where I gave up and manually intervened (huge confidence killer).

The frustrating thing is the response from my requests.patch (using https://api.alpaca.markets/v2/orders/ + order[‘id’]) returned perfectly fine with no errors and included the correct stop price and looked exactly like I would expect it to. However, the dashboard showed “rejected” and the next iteration of my script still pulled the “old” order info which then triggered it to send another UPDATE using request.patch and just repeated until I killed it.

What’s up with that and how can I prevent it from happening?

****Below is my logs with some stuff in the middle removed.
The “08:38:08 - Updated limit order for ACLX” is simply the output from:
response = requests.patch(url, json=payload, headers=headers).text
response = json.loads(response)

”***Open Orders:” is the returned list of all open orders from Alpaca:
API_Endpoint + “v2/orders?status=open”
response = requests.get(url, headers=headers).text
response = json.loads(response)

”Found order for: ACLX” is simply locally parsing through the Open Orders and comparing against the known held positions to match an open order to the list of held stocks for processing.

08:38:08 - Updated limit order for ACLX

{
“id”: “556a6811-f095-4638-afb0-6d18f5981109”,
“client_order_id”: “7fde7f54-5ecf-44de-8f72-b954b62dbee7”,
“created_at”: “2026-02-23T14:38:08.681335842Z”,
“updated_at”: “2026-02-23T14:38:08.681949152Z”,
“submitted_at”: “2026-02-23T14:38:08.681335842Z”,
“filled_at”: null,
“expired_at”: null,
“canceled_at”: null,
“failed_at”: null,
“replaced_at”: null,
“replaced_by”: null,
“replaces”: “18891e01-8d25-40a9-8965-8dfff65ae1a1”,
“asset_id”: “c984af2f-f1e3-4c45-9c68-dde36c158782”,
“symbol”: “ACLX”,
“asset_class”: “us_equity”,
“notional”: null,
“qty”: “10”,
“filled_qty”: “0”,
“filled_avg_price”: null,
“order_class”: “”,
“order_type”: “stop_limit”,
“type”: “stop_limit”,
“side”: “sell”,
“position_intent”: “sell_to_close”,
“time_in_force”: “day”,
“limit_price”: “56.98”,
“stop_price”: “113.97”,
“status”: “new”,
“extended_hours”: false,
“legs”: null,
“trail_percent”: null,
“trail_price”: null,
“hwm”: null,
“subtag”: null,
“source”: null,
“expires_at”: “2026-02-23T21:00:00Z”
}




08:38:09 - Processing

Alpaca price = $114.0001
Number of shares held = 10

***Open Orders:

[
{
“id”: “18891e01-8d25-40a9-8965-8dfff65ae1a1”,
“client_order_id”: “a1e96e8f-cdc1-4131-a9e8-2a1f31fd5587”,
“created_at”: “2026-02-23T14:36:00.456166Z”,
“updated_at”: “2026-02-23T14:38:08.695497Z”,
“submitted_at”: “2026-02-23T14:36:00.46287Z”,
“filled_at”: null,
“expired_at”: null,
“canceled_at”: null,
“failed_at”: null,
“replaced_at”: null,
“replaced_by”: null,
“replaces”: “fb79c003-4af3-470e-bf8e-2469fc8b0ab5”,
“asset_id”: “c984af2f-f1e3-4c45-9c68-dde36c158782”,
“symbol”: “ACLX”,
“asset_class”: “us_equity”,
“notional”: null,
“qty”: “10”,
“filled_qty”: “0”,
“filled_avg_price”: null,
“order_class”: “”,
“order_type”: “stop_limit”,
“type”: “stop_limit”,
“side”: “sell”,
“position_intent”: “sell_to_close”,
“time_in_force”: “day”,
“limit_price”: “56.95”,
“stop_price”: “113.89”,
“status”: “new”,
“extended_hours”: false,
“legs”: null,
“trail_percent”: null,
“trail_price”: null,
“hwm”: null,
“subtag”: null,
“source”: “access_key”,
“expires_at”: “2026-02-23T21:00:00Z”
}
]




Found order for: ACLX
{
“id”: “18891e01-8d25-40a9-8965-8dfff65ae1a1”,
“client_order_id”: “a1e96e8f-cdc1-4131-a9e8-2a1f31fd5587”,
“created_at”: “2026-02-23T14:36:00.456166Z”,
“updated_at”: “2026-02-23T14:38:08.695497Z”,
“submitted_at”: “2026-02-23T14:36:00.46287Z”,
“filled_at”: null,
“expired_at”: null,
“canceled_at”: null,
“failed_at”: null,
“replaced_at”: null,
“replaced_by”: null,
“replaces”: “fb79c003-4af3-470e-bf8e-2469fc8b0ab5”,
“asset_id”: “c984af2f-f1e3-4c45-9c68-dde36c158782”,
“symbol”: “ACLX”,
“asset_class”: “us_equity”,
“notional”: null,
“qty”: “10”,
“filled_qty”: “0”,
“filled_avg_price”: null,
“order_class”: “”,
“order_type”: “stop_limit”,
“type”: “stop_limit”,
“side”: “sell”,
“position_intent”: “sell_to_close”,
“time_in_force”: “day”,
“limit_price”: “56.95”,
“stop_price”: “113.89”,
“status”: “new”,
“extended_hours”: false,
“legs”: null,
“trail_percent”: null,
“trail_price”: null,
“hwm”: null,
“subtag”: null,
“source”: “access_key”,
“expires_at”: “2026-02-23T21:00:00Z”
}

Here’s my dashboard screenshot. You can see the wall of “canceled” ACLX’s along with a couple updates that stuck. Similarly, you can see a few of the INITIAL SL orders for VNDA were ‘canceled, but eventually stuck and updated accordingly. As mentioned, I’m seeing no ‘errors’ in my logs from the API calls. I’d love to hear anyone’s thoughts/hypothesis on what’s up with this!

@kcducttaper You asked about a number of orders for ACLX being rejected. The fundamental issue is that you are attempting to replace an order that had already been replaced.

When an order is replaced or “patched”, the API doesn’t immediately know if the order can actually be replaced. It may have already been filled, for example. The API immediately responds with an order number for the updated order, assuming it can be replaced. The orders you see in the dashboard are all of the orders created when you tried to replace an order.

The reason they have all been rejected is that each time you tried to replace the same order (order number 18891e01-8d25-40a9-8965-8dfff65ae1a1). That order had already been previously replaced, so it was not an active order. That is why each time you subsequently tried to replace it, the replacement order failed and was rejected.

Below is a typical log entry for each time you replaced the order.

{"level":"info","timestamp":"2026-02-23T14:39:48.097Z","caller":"httplogger/httplogger.go:171","msg":"httplog","ip":"3.148.88.57","method":"PATCH","path":"/gobroker/api/v2/orders/18891e01-8d25-40a9-8965-8dfff65ae1a1","query":"","elapsed":0.00554499,"request_id":"01a82a458ce1b66a42f5f2a12abae605","status_code":200,"body":"{\"qty\": \"10\", \"limit_price\": \"56.98\", \"stop_price\": \"113.97\", \"time_in_force\": \"day\"}","user_agent":"python-requests/2.32.3","order_id":"aeb2e770-c5b4-4298-bf2c04ba3195b294","ratelimit_limit":200,"ratelimit_remaining":197,"ratelimit_reset":"2026-02-23T14:39:48.000Z","env":"live"}

Hope that makes sense. Basically, you cannot replace the same order twice. You should wait until you receive confirmation that the order was replaced before trying to replace it again. Then use the order number of the replacement order, not the original order.

Kinda, but if that’s the case, wouldn’t the active replacement order be returned in my “v2/orders?status=open” call?

I update the order ID based on the open orders that call returns, so my assumption is if there was an order that has been replaced, and is active and open, it would return with that call.

@kcducttaper Looking at your order sequence more closely, it seems the issue was that your limit price was too far from your stock price. Your replace requests kept getting rejected, which is why your algo kept resubmitting the same replace request multiple times. I’ll step through the sequence of your orders.

Three identical orders were submitted roughly two seconds apart. They were identical and routed to Citadel Securities for execution. Citadel immediately rejected each of those orders with a reason code of. “Limit Price Too Far From Stop Price” This isn’t enforced by every execution partner, but generally it’s a good practice to set the limit price within 5% of the stop price. Below are the three order requests that were subsequently rejected.

{"timestamp":"2026-02-23T14:34:22.816Z", "POST", "status_code":200, "body":"{\"type\": \"stop_limit\", \"time_in_force\": \"day\", \"symbol\": \"ACLX\", \"qty\": \"10\", \"side\": \"sell\", \"limit_price\": \"56.7\", \"stop_price\": \"113.4\"}", "order_id":"817ffec5-f1a1-4720-8627-6e3eb67099eb"}
{"timestamp":"2026-02-23T14:34:24.410Z", "POST","status_code":200, "body":"{\"type\": \"stop_limit\", \"time_in_force\": \"day\", \"symbol\": \"ACLX\", \"qty\": \"10\", \"side\": \"sell\", \"limit_price\": \"56.7\", \"stop_price\": \"113.4\"}", "order_id":"de94c01d-906e-4fd0-bbe6-328953c1f04c"}
{"timestamp":"2026-02-23T14:34:25.965Z", "POST","status_code":200, "body":"{\"type\": \"stop_limit\", \"time_in_force\": \"day\", \"symbol\": \"ACLX\", \"qty\": \"10\", \"side\": \"sell\", \"limit_price\": \"56.7\", \"stop_price\": \"113.4\"}", "order_id":"c1834d7c-7319-418b-9e08-74365b845213"}

Your algo then entered a fourth identical order, which was routed to Virtu Americas. Virtu accepted the order (each execution partner has slightly different reject criteria), and it became active.

{"timestamp":"2026-02-23T14:34:27.505Z", "POST", "status_code":200, "body":"{\"type\": \"stop_limit\", \"time_in_force\": \"day\", \"symbol\": \"ACLX\", \"qty\": \"10\", \"side\": \"sell\", \"limit_price\": \"56.7\", \"stop_price\": \"113.4\"}", order_id":"fb79c003-4af3-470e-bf8e-2469fc8b0ab5"}

Then your algo attempted to replace (ie patch) this order with the request below:

{"timestamp":"2026-02-23T14:36:00.383Z", "PATCH", "status_code":422,"body":"{\"qty\": \"10\", \"limit_price\": \"56.94\", \"stop_price\": \"113.885\", \"time_in_force\": \"day\"}","err_body":"{\"code\":42210000,\"message\":\"invalid stop_price 113.885. sub-penny increment does not fulfill minimum pricing criteria\"}"}

However, the patch failed because the stop price was specified to 3 decimal places. For symbols trading over $1, limit and stop prices must be specified only to the penny. Sub-penny increments are not permitted.

The algo then attempted to patch that same order again, this time with a different stop price specified to the penny, which succeeded.

{"timestamp":"2026-02-23T14:36:00.462Z", "PATCH", "status_code":200,"body":"{\"qty\": \"10\", \"limit_price\": \"56.95\", \"stop_price\": \"113.89\", \"time_in_force\": \"day\"}", "order_id":"18891e01-8d25-40a9-8965-8dfff65ae1a1"}

Your algo then attempted to replace this order. However, the replace request was rejected by Virtu with the message OrderCancelReplace Reject : Invalid limit price 56.98. The price must be greater than or equal to 56.99 (50% of 113.97) The issue is similar to the reason Citadel rejected your previous orders. The limit price was too low.

Your algo continued to try to replace order 18891e01-8d25-40a9-8965-8dfff65ae1a1 66 times, finally closing the position with cancel_orders=true.

{"timestamp":"2026-02-23T14:39:51.826Z", "DELETE"positions/ACLX", "query":"cancel_orders=true"}

So ultimately, it seems the basic issue was that the orders were being rejected because the limit price was too far from the stop price.

1 Like

Oh, interesting… Makes sense now and I didn’t realize there was a percentage limit on the limit price! I just randomly set it to 50% because I didn’t put many brain cells into it and simply wanted to ensure that the entirety of the order was executed upon crossing my stop price. That’s probably what was also causing my occasional initial reject here and there too.

I’ll update my algo accordingly both in percentage and rounding. Thanks a bunch!