State and order lifetime

State and order lifetime

The Context answers state questions locally (no engine round-trip), all maintained from the fill/cancel stream:

  • ctx.Position(symbol) – signed quantity and average price.
  • ctx.WorkingOrders() – submitted orders neither filled nor cancelled; under lifetime v2, every unresolved attempt with its status and quantities, attached children included (next subsection).
  • ctx.Flat(symbol) – true only when there is no position and no working order in the symbol. The safest entry gate there is.
  • ctx.Exposure(symbol) – position plus working orders in one struct. On a continuous series both count a working order on the series’ held contract against the logical symbol.
  • ctx.ActiveContract(symbol) – the dated contract a continuous series is currently served from, per logical symbol (see the strategy’s view of continuous futures).
  • ctx.Account() – the engine-maintained cash snapshot.
  • ctx.Instrument(symbol) – resolved instrument metadata (exchange, tick size, point value); populated from the universe-resolved push, which every run on the pb strategy wire receives before its first bar, a continuous run included (GLE-356). On the CSV strategy wires, v1 and v2, it stays empty (see “Declaring extra data and universe expansion”).
  • ctx.Calendar(symbol) – the exchange calendar the engine pushed for the symbol’s root, with its lookups (TradeDate, Day, Span, NextBoundary, Complete); false until a push arrives, which the engine sends only to a strategy that asks for it (see Strategy SDK lookups).
  • ctx.Now() – the current bar’s close time: simulation time.
  • ctx.Log/Logf – diagnostic lines over the wire (what you saw printed as [smacross] INFO: ...).

The working view under lifetime v2

A run that negotiated lifetime-v2 (the strategy returned the token from RequiredCapabilities and the engine offered it; see Capability negotiation) keeps a different working view: the attempt-indexed view of the order-lifetime programme’s increment I3 (GLE-372, sdk/strategy/working.go). The SDK selects it once, before OnInit, from the negotiated capabilities and from nothing else. The engine offers the token on a backtest or an emulated-live run under --lifetime v2 (Lifecycle turns) and on nothing else, so every run under the default profile keeps the v1 view, which is unchanged in every observable way: its contents, its map order and its wire bytes, and with them its one known gap, that when a bracket’s entry fills its children are live at the venue but absent from the v1 view (a Fill deletes the label and the children were no entries of their own). The v2 view holds them.

Attempts. Every physical submission under a label is an attempt. The SDK stamps Order.Attempt in place on each order OnBar returns and on each labelled attached child, 1-based and increasing per label, so the OrderResponse carries it; a value the strategy set is overwritten. A label’s counter survives the retirement of its attempts, so an attempt number is not reused within one strategy process, and an adopted frame advances each label’s counter to the attempt it carries, terminal entries and their children included, so a number the engine’s ledger already holds is not reused either. (ClientID, Attempt) names one attempt, and every event about it echoes both.

Entries. The view holds one entry per unresolved attempt: a submission OnBar returned, an attached child of one (an entry of its own from the moment its parent is tracked, Inactive until the parent fills), or an attempt restored from the live-mode adoption frame. Each is a WorkingOrder carrying, beyond Order and SubmittedAt, six fields that stay zero on a v1 run: Status, FilledQuantity, RemainingQuantity, ParentClientID (the parent’s label on a child), OrderRef (the engine’s durable reference once an event carried one) and CancelRequestID (the id of the strategy’s cancel request while it is pending). Entries are kept in the order the view first learned of them, each parent followed by its children, a resubmission under a label at the end; a status change keeps an entry’s place, and WorkingOrders() and Exposure present that order every time.

Statuses. An entry’s status is always a live one:

StatusAn attempt that
Submittingwas tracked and has no acknowledgement yet: the initial status. Under --lifetime v2 the engine answers every submission with an OrderUpdate, Working on acceptance, at the instant or (on a backtest whose turn budget ran out) before the next event’s fills and cancels, so an entry is here briefly; a v1 engine sends none, and an entry stays here until a fill or a cancel.
Workingwas acknowledged by an OrderUpdate, or is a child activated by its parent’s full fill.
PartiallyFilledhas executed in part and has quantity still working.
PendingCancelctx.Cancel addressed and the engine has not yet answered, or answered Pending or Unknown (a live venue that has not confirmed the cancellation).
Held, SubmissionUnknownan OrderUpdate reported so: SubmissionUnknown on a live run whose venue left a submission’s fate open (Lifecycle turns), Held when the engine’s race guard holds a submission behind a pending cancellation (The engine race guard).
Inactiveis an attached child whose parent has not filled.

A terminal status (Filled, Cancelled, Rejected, Denied, Expired; OrderStatus.Terminal()) does not appear in the view: reaching one retires the entry. Retirement decides the children: a parent that retires filled turns its Inactive children Working in place, and one that retires any other way (cancelled, rejected, expired) takes them with it. A child already active through its own evidence, its own fill or OrderUpdate, is untouched by its parent’s end. Children belong to one generation of their parent’s label: a resubmitted bracket’s children are the new attempt’s.

Transitions. By event:

  • a Fill adds its quantity to FilledQuantity; a stamped fill’s RemainingQuantity is authoritative (0 is a full fill), an unstamped one reduces the remaining quantity by the fill. Nothing remaining retires the entry filled; otherwise it becomes PartiallyFilled, or stays PendingCancel with PartiallyFilled as the status a refusal restores. An Inactive child’s own fill activates it. The position folds every delivered fill whether or not the view knows the attempt, as before.
  • an OrderCancel retires the entry, whatever its reason.
  • a CancelReject (the v1 answer to a cancel the engine could not honour) returns the label’s oldest PendingCancel entry to the status it had before the request and clears its CancelRequestID.
  • an OrderUpdate sets the quantities when it accounts for them (FilledQuantity + RemainingQuantity > 0; a status-only update leaves them) and then the status: a terminal one retires the entry (filled only for Filled), PendingCancel marks it pending, Unspecified leaves it, and while a cancel is pending any other status is recorded underneath as what a refusal restores.
  • a CancelResponse moves no quantities. Confirmed retires the entry (the OrderCancel that follows is then a no-op); Pending and Unknown keep it PendingCancel and take the response’s RequestID; Refused with terminal evidence in Status retires it, and otherwise ends the pending cancellation and restores the live status, the response’s when it carries one.

Which attempt an event addresses. A stamped event (Attempt not 0) addresses exactly the live entry with that label and attempt, and is a no-op when there is none: a late OrderCancel, OrderUpdate or CancelResponse for a retired attempt resurrects nothing and touches no other attempt, and a terminal event applied twice changes nothing the second time. An unstamped event (every event a v1 engine sends) addresses the label’s oldest live entry, with three exceptions: an OrderCancel with reason Rejected addresses the newest, because a rejection answers the latest submission; an OrderCancel with reason ParentCancelled addresses the label’s oldest Inactive entry whose parent is no longer in the view, and is a no-op when there is no such orphan; and a CancelResponse addresses the oldest PendingCancel entry, else the oldest live one. The orphan rule exists for a bracket cancelled and resubmitted in one OnBar: the parent’s own OrderCancel has already retired the first generation’s children, so the ParentCancelled notices that follow it find no orphan and leave the second generation’s children, which belong to the live new parent, in place for its fill to activate. The engine delivers the events of an older attempt before those of a newer one under the same label, so the oldest-first rule addresses the right generation. No event creates an entry: the view mirrors what the strategy submitted or adopted, and an event about an unknown label or attempt changes nothing in it.

Cancelling. ctx.Cancel(clientID) is valid in every live status, Submitting included: a strategy need not wait for a Working acknowledgement. It addresses the label’s newest live attempt that is not already pending cancellation, else its newest live attempt, marks it PendingCancel at once and gives it a request id of the form <label>/<attempt>/c<n>, where n counts the strategy’s cancel requests (a re-request replaces the id). The request goes onto the wire with the attempt and the id (zero and empty on a v1 run, whose envelope is unchanged) and is sent even when no attempt of the label is live; the engine answers it. The attempt stays in the view, counted by Flat and Exposure, until the engine’s answer retires it (an OrderCancel, a confirmed CancelResponse) or refuses it (a CancelReject, a refused CancelResponse).

Observers. OnOrderUpdate and OnCancelResponse run after the view has moved on their event, exactly as OnFill runs after the position fold: a handler reads the view as the event left it. On a v1 run the view ignores both events and only the handler sees them, as in I2.

Flat and Exposure. Every entry is a commitment: a submission awaiting its acknowledgement, a pending cancellation, a held order, an inactive child. ctx.Flat(symbol) is false while any of them rests on the symbol (active-contract attribution on a continuous run included) and ctx.Exposure(symbol).WorkingOrders lists them in view order.

Copies. WorkingOrders() and Exposure return copies the strategy may keep or change, Order.AttachedOrders and their own children included; nothing a strategy writes to a returned slice, or to the order slice it returned from OnBar once OnBar has returned, reaches the view.

Adoption and corporate actions. OnAdoptedState under lifetime v2 replaces the view with the frame: each adopted order and its children in frame order, with its status (Working at top level and Inactive when nested if the frame carries none), quantities, attempt, reference and submit time; a terminal frame entry gets no entry but still advances its label’s counter; a v1 frame seeds Working entries with attempt 0 and the whole quantity remaining, which an unstamped event reaches. A split, rename or merger rewrites each attempt’s order and quantities on the symbol and keeps its identity, status and cancel-request state; an attempt whose remaining quantity floors to zero is dropped with its inactive children, as the engine drops the order.

Lifecycle turns

algo run --lifetime v2 selects the v2 order-lifetime profile for a backtest (increment I4 unit 4a of the order-lifetime programme, GLE-377; engine/lifeturn.go, engine/lifetimehost.go, engine/sim/attempt.go and sdk/strategy/lifetime.go) and for an emulated-live run (unit 4b, GLE-378; engine/liveturn.go; see “On a live run” at the end of this section). The default, --lifetime v1, is the legacy profile and is byte-identical to before: the v1 exchange, the v1 events (attempt 0, no request id), no OrderUpdate, no CancelResponse, no turn, and the KBD masters unchanged. --lifetime-turns N is the turn budget per instant under v2 (0, the default, selects 3). Under v2 the engine offers lifetime-v2 in Init, which the strategy must accept (Capability negotiation), and the strategy runs on the pb wire or on stdio-csv-v2: stdio-csv-v1 has no grammar for the capability or for the messages below, and the host (StdioHost, which carries the turns as the optional LifetimeHost) refuses each of them there with one text naming the pb protocol. The stdio-csv-v2 grammar carries them line for envelope, and a v2 run offers it ahead of v1 (Negotiating stdio-csv-v2). The profile runs on the plain backtest path and the emulated-live path in this release and is refused before any spawn elsewhere: on a continuous run (a bare root or a :cont composite) with engine: order lifetime v2 is not supported on a continuous run (<symbol>); long-lived orders across rolls are GLE-200 I15, and under --schema TRADES with engine: order lifetime v2 needs a strategy run (schema BARS, not TRADES). Another value of --lifetime is refused naming both valid ones (engine: unknown order lifetime "v3" (want v1 or v2)), a negative budget naming the default. In a run-config file the two flags are the data keys lifetime and lifetime-turns (The admission policy).

The exchange at an instant. A bar is answered as before: the queued cancel requests, then one OrderResponse for the bar’s close. The engine applies the cancels first, then the submissions, and answers each one at the instant. Every submission gets an OrderUpdate: Working with the whole quantity remaining on acceptance; Denied with the simulator’s reason and nothing remaining on a denial, whose rejection cancel still follows before the next bar’s events, now stamped with the attempt. Every cancel request gets a CancelResponse: Confirmed, with the attempt’s status Cancelled and its remaining quantity, when the order was cancelled; Refused otherwise, with the terminal status, attempt and reference of the order it found as evidence, or without evidence when the label, or the attempt, was not submitted this run. The responses go out in request order, then the updates in order order, each on its own envelope, and each carries the attempt, the simulator’s reference (sim-N; a denied attempt has one too, since a denial creates a Rejected record, so a later cancel of it answers already terminal (rejected) with the status Denied) and the instant as its timestamp. The evidence status is the venue’s, mapped onto the wire’s, with one exception since unit 4b: a venue’s Rejected is Denied on every path, because every rejection the engine knows of arrives as a denial and was announced to the strategy as Denied, so the attempt’s later evidence says the same; the wire’s Rejected is kept for a venue that rejects an order it had accepted. A v2 run sends no CancelReject: a refused cancel is the classified response, plus the operator line the v1 loop prints (algo: cancel of order "lim1" from instance ES rejected: <reason>), and the book is untouched. The outcome Pending and the status SubmissionUnknown do not occur on the simulated path; a live run produces both (below).

Attempt-addressed cancels. A cancel request names the attempt the SDK stamped on it (ctx.Cancel, above), and the simulator cancels exactly that attempt (Simulator.CancelAttempt): a request for attempt 1 of a label reaches attempt 1 alone, even once attempt 2 has been submitted (the race guard admits attempt 2 only as attempt 1’s successor; The engine race guard). Attempt 0 keeps the v1 meaning, the label’s newest order, and the response then names the attempt the simulator resolved. A request for an attempt that has ended, whether the simulator compacted it out of the book, denied it at validation or settled it at submission (an MOC), is Refused with that record’s status as evidence (order "lim1" is already terminal (filled)); one for a label or attempt not submitted this run is Refused without evidence (unknown client order id "x" attempt 2 (never submitted this run)); an empty label is refused as addressing nothing. A refusal changes nothing in the book. The queued OrderCancel of a confirmed cancel carries the request id (the parent-cancelled notices of its children do not), and every fill and cancel the simulator produces under v2 carries its attempt: parent and child fills, OCO sibling cancels, Day expiries, explicit cancels and the shutdown sweep.

What grants a turn. An answer the strategy may want to act on grants it a lifecycle turn before the clock advances: every CancelResponse (a confirmed cancel releases a replacement; a refused one leaves intent unmet) and every OrderUpdate whose status is neither Working nor Held (a denial, the race guard’s included). The Working acknowledgement of an accepted submission grants nothing, and nor does the Held one of a submission the race guard holds (The engine race guard), since the strategy just submitted that order, so a bar whose commands produce acknowledgements alone ends with them delivered and no turn. The engine writes one LifecycleTurn{at, turn, budget} envelope, at the instant and turn 1-based within it; the strategy answers exactly as it answers a bar, its queued cancel requests and then one OrderResponse whose for_bar is at; the loop applies and answers those commands in turn. A turn answered with no order and no cancel request ends the instant. So a replacement can follow a confirmed cancel before the next matching bar: bar 1 returns the cancel of lim1, the Confirmed response grants turn 1, the turn returns lim2, whose Working update ends the instant, and bar 2 fills lim2, delivering lim1’s OrderCancel (with the request id) before the fill.

The budget. Turn k is granted while k is at most the budget. After each step’s commands are applied the engine checks, in this order: no outcome ends the instant; outcomes that grant nothing are delivered and end it, however the budget stands; only then, when the budget is exhausted, the step’s outcomes, acknowledgements included, are deferred whole to the next event (on a backtest; a live run delivers them at once, below), with one line on stderr, algo: lifecycle turn budget 3 exhausted at <instant> for instance <label>: N outcome(s) deferred to the next event. Deferred outcomes are delivered at the top of the next bar, before the previous instant’s rejection cancels and the bar’s fills and cancels, or at the end of data before the shutdown sweep, in order and before anything else of that instant. Budget 1 therefore always defers turn 1’s granting outcomes, so 2 is the smallest budget under which a turn’s own outcome can grant another turn; the default is 3. The engine re-admits nothing: a strategy that re-emits a denied order on every turn is stopped by the budget (under budget 2, three Denied updates, the last deferred to the next bar, then the three stamped rejection cancels).

The SDK’s side. A strategy decides on turns by implementing TurnHandler, OnLifecycleTurn(ctx, t LifecycleTurn) ([]Order, error), where t carries At, Turn and Budget. The SDK moves ctx.Now() to t.At first, whether or not the strategy has the handler; no bar is appended, no series moves and BarCount is unchanged. The handler reads the working view as the engine’s answers left it (the updates and responses arrived, and moved the view, before the turn), may queue cancels with ctx.Cancel, and returns orders that are labelled and tracked exactly as OnBar’s are: a label resubmitted in a turn gets its next attempt, a blank label the collision-safe auto id. The queued cancel requests go out before the response, an observer’s included, whether or not the strategy has the handler; a strategy without one answers every turn with an empty response. A handler error ends the run as an OnBar error does. The lifecycle trace records a turn’s submissions and cancel requests as strategy records through the same taps as a bar’s, at the turn’s instant; its format is unchanged.

On a live run. --mode live --lifetime v2 (unit 4b, GLE-378) runs the same driver, lifetimeLoop, over venue-backed operations (engine/liveturn.go) in place of the simulator’s, so the exchange, the turns and the budget are the ones above, with the differences below. A submission goes through the live loop’s admission and submission sequence, the one the v1 loop uses: the attached-order and time-in-force refusals, the raw-frame translation, the ledger record before the venue call, the venue call and the reconciler registration. Its update is Working on acceptance and Denied with the venue’s reason on a denial, whose rejection cancel is keyed by the ledger reference so the ledger records the reject and maps it back to the label; a submission whose fate the venue left open is SubmissionUnknown, with the whole quantity remaining, and the reconciler resolves it (without a ledger there is no reconciler, and the run fails as a v1 run does). Updates and responses carry the ledger’s canonical reference as OrderRef, or the venue’s order id without a ledger. The teardown is the live loop’s, the same on both profiles (Tickets and cancels at the IBKR venue). Since I6 unit 3a the transport scheduler decides when each submission and cancel request reaches the venue (The transport scheduler); a request it queues is answered when it is sent, or at the end of the run.

Delivery at once. A live run defers nothing. When the budget is exhausted, the step’s outcomes, acknowledgements included, are delivered at once and grant no turn, with one line on stderr, algo: lifecycle turn budget 1 exhausted at <instant> for instance <label>: N outcome(s) delivered without a turn; the strategy decides on them at the next event. A deferred Working acknowledgement could otherwise land after the venue’s fill of the same attempt, which a venue produces on its own clock. Within the budget, turns are granted as on a backtest, and a Pending or Unknown response grants one too: it tells the strategy to wait. Holding a same-side replacement behind a pending cancel is the admission race guard’s (The engine race guard).

Attempt resolution. A cancel request addresses the attempt it names; attempt 0 resolves the label’s latest submission, and the response names the attempt resolved. The live loop knows an attempt by the order id the venue assigned it, so a request whose attempt has none (a label or attempt not submitted this run, or a label whose latest submission was denied or left open) is Refused without evidence, with no venue call and no ledger write: unknown client order id "x" (no broker order recorded for it this run), or unknown client order id "x" attempt 2 (no broker order recorded for it this run) when the request named the attempt. At attempt 0 this holds even when an earlier attempt of the label is still working. Here the live path differs from the simulator, which keeps a record of a denied attempt and refuses with it as evidence.

The request. For an attempt with an order id the loop records the ledger’s durable cancel intent (The order ledger) before the request leaves; a failure to record it is fatal (engine: cancelling order "L" for instance AAPL: engine: recording cancel request for <reference>: ...). The venue’s classified answer (Tickets and cancels at the IBKR venue) then passes through to the CancelResponse: Pending with the status PendingCancel and the venue’s text, Refused with the venue’s text, Unknown with the failure’s. A venue error, such as a transport failure, is an Unknown answer (cancel of broker order <id> unresolved: <error>), not an error of the run. A Pending answer prints nothing, an Unknown one algo: cancel of order "L" from instance AAPL unresolved: <reason> and a Refused one the v1 line quoted above, ending rejected: <reason>. A Confirmed, Pending or Unknown answer marks the order requested, so its cancel, when the venue reports it, is the strategy’s own and not a venue expiry. A Refused answer clears the intent only when this request recorded it and the venue did not report the order already pending cancellation, so an earlier request’s intent stands; a failure to clear it is fatal too. The IBKR adapter reports no remaining quantity on a cancel answer; the OrderCancel that follows carries it.

The post-submission poll. After the instant’s commands and turns, the loop polls the venue once more when an MOC was accepted at the instant or any cancel request was handled, addressed or not, which is the v1 rule: the events found are recorded in the ledger at once and delivered with the next bar’s batch, cancels ahead of fills (the simulator’s queue order), so a cancel the venue enacted at the instant reaches the strategy before the next bar’s fills.

Against a backtest. The emulator cancels synchronously but acknowledges with IBKR’s body, so a v2 emulated-live run equals the v2 backtest of the same strategy (fills, cash, positions, the sequence of events and GLE-195’s MOC contract) except that its cancel response is Pending with the status PendingCancel where the simulator’s is Confirmed; the OrderCancel follows from the post-submission poll. A same-side replacement sent with the cancel is therefore Held and then, released by that poll at the same bar, Working, where the backtest’s is Working at once (The engine race guard). The equality holds while a bar sends at most the transport scheduler’s five requests at its defaults; a run that sends more queues the excess to the next bar (The transport scheduler).

Known limits on the live path.

  • The teardown sweep cancels every Working order the account reports, another deployment’s included; restricting it to the ledger’s own orders is the ownership-aware shutdown of I9.
  • A live Explicit cancel carries no RequestID yet. I8, whose cancel-intent records carry the request, adds it; the working view addresses the cancel by label and attempt meanwhile.
  • The live attempt map (byAttempt, with each label’s latest attempt) keeps an entry per submission for the whole run, so it is bounded only by the run’s submissions; attempt lifetime across restarts is I8’s.
  • Host I/O that ignores the run’s cancellation (a strategy that stops reading blocks the engine’s host calls, v1’s SendBar included) and a concurrent or bounded drain of strategy output are I4 unit 4c, GLE-416; GLE-446 records the same drain for the event bursts of every stdio wire (The engine host).

The engine race guard

Under --lifetime v2 every new order passes the engine’s admission race guard before it can reach the venue (increment I6 unit 1 of the order-lifetime programme, GLE-419; engine/raceguard.go, engine/lifeturn.go and sdk/strategy/raceguard.go). It protects a strategy that cancels attempt A and submits a same-side B on the same symbol, in the same reply or a later one: a stop moved to a new level, an entry re-priced. A live venue acknowledges a cancel as Pending (Tickets and cancels at the IBKR venue) and A can still fill until the venue reports it cancelled, so a B sent at once can execute beside A: a long position protected by a sell stop A and its replacement B goes through zero to short. Owner decision D2 puts the wait for terminal evidence in the engine, not only in the strategy’s helper (The desired-order controller): the guard holds B until A has ended, and rejects B if A executes first.

The guard sits at the driver’s one admission point, apply (Lifecycle turns), so it applies on every lifetime-v2 path: the plain backtest and the emulated-live run today, on the pb wire and on stdio-csv-v2 alike (a v2 batch is read into the same reply), and a continuous run (I15) when it gains v2, since its acceptance requires the same driver. A v1 run has no driver and no guard, and is byte-identical. The simulator acknowledges cancels synchronously, and the driver applies a reply’s cancels before its orders, so a backtest holds nothing in the ordinary case and the KBD masters are unchanged. The one backtest hold is a cancel, in a later turn, of an MOC that settled at its submission: the simulator refuses it with Filled as evidence, the guard waits to observe that fill, and the fill, delivered at the next bar, rejects the held order with cancel_race (with hold_shutdown when the request came at the last bar).

Direction, not exposure. The guard classifies a new order O by direction, not by an exposure scalar. Its blockers are the live attempts with a cancellation outstanding (pending, unknown or refused; below) that are on O’s symbol and side, or carry O’s label. With no blocker O is admitted and submitted exactly as before the guard; otherwise a refused blocker rejects it (cancel_refused), else an unknown one rejects it (cancel_unknown), else O is held. An order on the opposite side under another label is therefore admitted at once, even while a cancel on its symbol is pending: a protective stop placed while an add is being cancelled cannot add to the exposure the pending cancel already reserves, and the caps of the admission policy (unit 2) count the add as still executable. An unlabelled order matches by symbol and side only.

The same-label successor rule. An order with a label first meets its label’s previous attempt, the most recently tracked attempt under the label that has not ended, an attached child included. If that attempt is held, or live with no cancellation requested, the order is rejected as duplicate_live_label. If it has a cancellation requested, in the same reply or earlier, the order is its successor and the predecessor is one of its blockers, so the successor is held while that cancel is pending, on either side, and rejected when the cancel was refused or its outcome is unknown. Cancel-and-replace under one label in one reply is the ordinary form: the cancel is applied first, so the successor is admitted at once on a backtest and held until the cancel resolves on a live run.

The cancel answers. A cancel request that reaches the venue is answered as before (Lifecycle turns), and the guard then records the answer for the attempt the request addressed (the attempt it named, or for attempt 0 the attempt the answer names; resolved as an event is, below):

AnswerThe attempt’s cancellation state
Confirmed (the simulator’s; the IBKR adapter does not confirm)none: the attempt ends, with its children
Pendingpending
Unknownunknown, unless already pending (an earlier accepted request stands)
Refused with the venue’s status PendingCancel, or a terminal one (Filled, Cancelled, Rejected), as evidencepending: the venue says the attempt is ending or has ended, and the guard waits to observe that end and any execution before it
Refused with the venue’s status Workingrefused
Refused without evidencerefused, unless already pending

Only an answer whose outcome is Refused is read as a refusal; an answer with no outcome (the zero value) changes nothing. A refusal belongs to its step, one reply (a bar’s commands or one turn’s), or, for a cancel request the transport scheduler queued, the observation that sends it (The transport scheduler). Within the step the refused attempt is a blocker, so a same-side order or a successor in the same reply as the refused cancel is rejected with cancel_refused, and so is every order held behind the attempt. When the step ends, on every return path, the state returns to none: the refusal resolved the request, and the attempt is live with no cancellation outstanding. At the next step, the turn the refusal grants included, a same-side order is an ordinary submission. An unknown state persists until the attempt is observed ended or a later request for it is accepted; meanwhile every same-side order and every successor is rejected with cancel_unknown, and opposite-side orders are admitted. Holding them instead would end only in timeouts.

Withdrawing a held order. A cancel request that addresses a held attempt (its label and attempt; for attempt 0, the label’s most recent top-level attempt that has not ended, when that one is held) withdraws it without reaching the venue: the attempt ends with its children, and the response is Confirmed with the status Cancelled, the held quantity remaining, the instant as its timestamp and no OrderRef. A request for a held attached child alone withdraws nothing; it goes to the venue.

Release and the watermark. A held order is released only when every attempt it waits for is terminal with nothing remaining, having executed nothing while the order was held, and the execution watermark covers that end, so a fill reported after the status cannot be missed. The watermark is the batch of the poll that observes the end. The loops hand the guard every venue event once, in one batch per observation of the venue, and the guard applies a batch whole: every execution first, whatever the batch’s order, then every cancel (whatever its reason, a rejection included), and only then does it decide the held orders. The IBKR adapter’s poll reads the live orders before the trades (Tickets and cancels at the IBKR venue), so every execution that preceded a terminal status it reports is in the same batch, and a predecessor’s end observed in a batch releases a held order only when no execution of the predecessor is in that batch or was observed during the hold. A venue that reported an execution in a later poll than its order’s terminal status would defeat the rule; that would breach the adapter’s contract. The reconciler’s terminal events wait for the next poll (the review panel’s fix): reconciler.Step reads the orders after the bar’s poll has read the trades, so a terminal status it reports can postdate an execution that no batch has carried yet, and the guard observes those events with the next bar’s batch, after that poll’s executions.

Where the loops observe. A backtest observes at each bar’s close, after the simulator’s events for the bar are delivered and before the bar’s decision; a release is submitted there and counted, and its denial’s rejection cancel is queued like the bar’s own. A live run observes at each bar’s close after the position reconciliation and after the submitter has moved to the bar, before the decision: a release is submitted on the bar it belongs to, and a released MOC, which settles at submission, does not trip the position check before its fill is recorded. The live loop observes the post-submission poll too, so a cancel the venue has already enacted (the emulator’s) releases the held order at the same bar, as the simulator’s synchronous cancel admits it, and a release that accepted an MOC adds one more poll, as the bar’s own MOCs do. An emulated-live v2 run therefore keeps the backtest’s fills, cash and positions: where the backtest’s replacement is Working at once, the live run’s is Held and then Working at the same bar.

A race. Any execution of a predecessor, partial or full, while its cancellation is pending rejects every order held behind it with cancel_race. The execution is delivered as usual, and the strategy re-decides at its next decision against the new position and remaining quantity: outcomes decided at an observation are delivered at once and grant no turn. A full execution also ends the attempt. Every execution moves its symbol’s position in the oracle (below), whether or not the guard tracks its attempt.

Held is an order state. A hold is answered with an OrderUpdate whose status is Held, with the whole quantity remaining, the instant as its timestamp, a reason that names the attempt it waits for (held: waiting for the pending cancellation of order "A" attempt 1 (sell ES)) and no OrderRef; no line is printed. The SDK’s working view keeps the entry as Held (The working view under lifetime v2), so ctx.Flat is false and Exposure lists it, and the oracle counts it. A held order has no broker correlation until its release: no venue id, no ledger reference and no OrderRef, because the live loop allocates the reference when it submits. A crash during a hold therefore leaves nothing at the venue to recover, and the strategy re-decides after the restart. Release re-runs admission serially, in hold order, which is the strategy’s decision order: in this unit that is the path a new order takes to the venue (on a live run the transport scheduler, then the live loop’s attached-order and time-in-force refusals, the ledger record and the venue call), where the admission policy (unit 2) inserts its caps. A release is answered as an admitted order is (Working, Denied with the venue’s reason and its rejection cancel, or SubmissionUnknown), stamped with the instant of the release, or, when the transport scheduler queues it, Held until it is sent (The transport scheduler). Held, like Working, grants no turn: the strategy just submitted that order.

Bounded holds. Holding is a state change, not a wait inside the loop: the held orders are decided again, in hold order, at each observation and after the cancel requests of each step that has any. --lifetime-hold-timeout (RunConfig.LifetimeHoldTimeout, default 5m) bounds a hold: one as old as the timeout is rejected with hold_timeout. The timeout is checked last, so a release decided at the instant a timeout falls due wins. It runs on the run’s event clock, the instants the loops pass (bar closes), not the wall clock, so a backtest and an emulated-live run of the same bars time out at the same bar; on daily bars a hold that the post-submission poll does not resolve times out at the next bar. The live path is bar-stepped today; the plan’s operational clock for live holds, recorded as a replay input, arrives with a real-time runner. A negative timeout is refused before any spawn (engine: order lifetime v2 needs a hold timeout above zero (0 selects the default 5m0s), got -1s), and a v1 run ignores the flag. At the end of the run every order still held is rejected with hold_shutdown, in hold order, at the last bar’s close, and the rejections are delivered before anything is cancelled: before the backtest’s end-of-data cancellation (and the lifecycle trace’s working-set snapshot) and before the live teardown sweep. On a live run a failure delivering them does not skip the sweep; the first error is returned after it.

Which attempt an event addresses. The guard tracks one attempt per admission or hold, not one per label and attempt number: a top-level X attempt 1 and a bracket child labelled X with attempt 1 are two. An event (a fill or a cancel) or a cancel answer addresses the live attempts with its label and, when it carries one, its attempt number. Among them the one whose venue id (the broker order id of its submission, which a child shares with its parent) equals the event’s wins, a top-level attempt first, then the oldest. With no such candidate, an event whose venue id names an attempt of the label that has already ended addresses nothing, so a confirmed cancel’s echo cannot end the label’s live successor and release what is held behind it; otherwise the top-level candidate wins, then the oldest. The guard remembers the label and venue id of every ended attempt for the rest of the run. "" is no label: an order, a request or an event with an empty label matches no label and addresses no attempt (an unlabelled order still has blockers by symbol and side).

Attached children. The guard tracks each labelled attached child from its parent’s admission or hold, on the child’s own symbol or its parent’s when it names none. A child is held, live and ended with its parent’s admission, release, rejection, withdrawal, confirmed cancel and venue denial; otherwise it ends on its own events, a fill that completes it or any cancel of it, so it outlives its parent’s observed fill or cancel. A live child is its label’s previous attempt and can be a blocker; its own label is not checked when its parent is admitted. Live brackets are refused today, so children matter on a backtest only.

The reasons. The guard’s decisions reach the strategy as OrderUpdate events whose Reason begins with a token and ": "; the rest of the text is for people, not for parsing. The SDK exports the tokens (sdk/strategy/raceguard.go):

TokenSDK constantStatusMeaning
heldGuardHeldHeldheld behind the pending cancellation of an attempt on the same symbol and side, or of the label’s previous attempt; released (Working) once every such attempt has ended without executing
duplicate_live_labelGuardDuplicateLiveLabelDeniedthe label’s previous attempt is held, or live with no cancellation requested
cancel_raceGuardCancelRaceDeniedan attempt the order was held behind executed, in whole or in part, while its cancellation was pending
cancel_refusedGuardCancelRefusedDeniedthe venue refused the cancellation the order depended on, and the attempt is still working
cancel_unknownGuardCancelUnknownDeniedthe outcome of the cancellation the order depended on is unknown
hold_timeoutGuardHoldTimeoutDeniedthe order was held for the run’s hold timeout
hold_shutdownGuardShutdownDeniedthe run ended while the order was held

A guard denial carries nothing the venue would have given (no OrderRef, nothing remaining, an empty BrokerCode, which is the venue’s code), and no OrderCancel{Rejected} follows it, since the attempt reached neither the venue nor the ledger; a venue denial is followed by one. A strategy that hears denials only through OnCancel therefore does not hear the guard’s. The operator hears the venue denial’s line, here algo: order "B" (ES) from instance ES denied: cancel_race: order "A" attempt 1 (sell ES) executed while its cancellation was pending. Within one step the outcomes are, in order: the cancel responses in request order, then the updates of the held orders the step’s cancels decided, in hold order, then the updates of the step’s own orders; a guard denial among them grants a turn, as a venue denial does. Outcomes decided at an observation or at shutdown are delivered at once on both paths and grant no turn. The run’s order count, the report’s Orders figure, counts the submissions the venue answered: a hold counts nothing until its release, and a guard denial nothing (nor, on a live run, a pacing refusal; The transport scheduler).

The exposure oracle. lifetimeLoop.exposure(symbol) returns an exposureInterval for one symbol: Position, the signed sum of every execution the guard observed on the symbol this run, tracked or not, and Buys and Sells, by side, the remaining quantity (its quantity less its executions, at least 0) of every tracked attempt on the symbol that has not ended: held, working, partially filled, pending cancellation, or a submission whose outcome is unknown. A labelled child counts in full, as a contingent commitment (an OCO pair counts both legs); an unlabelled child is not tracked. An attempt the guard rejected, the venue denied, or that was withdrawn, cancelled or fully filled counts nothing. Low(), Position - Sells, and High(), Position + Buys, are the ends of the interval of positions the symbol can reach. A nil loop (a v1 run) answers the zero interval. The admission policy (unit 2) calls it at the guard’s two admission points, a new order and a release. There is one loop per strategy instance, so an account-level cap sums across instances.

Known limits and follow-ups.

  • A parent’s blockers ignore its children’s sides: a buy entry with a sell-stop child is admitted while a sell stop’s cancel is pending, and once the entry fills both sell stops can execute and cross zero. The guard should treat a bracket as blocked by any blocker of its children’s sides, or hold the bracket. This must close before live brackets land.
  • Every-exit teardown is I9’s. Only the run’s normal end rejects the held orders, answers what the transport scheduler still queues and sweeps the venue; a host, poll or venue error, or the run’s cancellation, returns without any of them, as before I6. I9’s ownership-aware halt and shutdown ends holds, answers the transport’s queues and runs a best-effort sweep on every exit, and its halt rejects held orders before any cancellation, as shutdown does here.
  • The lifecycle trace (The order lifecycle trace) does not record guard denials; a release is recorded when it is submitted. They should be added if the trace is to explain every engine refusal.
  • The desired-order controller should re-send a cancel whose answer was Unknown: a later Pending answer turns the guard’s cancel_unknown rejections into a hold.
  • The guard tracks only the attempts admitted this run: after a restart, adopted orders are not predecessors and their positions are not in the oracle. Recovery (I8) must seed both.

The rest of I6. Unit 2 is the admission policy (The admission policy): the engine’s deterministic caps at both of the guard’s admission points (working-order counts by owner and instrument that count held, pending-cancel and unknown attempts; maximum quantity; exposure on both ends of the oracle’s interval), the event-time rate budget, their rejection reasons and the configuration surface, the hold timeout’s config key included. Two orders held behind one cancel then cannot together exceed a cap: at release the cap admits them in hold order while they fit. Unit 3a is the transport scheduler (The transport scheduler): pacing on the operational clock (per-attempt FIFO, cancels first, a bounded queue whose overload is visible, and a cancel of a submission not yet sent withdraws it and ends its attempt, as the guard’s withdrawal does for a held one), with conservative, configurable defaults because IBKR’s Web API reference publishes no pacing figures. Unit 3b (GLE-436) is IBKR’s order-reply-message policy (Order reply messages), which replaced the adapter’s blanket confirmed: true with an allow-list of message ids per deployment: any other reply is declined and reaches the strategy as a denial carrying IBKR’s text (reply_declined). Unit 3 does not depend on unit 2. With unit 3b, I6 is complete.

Tests. engine/tournament_guard_spec_test.go (40 tests) drives the guard through runLiveLoop over the scripted venue (the cancel-and-replace that does not cross zero, with no fill, a full fill and a partial fill during the pending cancel; the opposite-side admission; the unacknowledged cancel that times out), over the emulator (a release at the backtest’s bar), through runBarLoop over the simulator, and on the driver over scripted venue operations; engine/raceguard_review_test.go holds the review panel’s gap tests and the tests of its fixes; sdk/strategy/tournament_guard_spec_test.go pins the tokens and the view’s handling of a held order; and cmd/algo/tournament_guard_spec_test.go the flag. Six existing v2 tests were re-pinned for the same-label rule: their second attempt of a label is now the first’s successor in one reply, every addressing assertion kept, and the live-against-backtest comparisons read a held-then-released order as the backtest’s Working one.

The admission policy

Under --lifetime v2 an order the race guard admits or holds then passes the engine’s admission policy, and so does a held order when the guard releases it (increment I6 unit 2 of the order-lifetime programme, GLE-430; engine/admission.go, engine/lifeturn.go and sdk/strategy/admission.go). The policy enforces the limits the run configures, each per strategy instance and each off at 0, the default: caps on the working orders, on the quantity of one order and on the position each symbol could reach, and an event-time budget of new orders. It reads the guard and its exposure oracle (The engine race guard) and changes neither. Its decisions are a function of the event inputs (the strategy’s orders, the cancel answers, the executions and their instants), so it decides alike on the plain backtest and the emulated-live path. A v1 run has no driver and no policy, and refuses a limit (below).

The caps. Each applies per strategy instance, and 0 turns it off:

FlagLifetimeLimits fieldWhat it limits
--max-working-orders NMaxWorkingOrdersthe instance’s working orders: every attempt the guard tracks that has not ended (held, working, partially filled, pending a cancellation, or a submission whose outcome is unknown), each labelled attached child one
--max-symbol-working-orders NMaxSymbolWorkingOrdersthe same count on each symbol the order’s legs are on
--max-order-quantity QMaxOrderQuantitythe quantity of the order and of each attached order, labelled or not
--max-position QMaxPositionon each symbol the order’s legs are on, the exposure oracle’s interval with the order added: the positions the symbol could reach if every working and held order executed, checked only at the end the order extends

An order brings one leg for itself and one for each labelled attached child, on the child’s symbol or its parent’s when it names none. An attempt that has ended (filled in full, cancelled, denied by the venue, rejected or withdrawn) counts nothing. The guard does not track an unlabelled attached child, so it adds no working order and no exposure; its quantity is still checked.

Only the end the order extends. max-position checks the high end, Position + Buys, against the limit when the order’s legs add a buy on the symbol, and the low end, Position - Sells, against minus the limit when they add a sell; an order with legs on both sides is checked at both, the high end first. A reducing order therefore passes: a sell is admitted while the high end is past the limit, and a buy while the low end is. An end can already be past the limit when executions the guard does not track have moved the position (an order adopted after a restart, I8); an exposure refusal then says so (below).

Held orders count. Every cap counts held orders. The base of each count, and of each interval, is every attempt the guard tracks that has not ended, held ones and those with a cancellation pending included (a pending-cancel attempt can still execute), and the new order’s legs are added to it. An order the guard is about to hold is checked without the attempts it is held behind: the live attempts whose cancellation is pending on its symbol and side, or under its label (their attached children still count). The guard releases it only after they have ended with nothing executed, and an execution of one rejects it, so the two cannot both execute. This is what keeps simulation and emulation in parity: the simulator’s synchronous cancel has already ended the predecessor when the backtest admits the replacement, and the live run, which holds the replacement, decides it as if the predecessor had ended. It also lets a strategy at a working-order cap move a stop on a live run without a spare slot.

A release is re-checked. When the guard releases a held order, the policy runs the caps again in the original decision order, which is hold order. The orders released before it in the same pass are live and count, and so do the held orders before it that stay held; the held orders after it, decided after it, are left out with their children. The released order counts with its children that have not ended, each at its remaining quantity. Two orders held behind one cancel therefore cannot together exceed a cap. The second is refused as it is held when the two would not fit; when the state moved while they were held (an execution the guard does not track), the release admits them in hold order while they fit and refuses the first that does not. While an order after it stays held, the interval projected with every held order can briefly pass a cap; its own release re-checks it, so what reaches the venue stays within the caps. A held order the policy refuses at its release ends with its children and does not reach the venue.

Brackets. A labelled child counts in full on its own side, as the oracle counts it: it is a contingent commitment, and both children of an OCO pair are reserved. A bracket’s children can execute only after its entry has, but the policy does not net them against it, so a buy entry of 5 with a take-profit and a stop of 5 each takes a flat position’s low end to -10. A symmetric bracket therefore needs a max-position of at least twice its child side. The plan does not discount OCO alternatives without a verified guarantee; netting a bracket’s children against its own entry is a follow-up.

Unlabelled orders. The guard tracks an order with an empty ClientID until the run ends, because no event can address it, so an unlabelled order holds a working slot for the rest of the run. The Go SDK labels every order and child it sends; only a strategy that writes the pb wire itself can send one. The first unlabelled order the policy accepts while max-working-orders or max-symbol-working-orders is set writes one line to stderr, once per run:

algo: note: an unlabelled order counts against the working-order limits until the run ends (no event can end it); give every order a ClientID

A refused order writes nothing, and the order itself is unaffected.

The rate budget. --max-submissions N with --submission-window D, set together, allows N new orders in any window D of event time. Each order the policy accepts, admitted or held, takes a place at its instant, the bar close the driver passes (every turn at one instant shares it); a bracket is one place. An order is refused when N places are less than one window old, so a place exactly one window old has left the window. An order the guard or a cap refuses takes no place; a held order keeps its place whether it is later released, refused or withdrawn; a release takes no place and is not refused by the budget. Cancel requests are not limited: a budget must not block flattening, and a burst of cancels is the transport scheduler’s to absorb on a live run, which paces them ahead of every submission and refuses none for overload (The transport scheduler). Because the places are taken at the strategy’s decisions, the moment the venue resolves a cancellation does not move the budget. The policy keeps at most N places and grows that store with the orders it accepts, not with N.

The reasons. A policy refusal reaches the strategy as a guard denial does (The engine race guard): an OrderUpdate with status Denied, the order’s quantity, nothing remaining, the instant as its timestamp, no OrderRef and no OrderCancel{Rejected} after it, and a Reason that begins with a token and ": ". It is ordered, delivered and granted a turn as a guard denial is, makes no venue call (so the report’s Orders figure does not count it), and prints the operator’s denial line, here algo: order "B" (AAPL) from instance AAPL denied: cap_quantity: quantity 11 exceeds the limit of 10 per order. The SDK exports the tokens as algolang.Policy* constants (sdk/strategy/admission.go):

TokenSDK constantReason (Go format)
cap_quantityPolicyQuantitycap_quantity: quantity %d exceeds the limit of %d per order
cap_working_ordersPolicyWorkingOrderscap_working_orders: %d working orders with this one, above the limit of %d
cap_symbol_working_ordersPolicySymbolWorkingOrderscap_symbol_working_orders: %d working orders on %s with this one, above the limit of %d
cap_exposurePolicyExposurecap_exposure: the position on %s could reach %d with this order, beyond the limit of %d
rate_budgetPolicyRateBudgetrate_budget: the budget of %d submissions per %s is used up

At the low end an exposure reason names the negative reach and minus the limit (cap_exposure: the position on AAPL could reach -13 with this order, beyond the limit of -10). When the end it refuses at was already past the limit before the order’s legs were added (the base interval, less any attempts the order is held behind), the reason ends with ; it could already reach N without it: cap_exposure: the position on AAPL could reach 19 with this order, beyond the limit of 10; it could already reach 18 without it. The budget prints its window as a Go duration (rate_budget: the budget of 3 submissions per 2m0s is used up).

Precedence. The guard decides first, so its own refusals (duplicate_live_label, cancel_refused, cancel_unknown) come before any of the policy’s, and an order the guard would hold can be refused by the policy instead. Then the first check that fails decides, in this order: quantity, the instance’s working orders, each symbol’s working orders (the legs’ symbols in the order they first appear, the order’s own first), exposure (the same symbols, the high end first), the budget. A release is checked only once the guard has decided to release it (cancel_race, cancel_refused, cancel_unknown and hold_timeout stay the guard’s), from quantity to exposure; the budget does not apply.

Saturating arithmetic. The exposure sums saturate at the bounds of int64 instead of wrapping: the legs’ quantities by side, and the reaches Position + Buys and Position - Sells. A bracket whose two sell children of 1<<62 each sum past the largest int64 therefore reaches the bound and is refused, where a wrapped sum would have turned negative and skipped the low-end check. Whether the legs add a buy or a sell is read from the legs themselves, not from the sign of a sum.

Configuration. Six algo run flags set RunConfig.LifetimeLimits (engine.LifetimeLimits): --max-working-orders, --max-symbol-working-orders, --max-order-quantity, --max-position, --max-submissions and --submission-window (algo run flags). They are single-run flags, refused with --config like the others. In a run-config file they are data keys of the same names (Config files and run matrices), beside lifetime, lifetime-turns and lifetime-hold-timeout, which mirror --lifetime, --lifetime-turns and --lifetime-hold-timeout and had no config key before this unit:

"data": {
  "adapter": "bin/file-csv-adapter",
  "lifetime": "v2",
  "lifetime-hold-timeout": "90s",
  "max-working-orders": 8,
  "max-position": 10,
  "max-submissions": 20,
  "submission-window": "1m"
}

Each key may be set at any level (global, a strategies entry, a run): a key not set inherits, and an explicit 0 overrides an outer level’s value. Numbers must be whole JSON numbers (a quoted "4" is refused, naming the key) and durations Go duration strings such as "90s"; lifetime takes v1 or v2. Each run’s lifetime settings are checked as the plan is built, so a bad value stops algo run --config, and --verify before it prints the plan. The refusals, made before any spawn:

  • a negative limit, naming its key: engine: the admission limit max-position must not be negative (0 = no limit), got -1;
  • a budget whose count and window are not set together: engine: the admission limit max-submissions needs a submission-window above zero, or the same for submission-window without max-submissions;
  • any limit on a v1 run: engine: the admission limits need order lifetime v2 (--lifetime v2); a v1 run has no admission policy. A v1 run ignores --lifetime-turns and --lifetime-hold-timeout, but a safety rail that silently did nothing would be the worse failure, so a limit is refused.

The defaults are off. With every limit at 0 the policy accepts every order and keeps no state, so a v1 run, a v2 run without limits and the KBD masters are byte-identical to before.

Simulation and emulation. The two paths can give the policy different inputs. The simulator confirms a cancel at once, while a live venue, the emulator included, answers Pending, and a pending-cancel attempt still counts. So an opposite-side order in the same step as the cancel counts the pending attempt on the live run but not in the backtest, and can be refused only live. That is consistent, since the inputs differ. A same-side replacement is held on the live run and checked without the attempt it waits for, so it is decided as the backtest decides it: an emulated-live run with a symbol cap, an exposure cap and a budget configured makes the backtest’s refusals, with the same texts, fills, cash and positions.

Per instance. There is one driver, one guard and one policy per strategy instance, so every limit counts one instance’s orders. Under owner decision D4 the programme runs one instance per account, so per instance is per account in its scope; summing instances that share an account is the account execution coordinator’s, GLE-360. One value of each cap applies to every symbol; per-symbol values are a follow-up.

The halt latch is I9’s. The plan’s safety rails also latch a halt when existing exposure already breaches a limit. This unit refuses the order and latches nothing: a latch must survive a restart (I7 and I8’s ledger and recovery), its response (reject the held orders, cancel entries, keep verified protection, keep observing fills) is I9’s ownership-aware halt, and its trigger, a position already past the limit, comes from executions the guard does not track, which are recovery’s (I8). The ; it could already reach N without it suffix makes that trigger visible now; owner decision D10 is re-checked at I9.

The index. The policy reads the oracle and the counts on every submission, so the guard now indexes the attempts it tracks, and those with a cancellation outstanding, by symbol, with a running count for the instance: exposure and the blocker check read one symbol’s attempts and the label’s, and removing an attempt from its symbol’s set takes constant time. Blockers are still named in the order they entered their cancellation state.

Known limits and follow-ups.

  • Releasing a hold queue is quadratic in its length, since each release walks the held orders after it. Deferred: a hold queue is the same-side orders behind one pending cancel in one instance, a handful in practice, and a working-order cap bounds it; a running tally is a contained fix if a profile asks for one.
  • observe scans the hold queue on each fill of an attempt whose cancellation is pending (unit 1’s code), under the same bound.
  • A bracket’s children are not netted against its entry, and each cap has one value for every symbol (above).
  • The lifecycle trace does not record policy denials, as it does not record the guard’s.
  • Refusing unlabelled orders outright under a working-order cap would change the contract; the note is the warning instead.

Unit 3. Unit 3a, the transport scheduler, follows (The transport scheduler): pacing on the operational clock, cancels first. Unit 3b, IBKR’s order-reply-message policy (GLE-436), replaced the blanket confirmation of every IBKR order warning with an allow-list of message ids per deployment (Order reply messages).

Tests. engine/admission_tournament_spec_test.go (22 tests) drives the policy on the driver over scripted venue operations (each cap at admission and at release, the held-order projection, the release order, the budget’s window and places, the precedence, the exact reasons, the note, and the index’s timing), through runLiveLoop over the scripted venue (two held orders behind one cancel), through runBarLoop over the simulator, against the emulator (the same refusals on both paths), and end to end through engine.Run and a run-config plan; engine/admission_review_test.go holds the review panel’s gap tests and the saturation test; and engine/runconfig, sdk/strategy and cmd/algo each pin the keys, the tokens and the flags in a tournament_admission_spec_test.go.

The transport scheduler

Under --lifetime v2 a live run sends its submissions and cancel requests to the venue through the engine’s transport scheduler (increment I6 unit 3a of the order-lifetime programme, GLE-435; engine/transport.go, engine/lifeturn.go and sdk/strategy/transport.go). The race guard and the admission policy decide whether an order may go to the venue; the scheduler decides when. It paces the requests to a configured number per window, queues what the window does not cover in two first-in first-out queues, every cancel request ahead of every submission, bounds the submission queue and refuses what does not fit, and sends again a request the venue refused for pacing. It changes when an admitted request reaches the venue, not what was decided, and every effect it has on the strategy is in the event stream: Held updates with the transport_queued reason, and late updates and responses. A backtest has no venue transport and no scheduler, and a v1 run has no driver, so both are unchanged and the KBD masters are byte-identical.

What is paced, and on which clock. A request is one call to the venue: a submission, whether a new order the guard admits or a held order it releases, or a cancel request that does not address a guard-held order (one that does withdraws that order without reaching the venue, as before). At most --transport-requests requests (default 5), submissions and cancel requests together, are sent within any --transport-window (default 1s), and each call counts at its instant whatever the answer. The window is the half-open interval (now - window, now], so a request sent at t no longer counts at t + window. The clock is the run’s operational clock, the instants the driver passes, and nothing else. On today’s live loop the venue is stepped one bar per iteration and every instant is the bar’s close, so the scheduler reads no wall clock: an emulated or scripted run is deterministic, and its event history and venue answers reproduce every decision the scheduler made. This window is not the admission policy’s rate_budget, which counts new orders at the strategy’s decisions in event time and is decided first.

Per bar on the stepped loop. With a window no longer than the bar interval, “N requests per window” means N per bar observation: the requests at one bar’s close (its observations, its decision and its turns) share the N, and a request queued at a bar waits for the next bar’s observation. At the default a full queue of 100 submissions drains in 20 bars, and a burst of 50 cancel requests in 10. An emulated v2 run that sends more than five requests at one bar therefore queues the excess to the next bar and diverges from its backtest; no golden does, since the masters are v1 runs. A real-time loop, not built yet, will pass the wall clock and dispatch between bars.

Order. Dispatch sends every queued cancel request in arrival order, then every queued submission in arrival order. Nothing overtakes a queued request of its own kind, and no submission overtakes a queued cancel request: a new cancel request is sent at once only when no cancel request is queued, and a new submission only when nothing is queued, each when the window allows; otherwise it joins the end of its queue, even when the window has room. So an attempt’s submission reaches the venue before any cancel request for it, since a request for a submission still queued withdraws it (below) rather than overtaking it.

Cancel requests. A cancel request goes through the window as a submission does, ahead of every submission, and is not refused for overload: the cancel queue has no bound of its own. Pacing them keeps a burst under the venue’s limit; unpaced, a burst beyond it could draw pacing refusals and a block that stops every request, polls included. A request for attempt 0 is pinned on arrival to the label’s latest submission sent to the venue (the attempt the venue operations would address now) and keeps that attempt while it waits. A request for the label and pinned attempt of a cancel request already queued joins it: nothing more is queued or sent, and when the queued request is answered each request that joined it gets the same answer under its own request id, in the order they joined. Joining keeps the cancel queue to one request per distinct label and attempt, and absorbs a strategy that cancels again at every turn.

Withdrawal. A cancel request for a submission still queued, under its label and with its attempt or attempt 0 (the first such submission in queue order), withdraws it as the guard withdraws a held order: nothing reaches the venue, the attempt ends with its children, and the response is Confirmed with the status Cancelled, the whole quantity remaining, the instant as its timestamp and no OrderRef. It grants a turn.

A queued submission is held. A submission that cannot be sent at once is queued and answered with an OrderUpdate whose status is Held, with the whole quantity remaining, the instant as its timestamp, no OrderRef and the reason transport_queued: waiting for the venue transport (5 requests per 1s); like the guard’s Held, it grants no turn. While it waits the attempt is live with no venue id, reserved as an unknown submission is: the exposure oracle and the admission policy’s caps count it, the guard treats it as its label’s previous attempt, and the SDK’s working view keeps it Held. Once sent it is answered as a direct submission is (Working, Denied with the venue’s reason and its rejection cancel, or SubmissionUnknown), stamped with the instant it was sent. A held order the guard releases goes through the scheduler too, so it can be Held twice, behind a cancellation and then behind the window, before it is Working. No time limit applies: the guard’s hold timeout does not cover a queued submission, which waits until it is sent, withdrawn or refused at the end of the run, and the strategy, which sees it Held, can cancel it.

The queue bound. A submission that cannot be sent at once and finds --transport-queue submissions (default 100) or more already queued is refused: Denied with the reason transport_overload: the transport queue already holds 100 submissions, whose number is the queue’s length, and its attempt ends with its children. The operator hears the denial line, and the denial grants a turn as a guard denial does. The bound applies to a new arrival only: a submission the venue refused for pacing returns to the queue whatever the bound, and queued cancel requests do not count toward it.

Pacing refusals. A venue answer can be a pacing refusal: the venue refused the request for its rate limit without processing it, so the order is as it was and the request may be sent again. The venue marks such an answer Paced (a Denied sim.SubmitResult or a Refused sim.CancelResult): the scripted venue marks its Pacing refusals (The scripted venue) and the IBKR adapter an HTTP 429 on the place or the cancel endpoint (Tickets and cancels at the IBKR venue) and, since I6 unit 3b, on the confirmation of an order reply message (Order reply messages). A submission whose confirmation was refused for pacing is sent again as a whole: a new place call, which draws the reply again, and a new conversation. A pacing refusal pauses the transport for one window: nothing is sent before the refusal’s instant plus the window, cancel requests included. The refused request goes to, or stays at, the head of its queue, the observation stops there, and the request is the first sent when the transport allows. A submission refused for pacing when it was sent at once is answered Held, with no rejection cancel: transport_queued: the venue refused it for pacing (venuetest: pacing refusal); it is sent again when the transport allows. Each time a submission is refused for pacing, the live operations close, in the ledger, the reference it was sent under, and the retry goes under a new reference. A cancel request refused for pacing gets no response yet and waits at the head of the cancel queue. The report’s Orders figure counts the submissions the venue answered: a queued submission counts when it is sent, and a pacing refusal, an overload or a withdrawal counts nothing.

A queued cancel request. A queued cancel request is answered once, when it is sent, with the venue’s answer stamped with that instant; there is no interim response, so the response can come bars after the request. The SDK’s view shows the attempt pending cancellation from the request, and the race guard treats it as pending too: when the guard finds a live attempt for the pinned label and attempt with no cancellation outstanding, queueing the request marks it pending, so a same-side order, or the label’s successor, is held behind it rather than admitted or refused as a duplicate. An attempt already pending, unknown or refused is left as it is. When the request is sent, its mark is withdrawn first and the guard records the venue’s answer as it records a direct one: an unknown answer, or a refusal without evidence, then rejects the orders held behind it (cancel_unknown, cancel_refused) rather than leaving them held behind a mark. A request refused for pacing keeps its mark while it waits. A queued request is sent even when its attempt ended while it waited (filled, say), and the venue’s answer is delivered as any answer is.

The observation. The live loop’s observation (“Where the loops observe” under The engine race guard) hands the poll’s batch to the guard, then the scheduler sends what it queued as its window allows, then the guard decides its held orders, so a cancel sent at the observation can release an order held behind it there. The transport’s outcomes are delivered first, at once and with no turn; the submissions the venue answered add to the run’s order count, and the rejection cancels of those it denied are queued for the next bar, as a release’s are. The observation now ends the guard’s step: a refusal recorded there lasts until the observation ends, as a direct refusal lasts until its step ends, so a same-side replacement the strategy places at its next decision is an ordinary submission. A run without a scheduler records no refusal at an observation, so this changes nothing there.

Shutdown. At the end of the run the driver rejects the guard’s held orders (hold_shutdown), then answers what the scheduler still queues, and delivers all of it at once, with no turn, before the teardown cancels anything at the venue. Each queued submission is refused, in queue order, and its attempt ends: transport_shutdown: the run ended while order "B" attempt 1 (buy AAPL) was queued for the venue. Then each queued cancel request, followed by each request that joined it, is answered Unknown without reaching the venue, so every cancel request gets its one response: transport_shutdown: the run ended while this cancel request for order "A" attempt 1 was queued for the venue, naming the attempt pinned when it was queued. The operator hears the denial line of each refusal and the unresolved-cancel line of each answer. The teardown sweep that follows cancels whatever is working at the venue (Tickets and cancels at the IBKR venue).

The operator line and the counters. When any request was queued, refused for pacing or refused for overload over the run, the shutdown writes one line to stderr with the scheduler’s counters. An emulated run whose strategy places seven orders at one bar ends, at the defaults, with

algo: transport for instance AAPL: 7 request(s) sent, 2 queued (at most 2 at once), 0 pacing refusal(s), 0 withdrawn, 0 refused for overload
CounterCounts
request(s) sentevery call to the venue the scheduler made, pacing refusals and errors included
queuedevery request that had to wait, once, when it first entered a queue (queued, or refused for pacing when sent at once); a request that joined another is not counted
at most N at oncethe largest number of requests in the two queues together, right after one entered
pacing refusal(s)every pacing refusal
withdrawnevery queued submission a cancel request removed
refused for overloadevery submission refused because the queue was full

The counters reach neither the run’s summary nor result.json yet.

The reasons. The scheduler’s decisions reach the strategy as OrderUpdate events, and its shutdown answers as CancelResponse events, whose Reason begins with a token and ": "; the rest of the text is for people, not for parsing. The SDK exports the tokens (sdk/strategy/transport.go):

TokenSDK constantWhereMeaning
transport_queuedTransportQueuedHeld updatethe submission is queued for the venue, behind the window (waiting for the venue transport (N requests per D)) or after the venue refused it for pacing; released (Working, Denied or SubmissionUnknown) when it is sent
transport_overloadTransportOverloadDenied updatethe submission queue already held the run’s maximum
transport_shutdownTransportShutdownDenied update, or a CancelResponse with outcome Unknownthe run ended while the submission, or the cancel request, was queued

A transport denial carries what a guard denial carries (The engine race guard): the order’s quantity, nothing remaining, the instant as its timestamp, no OrderRef, an empty BrokerCode, and no OrderCancel{Rejected} after it.

Configuration. Three algo run flags set RunConfig.TransportLimits (engine.TransportLimits): --transport-requests, --transport-window and --transport-queue (algo run flags). Each 0 selects its default (5, 1s, 100), so every live v2 run has a scheduler. They are single-run flags, refused with --config like the others. In a run-config file they are data keys of the same names, set at any level as the admission policy’s keys are (The admission policy):

"data": {
  "adapter": "bin/file-csv-adapter",
  "lifetime": "v2",
  "transport-requests": 4,
  "transport-window": "2s",
  "transport-queue": 50
}

They are lifetime-v2 only. A v1 run has no scheduler and refuses any transport limit before any spawn (engine: the transport limits need order lifetime v2 (--lifetime v2); a v1 run has no transport scheduler), and a negative value is refused naming its key and default (engine: the transport limit transport-queue must not be negative (0 = the default 100), got -1); a run-config file’s values are checked as the plan is built. A v2 backtest accepts the limits and ignores them, since it has no venue transport, so one config file serves both modes and the backtest is unaffected.

Recorded assumptions. The unit rests on three, stated here as assumptions until they are checked:

  • IBKR’s pacing limits are not in its Web API reference (searched for “pacing”, “rate limit”, “429”, “Too Many Requests” and “throttling”), so the defaults are conservative and configurable: five requests per second, 100 queued submissions and a one-window pause after a pacing refusal. Figures recalled from memory rather than from the reference, about ten requests per second overall with stricter limits on some endpoints, leave room for what this unit does not pace: the polls, IBKR’s reply hops, the teardown sweep and the reconciler’s queries. IBKR’s pacing page or a paper-account probe should confirm them.
  • An HTTP 429, on the place call or, since I6 unit 3b, on the confirmation of an order reply message, is read as IBKR’s rate limiter refusing the request before it was processed, so the scheduler sends the order again under a new ledger reference and closes the refused one. Were the reading wrong (IBKR created the order), the broker would hold two orders: the ledger would apply the first one’s fills under the strategy’s label, the race guard would credit them to the retry, and the run would not stop on its own. A paper-account probe of IBKR’s 429 is therefore a gate before live trading; after it, either a paced retry reuses its reference, so IBKR’s cOID uniqueness refuses a duplicate, or the run fails closed on any fill or open order under a closed paced reference.
  • A v1 run has no scheduler, and it now hears an IBKR 429 on a submission, or on a confirmation since unit 3b, as a denial (the operator’s denial line and a rejection cancel before the next bar) where it heard an unknown submission left to reconciliation. v1 live runs only against the emulator today, which does not pace, so no output moves; the same probe gates it.

Known limits and follow-ups.

  • Every-exit teardown is I9’s. Only the run’s normal end answers the queues: a poll, observation or instant error returns before that, as it returns before the guard’s shutdown rejections and the venue sweep. A queued submission was not sent, so nothing of it rests at the venue, but its shutdown denial is lost. I9’s every-exit teardown must call the scheduler’s shutdown (transportShutdown) too.
  • Joining a cancel request scans the cancel queue, and removing a queued request copies the rest of its queue down, so a burst of joins and a drain are each quadratic in the queue’s length. The queues are small today (the cancel queue holds one request per distinct label and attempt, tens on today’s strategies, and the submission queue at most transport-queue, 100 by default), but transport-queue has no upper bound: an index keyed by label and attempt for the join, and a cap or a head index for the submission queue, are follow-ups.
  • The scheduler keeps each label’s latest attempt for the rest of the run, one entry per label, as the live attempt map does (Lifecycle turns); evicting a label the guard has dropped is a follow-up.
  • The live loop is stepped, so a queued request waits a bar. A real-time loop must pass the wall clock and dispatch between bars.
  • The pause is one fixed window. A pause taken from Retry-After, or an exponential back-off, follows the probe, since retrying at every window may extend IBKR’s block.
  • A queued submission has no time limit (above); one is a follow-up.
  • The counters reach stderr only; adding them to the run’s summary or result.json is a follow-up.
  • The scheduler is per run. A future multi-instance live run on one gateway must share one scheduler.

Tests. engine/aa_transport_tournament_spec_test.go (38 tests) drives the scheduler through runLiveLoop over the scripted venue (the window, cancels first, a withdrawal, a paced submission and a paced cancel, the overload, the shutdown), on the driver over scripted venue operations at chosen instants (the window’s boundary, the pause, the head kept at a paced dispatch, no overtaking, the mark and the join, a release through the transport, the counters, dispatch errors), and end to end through engine.Run against the emulator at the defaults; engine/transport_review_test.go holds the review panel’s seven gap tests; and a tournament_transport_spec_test.go in each of engine/venueibkr, engine/venuetest, engine/runconfig, sdk/strategy and cmd/algo pins, in turn, the 429 classification, the Paced mark, the keys, the tokens with the view’s handling of the transport’s updates, and the flags.

The desired-order controller

A strategy on lifetime v2 need not write its own cancel-and-resubmit logic. kit.Controller (strategies/kit/reconcile.go; the order-lifetime programme’s increment I5, GLE-379) takes, at each decision, the orders the strategy wants resting, each under a logical key, and makes the venue match them through the working view: an unchanged intent is left alone, a changed or withdrawn one has its attempt’s cancellation requested through ctx.Cancel, and the submissions the decision needs come back for the callback to return. The strategy declares what should rest; the controller works out the commands. It is a value the strategy owns, driven from the one goroutine that runs its callbacks, with no goroutine, timer or global state of its own, and it reads the view through a three-method interface, kit.View (Now, WorkingOrders, Cancel), which *algolang.Context satisfies and the kit’s tests drive with a fake. It needs lifetime v2 (--lifetime v2; see Lifecycle turns): the attempt-indexed view is what it reads and the lifecycle turns are where a replacement goes out, so on a v1 run neither is authoritative.

The API. Build the controller with NewController(prefix); the zero Controller has no prefix and Reconcile refuses it. The prefix names the controller’s scope: it owns every label that starts with the prefix and is longer than it, Label(key) is prefix + key, and Key(label) returns the key and true for a label in the scope, "", false otherwise (the bare prefix is outside). A prefix that is empty or contains a comma, a space, a tab, a line feed or a carriage return is refused; "p:", "demo/" and "x" are valid. Prefix() returns it.

type Intent struct {
    Key        string         // the intent's name in the scope; the attempt label is Prefix+Key
    Order      algolang.Order // the order as it should rest; ClientID and Attempt are ignored
    Group      string         // intents sharing a non-empty Group are replaced together; "" = the key alone
    MaxWindows int            // retire an attempt still live after this many eligible windows; 0 = no limit
}

type KeyState struct {
    Known   bool   // the controller remembers the key
    Live    int    // live entries under the key in the view (any status)
    Pending bool   // at least one of them is pending cancellation
    Waiting bool   // the intent wants a submission the controller is holding
    Windows int    // eligible windows of the key's newest live attempt
    Lapsed  bool   // that attempt had MaxWindows windows and must retire
    Stalled bool   // an attempt's cancel-request budget is exhausted
    Blocked string // the reason of the denial that blocks the intent; "" if none
    Filled  int64  // quantity filled under the key since the controller knew it
}

func NewController(prefix string) (*Controller, error)
func (c *Controller) Prefix() string
func (c *Controller) Label(key string) string
func (c *Controller) Key(label string) (string, bool)
func (c *Controller) Observe(v View, bar algolang.Bar)
func (c *Controller) Reconcile(v View, desired []Intent) ([]algolang.Order, error)
func (c *Controller) State(key string) KeyState
func (c *Controller) OnFill(_ *algolang.Context, f algolang.Fill)
func (c *Controller) OnOrderUpdate(_ *algolang.Context, u algolang.OrderUpdate)
func (c *Controller) Halt(reason string)
func (c *Controller) Resume()
func (c *Controller) Halted() (string, bool)
func (c *Controller) Reset(key string)

Observe(ctx, bar) goes at the top of OnBar, before the decision: it records the bar as an eligible window of the resting attempts it belongs to. Reconcile(ctx, desired) is the decision, at the end of OnBar and in OnLifecycleTurn: it requests the cancellations through the view and returns the submissions, which the callback returns; a nil or empty declaration retires every attempt in the scope, and the returned slice is nil when there is nothing to submit. OnFill and OnOrderUpdate have the FillHandler and OrderUpdateHandler signatures, so the strategy forwards each event with one line, or embeds the controller: OnFill counts the key’s filled quantity, OnOrderUpdate blocks a key the engine denied. State(key) is the controller’s observation of a key as of its last call, the zero KeyState for a key it does not remember. Halt(reason) stops every command until Resume; Halted reports the reason. Reset(key) forgets a key. MaxCancelRequests is the cancel-request budget per attempt, DefaultMaxCancelRequests (3) when left at 0, which NewController does. A nil *Controller is a valid receiver: Reconcile returns an error, State the zero value, Halted "", false, Label the key unchanged, and the rest do nothing.

A one-window limit entry, sized from the position so that the intent stays stable across partial fills:

type Dip struct {
    Size  int64 `algolang:"default=2,min=1,desc=Units to hold"`
    rc    *kit.Controller
    sym   string
    level float64
}

func (s *Dip) RequiredCapabilities() []string { return []string{algolang.CapabilityLifetimeV2} }

func (s *Dip) OnInit(ctx *algolang.Context, cfg algolang.InitConfig) error {
    rc, err := kit.NewController("dip:")
    if err != nil {
        return err
    }
    s.rc = rc
    return nil
}

func (s *Dip) OnBar(ctx *algolang.Context, bar algolang.Bar) ([]algolang.Order, error) {
    s.rc.Observe(ctx, bar)                      // the bar is a window of the resting attempts
    s.sym, s.level = bar.Symbol, bar.Close-0.25 // the signal, decided on bars only
    return s.rc.Reconcile(ctx, s.desired(ctx))
}

func (s *Dip) OnLifecycleTurn(ctx *algolang.Context, t algolang.LifecycleTurn) ([]algolang.Order, error) {
    return s.rc.Reconcile(ctx, s.desired(ctx)) // the same signal, re-sized: a successor goes out here
}

// desired is what should rest now: the level, for the quantity still to execute.
func (s *Dip) desired(ctx *algolang.Context) []kit.Intent {
    want := s.Size - ctx.Position(s.sym).Quantity
    if want <= 0 {
        return nil // nothing wanted: every attempt in the scope is retired
    }
    o := algolang.LimitBuy(s.sym, want, s.level)
    o.TIF = algolang.GTC
    return []kit.Intent{{Key: "entry", Order: o, MaxWindows: 1}}
}

func (s *Dip) OnFill(ctx *algolang.Context, f algolang.Fill) { s.rc.OnFill(ctx, f) }
func (s *Dip) OnOrderUpdate(ctx *algolang.Context, u algolang.OrderUpdate) {
    s.rc.OnOrderUpdate(ctx, u)
}

Intent and attempt. A key names an intent; each physical submission under it is an attempt, labelled Label(key) and stamped by the SDK (the controller submits with Attempt 0 and the view stamps the next number). The controller submits at most one attempt per key and treats more as interference (below). The successor of an attempt is the declaration of the decision that submits it: the controller caches no order to send later and invents no quantity.

Frame equality. An attempt is an intent when its Symbol, Side, Type, Price, StopPrice, TrailAmount, TIF and OCOGroup equal the intent’s (exactly, floats included: the strategy’s numbers are its own, and a level one tick away is a different intent), its remaining quantity equals the intent’s Quantity, and its attached children match the intent’s by label and on the same fields. So an intent’s Quantity is what is still to execute: a strategy that sizes from ctx.Position (or from State(key).Filled) declares a stable intent across partial fills, and a fill of 4 of 10 followed by a declaration of 6 is still met. ClientID, Attempt, SubmittedAt, FilledQuantity, OrderRef, CancelRequestID and ParentClientID play no part. A v1 entry (status Unspecified, no quantities) is compared on its Order.Quantity. The controller does not change an intent’s TIF: a resting order that should outlive one bar is declared GTC.

Met, retire, wait. An intent is met when its key has exactly one live attempt, that attempt is commandable (its status is neither Inactive nor PendingCancel: Submitting, Working, PartiallyFilled, Held, SubmissionUnknown and the v1 Unspecified all are), is the intent and has not lapsed; a met intent produces nothing, so an unchanged ladder costs no command bar after bar. An intent that is not met retires every commandable attempt under its key, and so does the absence of an intent under a key that has attempts: one ctx.Cancel(label) per retired entry, which the view addresses to the label’s newest attempt not yet pending; an attempt already PendingCancel is not re-requested, and an Inactive child is not cancelled directly. An unmet intent is submitted only when its key has no live entry at all (none in any status, Inactive and PendingCancel included), none of its declared child keys has one, it is not blocked and its group is not being replaced; otherwise it waits, and State(key).Waiting says so. The submission is the intent’s Order labelled Label(key), attempt 0, each child labelled Label(childKey) in a fresh slice, every other field as declared.

