Skip to main content

Place, observe, modify, and cancel a paper order

This workflow sends an order. Run it only against an explicitly authorized paper account on a paper TWS or IB Gateway session. A limit price reduces price risk but does not prevent a fill, and paper execution behavior is not evidence that a live order would behave identically.

Complete connection readiness, account scope, contract qualification, and what-if preview first. The preview result is an input to the placement decision, not an acknowledgement or reusable order object. Rebuild and revalidate the placement order from the immutable intent immediately before submission.

Local transitions are not broker acknowledgements

IB.placeOrder returns immediately with a live-updated Trade. For a new order, pinned ib_async allocates or uses an API order ID, sends the protocol request, creates a local Trade, and sets its local status to PendingSubmit. That return proves local dispatch, not acceptance by TWS, IBKR, or an exchange.

Likewise, IB.cancelOrder sends the cancel request and normally changes the local status to PendingCancel immediately. PendingCancel is not confirmation: an execution may still win the race. Treat only a later confirmed cancel status as cancellation of the unfilled balance, and still retain any fills that arrived before it.

Minimal paper lifecycle

The example uses one share and deliberately low illustrative prices. Replace both prices with valid, increment-aligned, non-marketable prices derived from current paper-session data for the qualified contract. Even then, always handle an unexpected fill.

from __future__ import annotations

import asyncio
from collections.abc import Callable

from ib_async import IB, LimitOrder, OrderStatus, Stock, Trade


async def wait_for_trade(
trade: Trade,
ready: Callable[[Trade], bool],
timeout: float = 15,
) -> Trade:
if ready(trade):
return trade
loop = asyncio.get_running_loop()
future: asyncio.Future[Trade] = loop.create_future()

def on_status(updated: Trade) -> None:
if ready(updated) and not future.done():
future.set_result(updated)

trade.statusEvent += on_status
try:
return await asyncio.wait_for(future, timeout=timeout)
finally:
trade.statusEvent -= on_status


async def main() -> None:
ib = IB()
trade: Trade | None = None
try:
await ib.connectAsync(
host="127.0.0.1",
port=4002,
clientId=12,
timeout=10,
readonly=False,
raiseSyncErrors=True,
)
expected_accounts = frozenset({"PAPER_ACCOUNT_A"})
if frozenset(ib.managedAccounts()) != expected_accounts:
raise RuntimeError("authorized paper account scope mismatch")

qualified = await ib.qualifyContractsAsync(
Stock("AAPL", "SMART", "USD", primaryExchange="NASDAQ")
)
if len(qualified) != 1 or qualified[0].conId <= 0:
raise LookupError("contract did not resolve uniquely")
contract = qualified[0]

# Persist this immutable intent before dispatch. Use a durable store in production.
order = LimitOrder(
action="BUY",
totalQuantity=1,
lmtPrice=1.00, # Replace with a valid non-marketable paper price.
account="PAPER_ACCOUNT_A",
tif="DAY",
outsideRth=False,
transmit=True,
orderRef="paper-order:rebalance:example-001",
)
trade = ib.placeOrder(contract, order)
print("local", trade.order.orderId, trade.orderStatus.status)

await wait_for_trade(
trade,
lambda item: (
item.orderStatus.status
in {
OrderStatus.PreSubmitted,
OrderStatus.Submitted,
OrderStatus.Filled,
OrderStatus.Cancelled,
OrderStatus.ApiCancelled,
OrderStatus.Inactive,
}
and item.orderStatus.permId > 0
),
)

if trade.orderStatus.status in {
OrderStatus.PreSubmitted,
OrderStatus.Submitted,
}:
previous_log_length = len(trade.log)
trade.order.lmtPrice = 1.01 # Same intent; valid paper increment required.
if ib.placeOrder(contract, trade.order) is not trade:
raise RuntimeError("modification lost Trade identity")
await wait_for_trade(
trade,
lambda item: (
any(
entry.message == "Modified"
for entry in item.log[previous_log_length:]
)
or item.isDone()
),
)

