Skip to content

Add BracketOrder API with engine-guaranteed OCO semantics - #9666

Draft
jhonabreul wants to merge 2 commits into
QuantConnect:masterfrom
jhonabreul:feature-bracket-orders
Draft

Add BracketOrder API with engine-guaranteed OCO semantics#9666
jhonabreul wants to merge 2 commits into
QuantConnect:masterfrom
jhonabreul:feature-bracket-orders

Conversation

@jhonabreul

@jhonabreul jhonabreul commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds a first-class bracket order API with engine-guaranteed one-cancels-the-other (OCO) semantics:

bracket = self.bracket_order(symbol, 100, stop_loss_price=95, take_profit_price=110)
# bracket.entry_ticket / bracket.stop_loss_ticket / bracket.take_profit_ticket
  • QCAlgorithm.BracketOrder(...) submits the entry (market by default, limit via entryLimitPrice) and returns a BracketOrderTicket.
  • BracketOrderTicket holds the linked tickets plus Cancel(), MoveStopLoss(price), MoveTakeProfit(price).
  • The linkage is enforced by the engine in the transaction handler, after the user's OnOrderEvent:
    • Entry fills → exit legs are placed, sized to the actual filled quantity.
    • One leg fills → the sibling is canceled. A single bar spanning both leg prices fills exactly one leg (deterministically the stop loss).
    • An unrelated order closes or flips the position → remaining legs are canceled; a partial external reduction downsizes the legs.
  • Re-entry refusal: submitting a new bracket while one is active for the symbol throws instead of silently stranding the previous legs. Transactions.GetBracketOrderTicket(symbol) exposes the active bracket.

Related Issue

N/A

Motivation and Context

Hand-rolled stop-loss/take-profit state machines are common and error-prone: an overwritten ticket field can leave a stop leg dangling, and both legs filling on a gapping bar can flip the position. This makes the correct pattern a one-liner.

Requires Documentation Change

Yes: new BracketOrder API and BracketOrderTicket type.

How Has This Been Tested?

  • New BracketOrderTests (16 tests) covering the full handler+brokerage loop: leg placement and sizing, OCO cancel in both directions, single-bar-spans-both-legs, manual close, partial reduction, re-entry refusal, Cancel(), MoveStopLoss, limit entry, short bracket, validation. Disabling the engine hook makes 12/16 fail.
  • New BracketOrderRegressionAlgorithm (C# + Python): full lifecycle over SPY minute data; both languages pass with identical statistics.
  • Affected suites: transaction handler/manager and trading tests — 429 passed, 0 failed.
  • Live-paper run: legs placed on the live event path, OCO cancel and manual close verified.

Deferred

  • Percentage-based stop_loss/take_profit (absolute prices only for now).
  • Trailing-stop legs and move_stop_to_breakeven().
  • Live-trading restart recovery of bracket linkage (the registry is in-memory).
  • Brokerage-native OCO submission (the sibling cancel is issued on the fill event).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • Refactor (non-breaking change which improves implementation)
  • Performance (non-breaking change which improves performance. Please add associated performance test and results)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Non-functional change (xml comments/documentation/etc)

Checklist:

  • My code follows the code style of this project.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • My branch follows the naming convention bug-<issue#>-<description> or feature-<issue#>-<description>

QCAlgorithm.BracketOrder places an entry order plus linked stop loss /
take profit exit tickets (BracketOrderTicket). The transaction handler
enforces the linkage on every order event: the entry fill places the
exit legs sized to the filled quantity, a leg fill cancels its sibling
(deterministically stop-loss-first on a bar spanning both legs), an
unrelated order closing or flipping the position cancels the remaining
legs, and a new bracket is refused while one is still active for the
symbol.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant