When to Use
Invoke this whenever building a trading platform or strategy engine intended to run
across multiple brokers or venues. Coupling strategy code directly to broker SDKs
(Zerodha kiteconnect, Alpaca alpaca-py, IBKR ibapi) fragments the codebase, leaks
float imprecision into prices, and makes venue migration a rewrite. An abstract
BaseBrokerAdapter with standardized models (OrderRequest, OrderResult, Position,
AccountBalance) in Decimal isolates strategy code from broker API drift.
The two things this layer must get right, because everything downstream trusts it: the status normalization (does the strategy believe this order is live?) and the request validation (does a malformed order reach the venue?).
When NOT to Use
- As a working broker client.
scripts/broker_adapter.pydefines the contract and ships simulated adapters that fabricate fills. It performs no network I/O, no auth, no rate limiting. Real adapters wrap real SDKs behind this interface. - As a single-broker abstraction. If you will only ever trade one venue, the indirection costs more than it saves; use the SDK directly and keep the enum normalization.
- For anything the interface does not model. Bracket/OCO orders, order modification,
multi-leg and options strategies, streaming order updates, and per-venue product types
(Zerodha's MIS/CNC/NRML) are outside
OrderRequest. Extend the model deliberately rather than smuggling them through a broker-specific side channel — that reintroduces the coupling this skill exists to remove. - As a substitute for idempotency or auth handling — see the Related Skills.
Prerequisites
- Python
abcfor the interface anddecimal.Decimalfor all monetary values. - Each broker's documented status enumeration, not the four statuses you happened to
see in testing.
references/standards.mdlists them with sources. - A typed exception hierarchy (
BrokerAdapterErrorand subclasses) that every adapter maps its SDK errors into. - A registry key per broker, supplied by configuration.
Workflow
-
Model the domain in
Decimal, and enforce it at the boundary._to_decimalacceptsDecimalandint(exact) and rejectsfloatwith an explanatory error. A float that slips through survives every comparison and only fails much later, as aTypeErrorthe first time it meets aDecimalin arithmetic — far from the code that introduced it. -
Validate the request on the base class, not in each adapter.
_validate_requestenforces symbol, enum types, finite positive quantity, and order-type/price consistency: LIMIT and STOP_LIMIT require a positive price, STOP and STOP_LIMIT require a positive stop price, and MARKET must not carry one. Putting it on the base class means a newly written adapter cannot forget it. -
Normalize status conservatively.
normalize_statusupper-cases and looks up_STATUS_MAP. An unmapped status returnsOrderStatus.UNKNOWNand logs at ERROR — neverPENDING. HandleUNKNOWNby re-querying or reconciling; it is neither live nor terminal, andOrderResult.is_terminalreturns False for it. Treat its appearance as a defect report: the broker has a status your map does not know. -
Echo
client_order_idon everyOrderResult. Without it the caller cannot correlate a response to the request that produced it, and retry-safe submission is impossible. -
Register adapters explicitly; the factory registry starts empty.
create()raises until you register a real adapter. The simulated adapters are not bound to production broker names by default — callregister_simulated_adapters()to opt in for offline work.register()rejects any class that is not aBaseBrokerAdapter, so a bad wiring fails at startup rather than on the first order. -
Treat
cancel_orderas a request, not a cancellation. ATruereturn means the broker accepted the cancellation request. The order can still fill in the race window. Confirm withget_order_statusbefore releasing risk budget or reusing the ID.
Full step-by-step procedure with broker-specific detail: see
references/workflows.md. Documented status sets per broker, with sources: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Defaulting an unrecognized status to PENDING. This is the worst default available:
it asserts the order is live and working. Real terminal statuses get missed by
hand-written maps — Zerodha's
LAPSED, IBKR'sApiCancelled, Alpaca'sdone_for_dayandreplacedare all finished, and calling any of them PENDING leaves the strategy waiting on a dead order or re-sending one that already resolved. - Case-sensitive status lookup. IBKR returns mixed-case strings (
PreSubmitted,ApiCancelled). A lookup that matches"Filled"but not"FILLED"sends the variant to the default branch — a filled order reported as still working. - Mapping only the statuses you saw in testing. Kite documents roughly a dozen order states and Alpaca a dozen more; the four obvious ones are not the contract.
- Binding simulated adapters to production broker names. A factory that resolves
config["broker"]to a mock reports every order FILLED at an invented price, with no error anywhere. - Falsy-checking a price.
if request.pricetreatsDecimal("0")as absent, so a zero limit price gets silently replaced by a default instead of rejected. - Floating point at the boundary.
floatcannot represent ordinary decimal prices and tick sizes exactly; constructDecimal(str(value))where the value enters. - Leaky abstractions. Broker SDK exceptions, raw JSON, and — easy to miss —
decimal.InvalidOperationfrom aNaNcomparison must all be wrapped intoBrokerAdapterErrorsubclasses before crossing the adapter boundary. - Treating a cancel acknowledgement as a cancellation.
- Mutating the shared registry from library code. It is process-wide class state; use
reset()for test isolation.
Verification
- Run the unit suite and confirm every test passes:
python -m unittest discover -s skills/broker-agnostic-adapter-interface/scripts - For each adapter, assert an invented status string returns
UNKNOWN, notPENDING. This is the highest-value single assertion in the suite. - Assert every status in the broker's documented enumeration maps to something, and
that terminal broker states map to terminal
OrderStatusvalues. - Assert case variants (
"Filled","FILLED","filled") normalize identically. - Assert a
floatquantity or price is rejected, and that anintwidens toDecimallosslessly. - Assert
create()raises on an empty registry, and thatregister()refuses a class that does not implementBaseBrokerAdapter. - Place orders through every adapter and confirm
filled_quantity,average_priceandcommissionare allDecimalinstances.