if not trade.isDone():
if ib.cancelOrder(trade.order) is not trade:
raise RuntimeError("cancel did not resolve the owned Trade")
await wait_for_trade(
trade,
lambda item: item.orderStatus.status
in {
OrderStatus.Cancelled,
OrderStatus.ApiCancelled,
OrderStatus.Filled,
OrderStatus.Inactive,
},
)

print("terminal", trade.orderStatus.status)
finally:
# A best-effort request is not confirmation; reconcile after any timeout/error.
if trade is not None and not trade.isDone() and ib.isConnected():
ib.cancelOrder(trade.order)
ib.disconnect()


if __name__ == "__main__":
asyncio.run(main())

The typical observable sequence is:

  1. placeOrder returns a local Trade in PendingSubmit and emits newOrderEvent.
  2. openOrder supplies the active order snapshot and OrderState; orderStatus supplies status, quantities, IDs, and hold/cap fields.
  3. Reusing the same owned order identity with placeOrder emits local modification events; a later callback establishes the broker-visible result.
  4. cancelOrder emits local cancel/status events and usually moves the local projection to PendingCancel.
  5. A later Cancelled/ApiCancelled, Filled, or other terminal observation classifies the outcome.

This is a common path, not a guaranteed callback sequence. Exact orderStatus payloads can repeat, intermediate statuses can be skipped, and a fast fill may arrive without every expected status. Monitor execution callbacks as well; the next workflow slice covers fills and commissions in detail.

Production owner with durable boundaries

The owner below serializes one paper order. It records the complete intent and contract snapshot before sending, preserves the original account/client/order/reference identity during modification, and waits for callback evidence rather than trusting local transitions. It poisons the owner before every post-dispatch durable write, classifies fast terminal results separately, and hands unresolved or execution-bearing work to an independent durable recovery queue before detaching or deliberately disconnecting. Replace both placeholder writers before using the pattern.

from __future__ import annotations

import asyncio
from collections.abc import Callable
from datetime import datetime, timezone
from math import isfinite

from ib_async import Contract, IB, LimitOrder, Order, OrderStatus, Stock, Trade

MAX_ORDER_QUANTITY = 1_000_000
MAX_ORDER_LIMIT = 1_000_000
ACKNOWLEDGED = frozenset({OrderStatus.PreSubmitted, OrderStatus.Submitted})
TERMINAL = frozenset(
{
OrderStatus.Cancelled,
OrderStatus.ApiCancelled,
OrderStatus.Filled,
OrderStatus.Inactive,
}
)
CANCEL_CONFIRMED = frozenset({OrderStatus.Cancelled, OrderStatus.ApiCancelled})


def validate_limit_value(value: float) -> None:
if not isfinite(float(value)) or value <= 0 or value > MAX_ORDER_LIMIT:
raise ValueError("paper order requires a positive finite limit")


def validate_placement(
order: Order, accessible_accounts: frozenset[str]
) -> None:
if order.account not in accessible_accounts:
raise ValueError("paper order account is not accessible")
if order.action not in {"BUY", "SELL"}:
raise ValueError("paper order action must be BUY or SELL")
if (
not isfinite(float(order.totalQuantity))
or order.totalQuantity <= 0
or order.totalQuantity > MAX_ORDER_QUANTITY
):
raise ValueError("paper order requires a positive finite quantity")
validate_limit_value(float(order.lmtPrice))
if order.orderType != "LMT" or order.tif != "DAY":
raise ValueError("paper workflow requires a DAY limit order")
if order.outsideRth or order.overridePercentageConstraints:
raise ValueError("paper workflow does not allow precaution bypasses")
if not order.transmit or order.whatIf:
raise ValueError("placement requires transmit=True and whatIf=False")
if order.orderId or order.clientId or order.permId:
raise ValueError("placement requires fresh order identifiers")
if not order.orderRef or len(order.orderRef) > 64:
raise ValueError("placement requires an intent-specific orderRef")
if any(
(
order.faGroup,
order.faProfile,
order.faMethod,
order.faPercentage,
order.modelCode,
)
):
raise ValueError("per-account order cannot include allocation fields")