A replacement takes two decisions. The decision that sees the change requests the cancellation; the successor goes out at a later decision, once the view has retired the predecessor: a confirmed CancelResponse at the turn it grants, an OrderCancel, a terminal OrderUpdate or a full fill. A Pending or Unknown response keeps the predecessor live and the successor waiting; a Refused one restores the attempt, which the next decision retires again, up to the budget. On the simulated path the confirmation arrives at the instant of the request and grants a turn, so the successor goes out before the clock advances: bar N returns the cancel, the turn returns the successor, and bar N+1 finds it resting. The declaration the strategy makes at that turn is the one submitted, which is why the example re-sizes in OnLifecycleTurn.

A fill during the cancel. p:k attempt 1 is S10@100; the strategy declares S10@99, and the controller requests the cancel. A fill of 4 lands while the cancel is pending: the view shows 6 remaining, still pending; State("k").Filled is 4; the position has moved; nothing is submitted. The confirmation retires attempt 1 and grants a turn, where the strategy declares S6@99 (10 less the fill count, or the position’s residual): attempt 2 is S6@99. The stale S10@99 is not sent at any point. TestTr5SDKFillDuringCancelResizesSuccessor runs this through the real SDK and wire.

Eligible windows and the lapse. The simulator evaluates a resting order against bars of its own symbol, on every series of the run, that close after the order became active (the fill simulator). Observe counts the same thing from the strategy’s side, per live attempt: a bar is a window of every live attempt whose Order.Symbol is the bar’s symbol and whose activation is strictly before the bar’s CloseTime; a bar is counted once per attempt (a second Observe with a bar not closing after the last counted close adds nothing); the bar’s SeriesID plays no part, so an auxiliary series of the same symbol counts, as the engine evaluates it. A top-level attempt is active from its SubmittedAt (the close of the bar or the instant of the turn that submitted it); an attached child from the first Observe or Reconcile that sees it other than Inactive, at ctx.Now() then, so the bar in which its parent filled is not its window and the next one is, as the simulator has it. An attempt that leaves the view forgets its windows; a successor starts at zero. An intent with MaxWindows > 0 lapses an attempt that has had that many windows: the attempt is no longer the intent, so it is retired and the successor waits as for any replacement; MaxWindows 0 lapses nothing, and Observe still counts.

The lapse is the one-window helper. With MaxWindows: 1 and GTC, a limit submitted at bar N’s close rests through bar N+1, its one window; the reconcile at bar N+1 cancels it when it is still unfilled, and the reconcile at the turn the confirmation grants submits the successor, which rests through bar N+2. An auxiliary callback (a bar of another symbol) adds no window, so reconciling the same declaration on it cancels nothing: the acceptance case, that an auxiliary callback does not cancel a market or limit order the engine has not yet evaluated. Declare GTC, not Day, for the one-window semantics: on the simulated path the simulator’s own Day expiry retires a Day attempt before the lapse can act (the attempt is gone at the next bar, so the controller resubmits with no cancel), and GTC with MaxWindows: 1 is the form that also holds live.

Child policy. A child is declared inside its parent’s Order.AttachedOrders under its own key, carried in the child’s ClientID, one level deep. While its parent is unfilled it is an Inactive entry under that key: it is not cancelled directly (the parent’s cancellation takes it along), and it counts as a live entry of its key, so a top-level intent declared under that key waits. Once active, through its parent’s fill or its own evidence, it is an attempt under its key like any other: a top-level intent under that key that is the child’s frame keeps it with no churn (a protective stop re-declared standalone once the strategy is long), a different one retires it and submits the successor once it is gone, and the absence of any intent under that key retires it. A declared child frame that differs from the view’s is a changed parent: the parent is retired and resubmitted whole with its children. A parent whose declared child key still has a live entry (the earlier generation’s child, active after its parent filled) waits until that entry is gone: one attempt per key holds for child keys too. Declaring a parent does not declare its child keys: a child key becomes known when its entry appears in the view, or when a fill or a declaration names it.

Group transitions. A key’s group is the Group of its last declaration. A group is being replaced at a decision when any entry of a key in it is in the retire set or PendingCancel: every commandable attempt of every key in the group is retired too (a met member is no longer met) and no member is submitted at that decision. A group not being replaced takes a new member at once. So changing one leg of an exit pair cancels both legs and resubmits both together once both are gone; withdrawing one leg retires the other; a pending cancellation on one member, the strategy’s own ctx.Cancel included, holds the whole group. The coordination is the controller’s; nothing is atomic at the venue. Intents with an empty Group are independent, and Group is not OCOGroup: the one is the controller’s replacement unit, the other the venue’s one-cancels-other tag, which is part of the frame.

Denial blocks. An OrderUpdate with status Denied or Rejected for a label the controller has submitted blocks the key for the frame of its last submission, with the update’s Reason (or the status name, denied or rejected) as State(key).Blocked. A blocked intent is not submitted while its declared Order is the blocked frame; a different intent under the key clears the block and goes out, and so does withdrawing the key or Reset. Updates with any other status, updates outside the scope and updates for labels the controller has not submitted change nothing. This is where an invalid instrument, an unresolved ownership or a cap surfaces: as a denial the engine reports, which the controller does not retry. A strategy that does not forward OnOrderUpdate has no blocks: it resubmits a denied intent at every decision, bounded by the turn budget per instant and then once per bar.

The cancel budget. The controller counts the cancel requests it issues per attempt. An attempt whose count has reached MaxCancelRequests (DefaultMaxCancelRequests, 3, when the field is 0; a negative value is a budget of none and stalls every attempt) is not requested again: it is stalled, State(key).Stalled reports it while the attempt is live and not pending, and any successor keeps waiting. A refused cancel that restores the attempt therefore gets at most the budget’s requests in all. A successor attempt starts with a zero count; Reset zeroes the key’s.

Halt. While halted, Reconcile validates the declaration, records the declared keys (known, their group, a block cleared by a changed intent), refreshes Live, Pending, Windows and Stalled from the snapshot, leaves Waiting and Lapsed as the last non-halted Reconcile set them, forgets nothing, requests no cancel and returns nil, nil. Observe, OnFill and OnOrderUpdate keep recording.

Forgetting. The controller remembers a key from the first Reconcile that declares it, the first Observe or Reconcile that sees a live attempt under it, or an OnFill for it, until the first Reconcile at which the key is neither declared nor has a live entry; then everything about it is forgotten, State returns the zero KeyState, and a later declaration of the same key starts afresh, its block gone and its fill count at zero. Filled accumulates every scoped fill forwarded to OnFill across the key’s attempts, a fill for an attempt the view has already retired included. An attempt’s memory (its activation, windows and cancel count) lasts while it is in the view. Reset(key) forgets at once; live attempts in the view are learned again, as fresh attempts with zero windows and zero cancel counts, at the next call. After a live restart the controller is fresh in the same way: adopted entries in the scope are learned as attempts with zero windows and counts, and Filled restarts at zero.

Sorted commands. Cancel requests are issued in label order (byte-wise) and, within one label, newest attempt first, so that each ctx.Cancel addresses the attempt the controller counts it against; submissions are returned in key order. The declaration’s own order and the view’s order across labels do not affect the commands, so a decision is deterministic however the strategy builds its slice.

Validation. Reconcile validates the whole declaration before anything else and, on the first defect, returns nil and an error beginning kit: that names it, having commanded and remembered nothing: a key that is empty or contains a comma, a space, a tab, a line feed or a carriage return (other Unicode spaces are allowed); a key declared twice, as a key or a child key; an empty Symbol, a Side other than Buy or Sell, a Quantity that is not positive, a Type outside Market to StopTouch; a negative MaxWindows; a child that fails those rules or carries children of its own. A zero Price, StopPrice or TrailAmount is a value, not a defect, and TIF, OCOGroup and Group are not checked. The same validation runs while halted.

Scope and interference. The controller reads and commands labels in its scope and nothing else: an entry under another prefix, under no prefix or under the bare prefix is neither counted nor cancelled, whatever its symbol, and fills and updates outside the scope are ignored. Within the scope it submits one attempt per key; a key found with two live attempts, equal or not, has both retired (ctx.Cancel addresses a label’s newest attempt, so an older one cannot be retired selectively) and its successor waits until both are gone. One case needs a detour: a retired attempt masked by a newer Inactive child under the same label, where the label’s cancel would reach the child. The controller retires the child’s parent instead, when the parent is in the scope: the parent’s cancellation takes the child along, and the masked attempt is requested once the child has gone. With no parent in the scope nothing is requested and the successor waits; no foreign or inactive entry is cancelled (the review panel’s correctness fix, TestRcrMaskedAttemptRetiresTheChildsParent).

State. State(key) is computed at the end of every Observe and Reconcile from the snapshot that call read, and updated by OnFill (Filled) and OnOrderUpdate (Blocked). Live counts the key’s entries in the snapshot, so after a Reconcile that submits it counts what was read, the submission not yet tracked; Pending is read after the call’s own requests, which the view marks at once; Windows is the newest live attempt’s; Waiting and Lapsed are set by the last Reconcile, and Observe leaves them.

Known limits. Two findings of the review panel are recorded rather than fixed. A rejected attached child re-declared standalone with the same frame is not blocked: the block records submissions under the parent’s key, and declaring a parent does not declare its child keys, so the retry is the strategy’s and the turn budget bounds it; the rule is deferred to the strategy migrations, I16 onward, the first with protective children. And under foreign interference two rows with the same label and attempt number share one memory, assigned by view order, so when one of them leaves the memory rebinds; the SDK stamps attempts per label and the controller submits one per key, so duplicates arise only from outside, the effect is confined to the windows and cancel counts of the interfering rows, and a stable row identity would need OrderRef, which v1 and unacknowledged entries lack.

Cost. BenchmarkTr5UnchangedLadder declares the same ten-leg ladder on each of 525,600 one-minute bars, Observe then Reconcile, against a view that hands out a stable slice: about 1.3 µs per bar with no allocations (the tournament’s reference measured 3.6 µs and three). The real ctx.WorkingOrders() copies its entries on every call on top of that, the SDK’s cost rather than the controller’s. The controller does not sort, rewrite or retain the slice the view hands it.

The race guard beneath it. The engine’s admission race guard (The engine race guard; I6: same-side orders held behind a pending cancel on every path, owner decision D2) is the live safety net under the controller, and the controller does not depend on it, nor on Submitting surviving a bar: it reads a status only as Inactive, PendingCancel or any other live one. Held and SubmissionUnknown are live and commandable to it: a held attempt is a live attempt that waits, and may be cancelled.

Tests. strategies/kit/tournament_tr5_spec_test.go drives the controller over a fake view that follows the view’s laws (24 tests and the benchmark); strategies/kit/kit_reconcile_review_test.go holds the review panel’s gap tests; sdk/strategy/tournament_tr5_sdk_spec_test.go runs a controller-built strategy through the real runner, view, pb wire and lifecycle turns against a scripted engine (the stable-then-changed replacement at the turn, the 4-of-10 residual, the auxiliary bar, the denial, the bracket child’s window).