feature: add one-cancels-the-other (OCO) order group - #9635
Open
Romazes wants to merge 21 commits into
Open
Conversation
Romazes
marked this pull request as draft
July 24, 2026 15:34
Romazes
force-pushed
the
feature-8253-oco-order
branch
from
July 28, 2026 20:34
99e04e7 to
05ad8b1
Compare
- add ComboType to GroupOrderManager and the OneCancelsTheOtherOrder api - gate live order groups behind BrokerageModel.SupportsGroupExecution - simulate group fill and sibling cancel in the backtesting brokerage - count one leg per group in buying power and open order aggregations - carry the group manager through the order factory and json round trip
- end the group pass on any fill, partial as well as complete - take the executed quantity off every other open leg instead of only canceling on a complete fill, and cancel a leg left with nothing to execute - add a regression algorithm that fails without the fix - assert the sibling reduction in the backtesting brokerage tests
- add a second group that buys 100 and is won by its stop market leg - sell 200 in the first group so holdings reverse through zero - open the position on its own bar and drop the price rounding the transaction handler already does - find the legs by order type instead of by list index
- reject a group that is not exactly 2 legs, us equity, limit plus stop market, one side, day or gtc tif - add the five brokerage messages for those rejections - cover the valid group and every rejection reason, and make the group execution gate test able to fail
- move the enum to its own file Common/Orders/GroupExecutionType.cs - rename GroupOrderManager.ComboType to ExecutionType, json name to executionType
- lean already rounds order prices before sending them - mirror the change in the python twin of the cancel algorithm
- PreOrderChecksImpl already answers ZeroQuantity, so the group comes back as one invalid ticket - assert the invalid ticket instead of the exception
- drop the per group skip so every open order gets its own cancel, like CancelOpenOrders - a leg its siblings already canceled answers with an error response instead of throwing
- remove SupportsGroupExecution from IBrokerageModel, DefaultBrokerageModel, alpaca, ib and the python wrapper - remove the live mode check from the transaction handler and every test that covered it - restore PythonWrapper as it was: no default bodied interface method is left to skip
- places a market entry and a 2 leg oco exit, logs every order event - manual live/paper testing aid, it does not implement IRegressionAlgorithmDefinition
- ProcessOneCancelsTheOtherGroup returns true when the group must be evaluated again, no ref parameter - shorten the comments to the reason behind the code
- CompositeLogHandler and BrokerageTests go back to master, they are not part of the order group work - restore the group manager comments to one line each in Order and OrderJsonConverter - place the cash buying power oco test through OneCancelsTheOtherOrder instead of hand built requests
Romazes
force-pushed
the
feature-8253-oco-order
branch
from
August 20, 2026 19:40
9df6d96 to
cdfd68c
Compare
- one line each in CashBuyingPowerModel, SecurityPortfolioManager and BrokerageTransactionHandler
Romazes
marked this pull request as ready for review
August 24, 2026 16:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a one-cancels-the-other (OCO) order group.
QCAlgorithm.OneCancelsTheOtherOrderplaces a take profit limit leg and a stop loss stop market leg together, both for the same quantity. Both legs rest in the market; the first one to execute ends the group.C#:
Python:
The group can never trade more than the quantity it was given. When a leg executes, that quantity comes off every other open leg, and a leg left with nothing is canceled in the same order event batch. A full fill is just the case where the others reach zero. This also covers a partial fill: the stop selling 30 of 100 leaves the limit leg at 70, so the position keeps its target and the group never sells 130.
Main changes:
GroupExecutionTypeenum (Combo,OneCancelsTheOther) andGroupOrderManager.ExecutionType, serialized asexecutionType. Groups serialized before this keep loading asCombo.Order.CreateOrderandOrderJsonConverterattach the group manager to a plainLimit/StopMarketleg before its Id is set, so a leg restored from JSON stays in its group. Only combo order types carried the manager through those paths before.QCAlgorithm.SubmitGroupOrderis the shared submitter for group types that are not the ratio combo: it builds the legs' requests, runs every pre-order check before submitting any leg, and submits in list order. The conditional (OTO) and bracket orders add their own thin wrapper over it.BacktestingBrokeragesimulates the group so backtesting and paper trading need no brokerage support. Legs are evaluated in a fixed order — stop legs first, then by id — and the first leg to execute any quantity ends the pass and reduces its siblings.CashBuyingPowerModelstops counting a sibling's quantity as reserved, andGetProjectedHoldingsandShortablecount one leg per group.HandleUpdateOrderRequestnow validates buying power when a leg of a non-combo group is updated. Combo legs keep the old skip.AlpacaBrokerageModel.CanSubmitOrderchecks Alpaca's own rules per leg: 2 legs, US equity, limit or stop market, both legs on the same side, day or good til canceled.Related PR(s)
Both are drafts that depend on this one:
Related Issue
#8253
Motivation and Context
First step of the bracket order plan: OCO now, conditional (OTO) next, bracket on top of both. A user can protect a position with a take profit and a stop loss pair without canceling the other one by hand, and without the risk of both filling.
Requires Documentation Change
Yes. The new
OneCancelsTheOtherOrderAPI needs a page, and the brokerage pages need to say which brokerages accept the group once the two plugin pull requests land.How Has This Been Tested?
Regression algorithms on SPY hourly, January 2019:
OneCancelsTheOtherOrderRegressionAlgorithm(C# and Python) — the take profit leg fills, the stop leg endsCanceledin the same batch, and the algorithm ends flat.OneCancelsTheOtherOrderCancelRegressionAlgorithm(C# and Python) — canceling one leg cancels both, and the original market order fill is untouched.OneCancelsTheOtherOrderPartialFillRegressionAlgorithm(C# only, it needs a custom fill model) — the stop leg executes 30 of 100 and the limit leg has to shrink to 70. It asserts the leg quantity, not just the total, because a group that stopped early would also end flat.OneCancelsTheOtherOrderDemoAlgorithm— a manual aid for running the group against a live or paper brokerage. It does not implementIRegressionAlgorithmDefinition.Unit tests:
BacktestingBrokerageTests:LegFillCancelsSiblingInSameEventBatch,StopLegWinsTieOverLimitLeg,CancelingOneLegCancelsWholeGroup,CancelOrderLeavesAlreadyClosedLegUntouched,TimeInForceExpiryOnAnyLegCancelsWholeGroup,PartialFillReducesSiblingsAndGroupStaysPending,GroupIsProcessedOnlyOnceExactlyPerScanDespiteTwoPendingEntries.AlgorithmTradingTests: the two tickets come back limit first and share one manager; a zero quantity returns one invalid ticket; the tag and order properties reach both legs;Liquidatecancels every leg of an open group.BrokerageTransactionHandlerTests: the legs buffer until the group is complete, a submit time failure invalidates every leg, and the update path validates buying power for OCO but not for combo.SecurityPortfolioManagerTestsandCashBuyingPowerModelTests: the most expensive leg rule, and a cash account holding exactly 1 BTC can place the sell pair throughOneCancelsTheOtherOrder.OrderJsonConverterTestsandOrderTests: a plain leg keeps its group over two round trips, and old JSON without the field loads asCombo.AlpacaBrokerageModelTests: a valid group passes, and each of the five per-leg rules rejects on its own.Types of changes
Checklist:
bug-<issue#>-<description>orfeature-<issue#>-<description>