def build_paper_order(
account: str,
accessible_accounts: frozenset[str],
quantity: float,
limit_price: float,
intent_ref: str,
) -> LimitOrder:
order = LimitOrder(
action="BUY",
totalQuantity=quantity,
lmtPrice=limit_price,
account=account,
tif="DAY",
outsideRth=False,
transmit=True,
orderRef=intent_ref,
)
validate_placement(order, accessible_accounts)
return order


class PaperOrderOwner:
def __init__(
self,
ib: IB,
accessible_accounts: frozenset[str],
authorized_accounts: frozenset[str],
connection_epoch: str,
policy_version: str,
persist: Callable[[dict[str, object]], None],
timeout: float = 15,
) -> None:
if not isfinite(timeout) or timeout <= 0:
raise ValueError("order timeout must be positive and finite")
if accessible_accounts != authorized_accounts:
raise ValueError("accessible and authorized paper scope must match")
self.ib = ib
self.accessible_accounts = authorized_accounts
self.connection_epoch = connection_epoch
self.policy_version = policy_version
self.persist = persist
self.timeout = timeout
self.trade: Trade | None = None
self.unknown_outcome = False
self.execution_reconciliation_required = False
self.recovery_handed_off = False
self._identity: tuple[str, int, int, int, str] | None = None
self._last_intent: dict[str, object] | None = None
self._recovery_reason = ""

def _intent_record(
self, kind: str, contract: Contract, order: Order
) -> dict[str, object]:
return {
"kind": kind,
"observed_at": datetime.now(timezone.utc).isoformat(),
"connection_epoch": self.connection_epoch,
"policy_version": self.policy_version,
"account": order.account,
"con_id": contract.conId,
"sec_type": contract.secType,
"symbol": contract.symbol,
"local_symbol": contract.localSymbol,
"exchange": contract.exchange,
"primary_exchange": contract.primaryExchange,
"currency": contract.currency,
"trading_class": contract.tradingClass,
"order_ref": order.orderRef,
"action": order.action,
"quantity": float(order.totalQuantity),
"order_type": order.orderType,
"limit_price": float(order.lmtPrice),
"tif": order.tif,
"outside_rth": order.outsideRth,
"transmit": order.transmit,
"what_if": order.whatIf,
"override_percentage_constraints": order.overridePercentageConstraints,
"fa_group": order.faGroup,
"model_code": order.modelCode,
}

def _write(self, record: dict[str, object], post_dispatch: bool) -> None:
previous_unknown = self.unknown_outcome
if post_dispatch:
# Poison first: a failing durable write must never permit another command.
self.unknown_outcome = True
try:
self.persist(record)
except Exception:
if post_dispatch:
self._recovery_reason = f"persistence-failed:{record['kind']}"
raise
else:
if post_dispatch:
self.unknown_outcome = previous_unknown

def _record(
self,
kind: str,
trade: Trade | None = None,
extra: dict[str, object] | None = None,
) -> None:
item = trade or self.trade
record: dict[str, object] = {
"kind": kind,
"observed_at": datetime.now(timezone.utc).isoformat(),
"connection_epoch": self.connection_epoch,
"policy_version": self.policy_version,
}
if item is not None:
latest_log = item.log[-1] if item.log else None
record.update(
account=item.order.account,
con_id=item.contract.conId,
sec_type=item.contract.secType,
symbol=item.contract.symbol,
local_symbol=item.contract.localSymbol,
exchange=item.contract.exchange,
primary_exchange=item.contract.primaryExchange,
currency=item.contract.currency,
trading_class=item.contract.tradingClass,
order_ref=item.order.orderRef,
action=item.order.action,
quantity=float(item.order.totalQuantity),
order_type=item.order.orderType,
tif=item.order.tif,
outside_rth=item.order.outsideRth,
transmit=item.order.transmit,
what_if=item.order.whatIf,
override_percentage_constraints=(
item.order.overridePercentageConstraints
),
fa_group=item.order.faGroup,
model_code=item.order.modelCode,
client_id=item.order.clientId,
order_id=item.order.orderId,
perm_id=max(item.order.permId, item.orderStatus.permId),
status=item.orderStatus.status,
filled=float(item.orderStatus.filled),
remaining=float(item.orderStatus.remaining),
avg_fill_price=float(item.orderStatus.avgFillPrice),
parent_id=item.orderStatus.parentId,
last_fill_price=float(item.orderStatus.lastFillPrice),
why_held=item.orderStatus.whyHeld,
market_cap_price=float(item.orderStatus.mktCapPrice),
limit_price=float(item.order.lmtPrice),
log_message=latest_log.message if latest_log else "",
log_error_code=latest_log.errorCode if latest_log else 0,
advanced_error=item.advancedError,
)
if extra:
record.update(extra)
self._write(record, post_dispatch=item is not None)

def _on_status(self, trade: Trade) -> None:
self._record("status-observed", trade)

def record_correlated_error(
self, req_id: int, error_code: int, error_text: str
) -> None:
if self.trade is not None and req_id == self.trade.order.orderId:
self._record(
"correlated-error",
extra={"error_code": error_code, "error_text": error_text},
)

def _require_known(self) -> Trade:
if self.unknown_outcome:
raise RuntimeError("reconciliation required before more order work")
if self.trade is None:
raise RuntimeError("no owned order")
return self.trade

async def _wait_for(
self, ready: Callable[[Trade], bool], operation: str
) -> Trade:
trade = self._require_known()
if ready(trade):
return trade
loop = asyncio.get_running_loop()
future: asyncio.Future[Trade] = loop.create_future()

def on_status(updated: Trade) -> None:
if ready(updated) and not future.done():
future.set_result(updated)

trade.statusEvent += on_status
try:
return await asyncio.wait_for(
asyncio.shield(future), timeout=self.timeout
)
except (asyncio.TimeoutError, asyncio.CancelledError):
self.unknown_outcome = True
self._record(f"{operation}-outcome-unknown")
raise
finally:
trade.statusEvent -= on_status
if not future.done():
future.cancel()

async def submit(self, contract: Contract, order: Order) -> Trade:
if self.trade is not None or self.unknown_outcome:
raise RuntimeError("owner already used or requires reconciliation")
if contract.conId <= 0:
raise ValueError("placement requires a qualified contract")
validate_placement(order, self.accessible_accounts)
self._last_intent = self._intent_record("intent-ready", contract, order)
self._write(self._last_intent, post_dispatch=False)
try:
trade = self.ib.placeOrder(contract, order)
except Exception:
self.unknown_outcome = True
self._recovery_reason = "submit-dispatch-error"
self._write(
self._intent_record(
"submit-dispatch-error-unknown", contract, order
),
post_dispatch=True,
)
raise
self.trade = trade
trade.statusEvent += self._on_status
self._record("local-submit-dispatched")
await self._wait_for(
lambda item: (
(
item.orderStatus.status in ACKNOWLEDGED
and max(item.order.permId, item.orderStatus.permId) > 0
)
or item.orderStatus.status in TERMINAL
),
"submit",
)
status = trade.orderStatus.status
if status == OrderStatus.Filled:
self.execution_reconciliation_required = True
self._record("submit-terminal-filled")
return trade
if status in CANCEL_CONFIRMED:
if trade.orderStatus.filled > 0:
self.execution_reconciliation_required = True
self._record("submit-terminal-cancelled")
return trade
if status == OrderStatus.Inactive:
self.unknown_outcome = True
self._record("submit-terminal-inactive")
raise RuntimeError("inactive order requires reconciliation")
self._identity = (
trade.order.account,
trade.order.clientId,
trade.order.orderId,
max(trade.order.permId, trade.orderStatus.permId),
trade.order.orderRef,
)
self._record("broker-acknowledged")
return trade

async def modify_limit(self, new_limit: float) -> Trade:
trade = self._require_known()
if trade.orderStatus.status not in ACKNOWLEDGED or self._identity is None:
raise RuntimeError("order is not in a modifiable working state")
validate_limit_value(new_limit)
identity = (
trade.order.account,
trade.order.clientId,
trade.order.orderId,
max(trade.order.permId, trade.orderStatus.permId),
trade.order.orderRef,
)
if identity != self._identity:
raise RuntimeError("owned order identity changed")
if float(trade.order.lmtPrice) == float(new_limit):
raise ValueError("modification must change the limit")
start = len(trade.log)
modification = self._intent_record(
"modify-intent-ready", trade.contract, trade.order
)
modification.update(
client_id=trade.order.clientId,
order_id=trade.order.orderId,
perm_id=identity[3],
previous_limit=float(trade.order.lmtPrice),
desired_limit=float(new_limit),
)
self._write(modification, post_dispatch=False)
trade.order.lmtPrice = new_limit
try:
returned = self.ib.placeOrder(trade.contract, trade.order)
except Exception:
self.unknown_outcome = True
self._record("modify-dispatch-error-unknown")
raise
if returned is not trade:
self.unknown_outcome = True
raise RuntimeError("modification lost Trade identity")
self._record("local-modify-dispatched")
await self._wait_for(
lambda item: (
any(entry.message == "Modified" for entry in item.log[start:])
or item.orderStatus.status in TERMINAL
),
"modify",
)
if trade.orderStatus.status == OrderStatus.Filled:
self.execution_reconciliation_required = True
self._record("modify-terminal-filled")
return trade
if trade.orderStatus.status in CANCEL_CONFIRMED:
if trade.orderStatus.filled > 0:
self.execution_reconciliation_required = True
self._record("modify-terminal-cancelled")
return trade
if trade.orderStatus.status == OrderStatus.Inactive:
self.unknown_outcome = True
self._record("modify-terminal-inactive")
raise RuntimeError("inactive modification requires reconciliation")
self._record("modify-acknowledged")
return trade

async def cancel(self) -> str:
trade = self._require_known()
if trade.orderStatus.status in TERMINAL:
return trade.orderStatus.status
self._record("cancel-requested")
try:
returned = self.ib.cancelOrder(trade.order)
except Exception:
self.unknown_outcome = True
self._record("cancel-dispatch-error-unknown")
raise
if returned is not trade:
self.unknown_outcome = True
raise RuntimeError("cancel did not resolve the owned Trade")
self._record("local-cancel-pending")
await self._wait_for(
lambda item: item.orderStatus.status in TERMINAL,
"cancel",
)
status = trade.orderStatus.status
if status in CANCEL_CONFIRMED:
if trade.orderStatus.filled > 0:
self.execution_reconciliation_required = True
self._record("cancel-confirmed")
elif status == OrderStatus.Filled:
self.execution_reconciliation_required = True
self._record("fill-won-cancel-race")
else:
self.unknown_outcome = True
self._record("terminal-reconciliation-required")
return status

@property
def recovery_required(self) -> bool:
return (
self.unknown_outcome
or self.execution_reconciliation_required
or (self.trade is not None and not self.trade.isDone())
)

def recovery_work(self, reason: str) -> dict[str, object]:
record = dict(self._last_intent or {})
record.update(
connection_epoch=self.connection_epoch,
policy_version=self.policy_version,
)
if self.trade is not None:
item = self.trade
record.update(
account=item.order.account,
con_id=item.contract.conId,
order_ref=item.order.orderRef,
client_id=item.order.clientId,
order_id=item.order.orderId,
perm_id=max(item.order.permId, item.orderStatus.permId),
status=item.orderStatus.status,
filled=float(item.orderStatus.filled),
remaining=float(item.orderStatus.remaining),
)
record.update(
kind="order-recovery-handoff",
observed_at=datetime.now(timezone.utc).isoformat(),
reason=self._recovery_reason or reason,
execution_reconciliation_required=(
self.execution_reconciliation_required
),
)
return record

async def finalize(
self, handoff: Callable[[dict[str, object]], None]
) -> None:
if self.trade is not None and not self.trade.isDone() and not self.unknown_outcome:
try:
await self.cancel()
except (Exception, asyncio.CancelledError):
pass
if self.recovery_required:
# If this write fails, do not detach or deliberately disconnect.
handoff(self.recovery_work("finalization-requires-reconciliation"))
self.recovery_handed_off = True
if self.trade is not None:
self.trade.statusEvent -= self._on_status


def persist(record: dict[str, object]) -> None:
# Replace with a transactional durable writer before using this pattern.
print(record)


def persist_recovery(record: dict[str, object]) -> None:
# Replace with a durable outbox/operator queue independent of the order writer.
print("RECOVERY", record)


async def main() -> None:
ib = IB()
owner: PaperOrderOwner | None = None

def on_error(
req_id: int, error_code: int, error_text: str, _contract: Contract
) -> None:
if owner is not None:
owner.record_correlated_error(req_id, error_code, error_text)

ib.errorEvent += on_error
try:
await ib.connectAsync(
host="127.0.0.1",
port=4002,
clientId=12,
timeout=10,
readonly=False,
raiseSyncErrors=True,
)
expected_accounts = frozenset({"PAPER_ACCOUNT_A"})
accessible_accounts = frozenset(ib.managedAccounts())
if accessible_accounts != expected_accounts:
raise RuntimeError("authorized paper account scope mismatch")
qualified = await ib.qualifyContractsAsync(
Stock("AAPL", "SMART", "USD", primaryExchange="NASDAQ")
)
if len(qualified) != 1:
raise LookupError("contract did not resolve uniquely")

order = build_paper_order(
account="PAPER_ACCOUNT_A",
accessible_accounts=accessible_accounts,
quantity=1,
limit_price=1.00,
intent_ref="paper-order:rebalance:example-001",
)
owner = PaperOrderOwner(
ib,
accessible_accounts,
expected_accounts,
connection_epoch="paper-epoch-example-001",
policy_version="paper-order-policy-v1",
persist=persist,
)
trade = await owner.submit(qualified[0], order)
if trade.orderStatus.status in ACKNOWLEDGED:
trade = await owner.modify_limit(1.01)
if not trade.isDone():
await owner.cancel()
finally:
if owner is not None:
# A failed recovery handoff prevents the deliberate disconnect below.
await owner.finalize(persist_recovery)
ib.errorEvent -= on_error
ib.disconnect()


if __name__ == "__main__":
asyncio.run(main())

finalize first requests and waits for cancellation when the owned result is still known and active. If the outcome is already unknown, cancellation cannot be confirmed, a post-dispatch write failed, or executions need the next reconciliation slice, it writes an order-recovery-handoff record through the independent recovery channel before removing the status observer. If that handoff write fails, the example does not reach its deliberate disconnect. The handoff is durable work to reconcile; it is not proof that the order was cancelled.

Every derived status record includes the intent and routing snapshot, policy/connection epoch, all OrderStatus fields, the latest warning/error log fields, and advanced rejection data. record_correlated_error adds the error code and text from IB.errorEvent when its request ID matches the owned API order ID. The persistence tests deliberately fail each post-submit, post-modify, and post-cancel write to prove that no later order command is allowed.

modifyEvent and the local TradeLogEntry(message="Modify") mean the library dispatched a modification. Pinned ib_async records message="Modified" only when a later unchanged Submitted callback follows that local marker; a changed callback also updates the trade. Neither form is an execution guarantee. Persist the desired change before dispatch and the observed callback afterward.

cancelEvent is also local. cancelledEvent is emitted when pinned ib_async processes Cancelled (or immediately for a narrow local untransmitted/inactive path). Applications should still persist the full status payload and reconcile ApiCancelled, Inactive, correlated errors, and fills rather than treating an event name as a universal terminal contract.

Identity and ownership rules

  • Submit a fresh object with zero orderId, clientId, and permId; let the connected library allocate the API order ID.
  • Persist a unique non-secret orderRef before dispatch. It is application correlation, not an IBKR idempotency key; repeating it does not make a repeated placeOrder safe.
  • After acknowledgement, persist (account, clientId, orderId, permId, orderRef) together. The API order ID is client-scoped; permId is the stronger cross-session broker correlation once assigned.
  • Modify only an active order owned by the same username/session client ID, with the same API order ID. This slice changes only the limit price. For materially different semantics, cancel, confirm/reconcile, then create a new intent.
  • Do not assume master-client visibility or reqAllOpenOrders grants modification rights. Individual cancellation is restricted to the owning client ID, except client 0's documented handling of its bound TWS orders.
  • Never fall back to reqGlobalCancel for one missing order; it cancels every active order visible to that TWS session regardless of origin.

Callback and persistence rules

Persist before acknowledging each application command:

  • immutable intent, account/contract snapshot, policy version, quantity/price/TIF, and orderRef before submission;
  • local client/order IDs and PendingSubmit immediately after dispatch;
  • every changed status payload, permId, filled/remaining quantities, hold reason, cap price, warning, and correlated error;
  • desired and dispatched modification versions plus the broker-observed result;
  • cancellation request time, local PendingCancel, confirmed terminal status, and any fill that raced with cancellation.

Exact duplicate status payloads are suppressed by pinned ib_async's high-level trade event, except the special unchanged Submitted callback used to acknowledge a local modification. Write idempotent persistence anyway, because upstream duplicates and semantically repeated observations remain possible. Never resubmit solely because an expected callback did not arrive.

Failure and recovery matrix

SignalMeaningRequired action
Submit/modify/cancel timeout or caller cancellationThe command may have reached TWS or IBKR even though the application did not classify the result.Stop issuing order commands for that intent, retain the durable intent/command record, reconnect if connection health is uncertain, and reconcile open/completed orders plus executions before deciding anything.
PendingSubmitLocal dispatch or broker processing is incomplete.Do not report accepted or resubmit. Continue observing callbacks/errors.
PendingCancel or PreCancelledCancellation is not confirmed and a fill may still arrive.Keep execution handling active; do not release risk/reserve state yet.
Cancelled/ApiCancelled with partial quantity filledOnly the remaining balance is cancelled.Persist the terminal status and retain/reconcile every execution.
Filled after cancel requestThe execution won the race.Treat the order as filled, not cancelled; reconcile fills and commissions.
Inactive, validation error, warning, or correlated order errorThe order is not proven working and may require policy or operator review.Persist raw evidence, fail closed, and reconcile rather than automatically changing fields or IDs.
Duplicate-order-ID or modification/ownership errorIdentity or client ownership is wrong.Do not allocate a guessed replacement ID or submit a new order; recover the owning client state and reconcile first.
Disconnect after local dispatchIn-memory Trade state is not a durable ledger.Reconnect through the readiness workflow and reconcile by account, client/order IDs, permId, orderRef, and executions.

Evidence and offline boundary

Behavioral claims above are backed by current IBKR Campus sections for placing orders, modifying orders, cancelling orders, order status, and order placement considerations, plus pinned ib_async 2.1.0 placement/modification and cancellation, open-order and status handling, and Trade/OrderStatus.

CI compiles and signature-checks both examples. Offline tests exercise the pinned local placement, duplicate-status suppression, modification acknowledgement, cancellation transition, confirmed cancel, and fill/cancel race. They never connect to TWS, transmit an order, establish paper fill realism, or prove venue acceptance.