909 lines
41 KiB
Markdown
909 lines
41 KiB
Markdown
|
|
# Typed EMS and Brokerd Messaging
|
||
|
|
|
||
|
|
## October 2026 implementation baseline
|
||
|
|
|
||
|
|
This revision carries the August 28 design into the September Piker
|
||
|
|
baseline (`eed58566`). Preserve IB execution-ID accounting (`5eb4fc9d`)
|
||
|
|
and socket-disconnect propagation (`efa72980`) while changing payloads.
|
||
|
|
The installed Tractor is `5c98ab1f`; qualification must distinguish
|
||
|
|
that April revision from local `main` (`5cb600a8`) and the September
|
||
|
|
context-fault development stack. Do not test against a dirty upstream
|
||
|
|
checkout without recording its changes.
|
||
|
|
|
||
|
|
The complete contract includes local client queues, EMS startup and
|
||
|
|
service readiness, both order-control streams, quote normalization for
|
||
|
|
dark/paper execution, and typed internal trigger/dialog records.
|
||
|
|
Provider-native APIs and persistent ledger formats retain their own
|
||
|
|
normalization boundaries. Runtime handles are not wire messages.
|
||
|
|
|
||
|
|
Use explicit Struct validation before local state mutation or sending;
|
||
|
|
annotations and Struct construction do not validate field values.
|
||
|
|
Receive decoding proves wire shape, not the sender's Python type: a
|
||
|
|
correctly tagged mapping has the same encoding as a Struct.
|
||
|
|
|
||
|
|
Endpoint-wide unions need separate startup, receive-direction, and
|
||
|
|
final-result checks. Validate RPC arguments explicitly until Tractor
|
||
|
|
issue #514 supplies signature-derived `Start` contracts. Never admit
|
||
|
|
`Any` or raw diagnostic mappings into the clearing wire schema.
|
||
|
|
|
||
|
|
Frozen snapshots must use replacement rather than the inherited
|
||
|
|
`Struct.copy(update=...)`, which mutates its source. Keep mutable EMS
|
||
|
|
dialog state separate from published messages and retain execution IDs
|
||
|
|
for fill deduplication. Position messages remain explicit summaries;
|
||
|
|
they must never become synthetic entries in durable trade accounting.
|
||
|
|
|
||
|
|
Port each boundary's producers and consumers together before enabling
|
||
|
|
its strict receive set. Qualify schema errors, wrong-direction and
|
||
|
|
wrong-phase messages, startup completion, cancel-before-ack, reordered
|
||
|
|
and repeated fills, transport failure, and bounded teardown with
|
||
|
|
offline provider fixtures and real local actors. Live order submission
|
||
|
|
is outside this qualification.
|
||
|
|
|
||
|
|
|
||
|
|
### Qualified dependency baseline and first review boundary
|
||
|
|
|
||
|
|
The first boundary introduces `._schema`, local validation in `._codec`,
|
||
|
|
directional streams and lifecycle checks in `._protocol`, and a pure
|
||
|
|
`._state.OrderState` reducer. Existing endpoints keep their original
|
||
|
|
`._messages` imports until their paired migration. This is infrastructure
|
||
|
|
qualification, not a claim that paper or any live adapter is ported.
|
||
|
|
|
||
|
|
The installed baseline is Python 3.13.14, msgspec 0.19.0, Trio 0.32.0,
|
||
|
|
and Tractor `5c98ab1fb6e2cb7781aa75845c7f323a52853c46`. Its existing
|
||
|
|
payload APIs support this boundary without a dependency update.
|
||
|
|
All eight actor contract tests also pass against clean local Tractor
|
||
|
|
`main` at `5cb600a84b6e9fcac68bad04a487b83640b3e5fb`.
|
||
|
|
|
||
|
|
The first set has 65 schema/state/phase tests and eight real IPC tests.
|
||
|
|
These tests cover protocol fixtures and the new reducer; adapter-level
|
||
|
|
conformance, feed-driven paper clearing, abrupt transport EOF, and
|
||
|
|
other spawn backends remain qualification work for subsequent ports.
|
||
|
|
|
||
|
|
That local main adds `--ll` to Tractor's pytest plugin; Piker's current
|
||
|
|
`tests/conftest.py` adds the same option. The ordinary downstream
|
||
|
|
invocation therefore fails before collection. The isolated API check
|
||
|
|
uses `--noconftest -p tractor._testing.pytest` on the same test file.
|
||
|
|
Resolve that harness collision before upgrading Piker's dependency pin.
|
||
|
|
Do not conflate this isolated API qualification with a whole-suite pass.
|
||
|
|
|
||
|
|
Relevant unmerged Tractor work remains a separate qualification input:
|
||
|
|
`pld_dec_refinements` (`6bd685ad`), `auto_codecs` (`45508252`),
|
||
|
|
`wkt/context_fault_status` (`9547c2f9`), and
|
||
|
|
`wkt/ipc_fault_regressions` (`fe9eb738`). The context-fault worktree has
|
||
|
|
uncommitted transport-error handling changes; it is not a clean test
|
||
|
|
baseline. The IPC-fault branch includes `192411c4` for transport EOF
|
||
|
|
propagation. Typed Start arguments remain upstream work; validate them
|
||
|
|
explicitly when migrating the actual clearing contexts.
|
||
|
|
|
||
|
|
## Purpose
|
||
|
|
|
||
|
|
Replace EMS's dictionary-shaped actor traffic with explicit, tagged
|
||
|
|
`msgspec.Struct` protocols and modern Tractor payload enforcement on both
|
||
|
|
actor boundaries:
|
||
|
|
|
||
|
|
```text
|
||
|
|
order client <-> emsd <-> brokerd.<backend> / paperboi.<backend>
|
||
|
|
```
|
||
|
|
|
||
|
|
The broker boundary is part of the formal protocol, not an implementation
|
||
|
|
detail behind the EMS endpoint. Paper, Binance, Kraken, and IB must all emit
|
||
|
|
the same startup type, consume the same request union, and produce the same
|
||
|
|
event union before strict Tractor payload limits are enabled.
|
||
|
|
|
||
|
|
This plan deliberately normalizes schemas and backend behavior before adding
|
||
|
|
`pld_spec`. Enabling transport validation first would only turn existing
|
||
|
|
status drift, startup-shape drift, and raw dictionary reconstruction into
|
||
|
|
hard actor failures without first defining the intended behavior.
|
||
|
|
|
||
|
|
## Scope
|
||
|
|
|
||
|
|
This work includes:
|
||
|
|
|
||
|
|
- typed client-to-EMS requests and EMS-to-client events;
|
||
|
|
- typed EMS-to-broker requests and broker-to-EMS events;
|
||
|
|
- one typed broker startup payload shared by live and paper endpoints;
|
||
|
|
- one typed EMS startup payload;
|
||
|
|
- stable tagged unions suitable for native `msgspec` decoding;
|
||
|
|
- normalized status, identifier, startup, and metadata semantics;
|
||
|
|
- per-direction `Context.pld_rx.limit_plds()` enforcement;
|
||
|
|
- backend-neutral conformance tests for paper, Binance, Kraken, and IB;
|
||
|
|
- deterministic actor-free tests and real Tractor actor/IPC tests;
|
||
|
|
- bounded handling of acknowledgement, status, cancellation, and teardown
|
||
|
|
races;
|
||
|
|
- removal of raw dictionary dispatch after all producers have migrated.
|
||
|
|
|
||
|
|
This work does not include:
|
||
|
|
|
||
|
|
- typing provider-native websocket or REST payloads inside each backend;
|
||
|
|
- changing the datad/brokerd actor split;
|
||
|
|
- spawning brokerd from any location other than `open_brokerd_dialog()`;
|
||
|
|
- redesigning accounting or replacing `BrokerdPosition` with the full
|
||
|
|
accounting `Position` type in the same patch series;
|
||
|
|
- live order submission in CI;
|
||
|
|
- preserving undocumented dictionary IPC between actors running different
|
||
|
|
Piker revisions. The actor tree is treated as a same-checkout deployment.
|
||
|
|
|
||
|
|
## Current Contract Inventory
|
||
|
|
|
||
|
|
### Client and EMS boundary
|
||
|
|
|
||
|
|
| Location | Startup payload | Stream received | Stream sent | Current issue |
|
||
|
|
| --- | --- | --- | --- | --- |
|
||
|
|
| `_emsd_main()` | tuple of positions map, accounts list, dialogs map | dictionaries reconstructed as `Order` or `Cancel` | `Status` and `BrokerdPosition`, often decoded as dictionaries by clients | no payload spec, tuple startup, raw matching, loose nested fields |
|
||
|
|
| `open_ems()` / `OrderClient` | destructures tuple | `Status` and `BrokerdPosition` dictionaries | annotated as `Order | dict`, but implementation assumes struct attributes | public shape and wire shape are conflated |
|
||
|
|
|
||
|
|
### EMS and broker boundary
|
||
|
|
|
||
|
|
| Endpoint | Startup payload | Stream received by backend | Stream sent by backend | Current issue |
|
||
|
|
| --- | --- | --- | --- | --- |
|
||
|
|
| paper `open_trade_dialog()` | `(list[BrokerdPosition], ['paper'])` | raw `BrokerdOrder` / `BrokerdCancel` dictionaries | ack, status, fill, error, position structs | raw dispatch and network-dependent simulator setup |
|
||
|
|
| Binance `open_trade_dialog()` | `'paper'` sentinel or `(positions, accounts)` | raw request dictionaries | structs plus client-side `Status` for existing orders | mixed protocol layers and `fill` status drift |
|
||
|
|
| Kraken `open_trade_dialog()` | `'paper'` sentinel or `(positions, accounts)` | raw request dictionaries | structs plus client-side `Status` for existing orders | `filled` status drift and race workarounds |
|
||
|
|
| IB `open_trade_dialog()` | `(positions, tuple(accounts))` | raw request dictionaries | structs plus client-side `Status` for existing orders | status can race ack and internal event types leak into sequencing |
|
||
|
|
|
||
|
|
The shared broker boundary already exists operationally: all four endpoints
|
||
|
|
are long-lived Tractor contexts opened by `open_brokerd_dialog()`. The upgrade
|
||
|
|
formalizes that de facto interface instead of allowing each backend to define
|
||
|
|
its own wire dialect.
|
||
|
|
|
||
|
|
## Target Protocol
|
||
|
|
|
||
|
|
### Tagging and decoding policy
|
||
|
|
|
||
|
|
Every union member must have an explicit stable tag. Use one common tag field,
|
||
|
|
called `type`, with explicit protocol values such as `order`, `cancel`,
|
||
|
|
`broker_ack`, `status`, `broker_fill`, `broker_error`, `position`, and
|
||
|
|
`open_order`. Broker requests have distinct `broker_order` and
|
||
|
|
`broker_cancel` tags so decoding cannot confuse the two directions. Do not rely on
|
||
|
|
Python class names as wire tags.
|
||
|
|
|
||
|
|
All newly formalized wire structs should use:
|
||
|
|
|
||
|
|
- `tag='<stable value>'` and `tag_field='type'`;
|
||
|
|
- `frozen=True` for wire snapshots;
|
||
|
|
- `kw_only=True` where it improves constructor safety;
|
||
|
|
- immutable tuple defaults; factories for mutable internal state;
|
||
|
|
- finite `Literal` values for protocol enums;
|
||
|
|
- `forbid_unknown_fields=True` from schema introduction; adopt the new
|
||
|
|
schema only when both endpoints can meet it.
|
||
|
|
|
||
|
|
The tag is the dispatch discriminator. Fields such as `action`, `status`, and
|
||
|
|
`resp` retain only domain meaning and are not overloaded as message type tags.
|
||
|
|
Mutable EMS dialog bookkeeping must use a separate internal state type. Do not
|
||
|
|
make a wire struct mutable merely because current code mutates and re-sends the
|
||
|
|
same `Status` or `Order` instance.
|
||
|
|
|
||
|
|
Define shared scalar aliases and use them consistently:
|
||
|
|
|
||
|
|
```python
|
||
|
|
RequestId = int | str
|
||
|
|
FillId = str
|
||
|
|
```
|
||
|
|
|
||
|
|
### Client-to-EMS requests
|
||
|
|
|
||
|
|
Define:
|
||
|
|
|
||
|
|
```python
|
||
|
|
EmsRequest = Order | Cancel
|
||
|
|
```
|
||
|
|
|
||
|
|
Normalize the request structs as follows:
|
||
|
|
|
||
|
|
- `Order.action` is exactly `buy | sell | alert`.
|
||
|
|
- `Order.exec_mode` is exactly `dark | live`.
|
||
|
|
- `Order.oid`, `symbol`, and `account` remain required strings.
|
||
|
|
- `Order.price` remains `Decimal`; tests must prove transport round-tripping.
|
||
|
|
- `Order.size` remains signed `float` for this migration. Resolving the
|
||
|
|
duplicated side/action representation is a later domain change.
|
||
|
|
- `Cancel` has its own tag; `action='cancel'` can be removed once no local UI
|
||
|
|
matching depends on it.
|
||
|
|
- Actor IPC accepts structs only. Any user-facing convenience constructor for
|
||
|
|
mappings must validate and construct a struct before entering the stream.
|
||
|
|
|
||
|
|
### EMS-to-client events
|
||
|
|
|
||
|
|
Define:
|
||
|
|
|
||
|
|
```python
|
||
|
|
EmsEvent = Status | BrokerdPosition
|
||
|
|
```
|
||
|
|
|
||
|
|
Retain `BrokerdPosition` as the wire-level position summary for this series,
|
||
|
|
but document it as shared by the broker and client event protocols. A later
|
||
|
|
accounting patch may rename it to `PositionSummary` without coupling that
|
||
|
|
change to IPC enforcement.
|
||
|
|
|
||
|
|
Normalize `Status` so that:
|
||
|
|
|
||
|
|
- `resp` is exactly `pending | open | dark_open | triggered | fill | closed |
|
||
|
|
canceled | error`;
|
||
|
|
- `req` is a required `Order`, never an unvalidated nested dictionary;
|
||
|
|
an uncorrelated backend error must not fabricate a client dialog;
|
||
|
|
- `brokerd_request` is `BrokerdRequest | None` and records the command that
|
||
|
|
caused the status transition;
|
||
|
|
- `brokerd_event` is `BrokerdEvent | None` and records the backend response;
|
||
|
|
- the current mixed-purpose `brokerd_msg` field is removed after callers have
|
||
|
|
migrated; neither replacement accepts an untyped dictionary;
|
||
|
|
- an error is represented by `Status(resp='error', ...)` or by a distinct
|
||
|
|
tagged client event, not the current incomplete `Error(Status)` subclass;
|
||
|
|
- internal mutable dialog state is not the same object sent repeatedly over
|
||
|
|
the wire. Build a status snapshot when broadcasting so prior history and
|
||
|
|
multi-client delivery cannot be changed by later in-place mutation.
|
||
|
|
|
||
|
|
### EMS-to-broker requests
|
||
|
|
|
||
|
|
Define:
|
||
|
|
|
||
|
|
```python
|
||
|
|
BrokerdRequest = BrokerdOrder | BrokerdCancel
|
||
|
|
```
|
||
|
|
|
||
|
|
Normalize both variants so that:
|
||
|
|
|
||
|
|
- they have explicit `broker_order` and `broker_cancel` protocol tags
|
||
|
|
independent of `action`;
|
||
|
|
- `oid` always identifies the EMS dialog;
|
||
|
|
- `BrokerdOrder.reqid` is `RequestId | None` because allocation policy
|
||
|
|
differs by backend; `BrokerdCancel.reqid` is required, and EMS retains
|
||
|
|
cancel intent until acknowledgement supplies that identity;
|
||
|
|
- account and symbol validation failures produce a typed `BrokerdError`
|
||
|
|
rather than logging and continuing silently;
|
||
|
|
- updates remain `BrokerdOrder` with an existing `reqid`; a separate modify
|
||
|
|
variant is deferred unless conformance tests prove the current distinction
|
||
|
|
ambiguous.
|
||
|
|
|
||
|
|
### Broker-to-EMS events
|
||
|
|
|
||
|
|
Define:
|
||
|
|
|
||
|
|
```python
|
||
|
|
BrokerdEvent = (
|
||
|
|
BrokerdOrderAck
|
||
|
|
| BrokerdStatus
|
||
|
|
| BrokerdFill
|
||
|
|
| BrokerdError
|
||
|
|
| BrokerdPosition
|
||
|
|
| BrokerdOpenOrder
|
||
|
|
| BrokerdOpenOrdersComplete
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
`BrokerdOpenOrder` replaces the current backend-to-EMS use of client-facing
|
||
|
|
`Status`. It carries the provider request id, account, source backend, and a
|
||
|
|
fully typed embedded `Order`. EMS alone translates this event into a client
|
||
|
|
`Status` and registers it in the dialog book.
|
||
|
|
|
||
|
|
Require `oid` on acknowledgements. Status and fill events identify the
|
||
|
|
order by `(account, reqid)` within their broker session; EMS retains that
|
||
|
|
correlation across edits and terminal events. For an external order, the
|
||
|
|
adapter creates a collision-safe stable `oid` from broker, account, and
|
||
|
|
provider `reqid` before emitting `BrokerdOpenOrder`. Its embedded
|
||
|
|
`Order.oid` carries that value. EMS registers it without allocating a
|
||
|
|
reverse-message identity. Service errors may omit `oid`; a typed rejected
|
||
|
|
request can identify a submission which never received a provider ID.
|
||
|
|
|
||
|
|
`BrokerdFill` has a required `fill_id: FillId`. Use the provider execution id
|
||
|
|
or provider event sequence. The four current implementations must use Binance
|
||
|
|
trade id, Kraken transaction id, IB execution id, and a distinct paper-ledger
|
||
|
|
transaction id respectively. Price/size/time are not a safe deduplication key
|
||
|
|
because two legitimate executions may share them. A future provider with no
|
||
|
|
execution identity must define an adapter session-id plus monotonic sequence
|
||
|
|
and document that deduplication does not survive brokerd restart; it may not
|
||
|
|
silently synthesize identity from equal-valued fill fields.
|
||
|
|
|
||
|
|
Use one broker status vocabulary:
|
||
|
|
|
||
|
|
```python
|
||
|
|
BrokerdOrderState = Literal[
|
||
|
|
'pending',
|
||
|
|
'open',
|
||
|
|
'canceled',
|
||
|
|
'closed',
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
Fills are events, not status values. A partial execution emits a
|
||
|
|
`BrokerdFill` and may then emit `BrokerdStatus(status='open', ...)`. A complete
|
||
|
|
execution emits the final `BrokerdFill` followed by
|
||
|
|
`BrokerdStatus(status='closed', remaining=0)`. Binance `fill`, Kraken
|
||
|
|
`filled`, and IB provider status names must be translated inside their
|
||
|
|
backend adapters and must never cross the brokerd IPC boundary.
|
||
|
|
|
||
|
|
### Startup payloads
|
||
|
|
|
||
|
|
Replace every tuple and string sentinel with tagged startup structs.
|
||
|
|
|
||
|
|
`BrokerdStartup` contains:
|
||
|
|
|
||
|
|
- `mode: Literal['live', 'paper', 'paper_fallback']`;
|
||
|
|
- `broker: str`;
|
||
|
|
- `accounts: tuple[str, ...]`;
|
||
|
|
- `positions: tuple[BrokerdPosition, ...]`.
|
||
|
|
|
||
|
|
A live endpoint without credentials returns
|
||
|
|
`BrokerdStartup(mode='paper_fallback', broker=..., accounts=(), positions=())`.
|
||
|
|
`open_brokerd_dialog()` then opens the paper endpoint, which returns the same
|
||
|
|
type with `mode='paper'`, its paper account, and persisted positions. The
|
||
|
|
fallback value is a typed delegation result, not an active broker snapshot: it
|
||
|
|
is exempt from account/position and open-order-completion requirements, and
|
||
|
|
its context returns after startup. This removes the raw `'paper'` union branch
|
||
|
|
without hiding fallback behavior.
|
||
|
|
|
||
|
|
Existing open orders remain `BrokerdOpenOrder` stream events because some
|
||
|
|
providers discover them only after authenticated stream subscription. The
|
||
|
|
contract must state that startup positions/accounts are a complete snapshot,
|
||
|
|
while open-order synchronization completes through typed stream events. Every
|
||
|
|
active live or paper backend emits exactly one `BrokerdOpenOrdersComplete`
|
||
|
|
after the initial set, including when it found zero orders, so EMS can
|
||
|
|
distinguish completion from delay and can order initial client readiness
|
||
|
|
deterministically. A `paper_fallback` delegation does not open a stream and
|
||
|
|
does not emit this event; the subsequently opened paper endpoint does.
|
||
|
|
|
||
|
|
`EmsStartup` contains:
|
||
|
|
|
||
|
|
- `broker: str`;
|
||
|
|
- `positions: tuple[BrokerdPosition, ...]`;
|
||
|
|
- `accounts: tuple[str, ...]`;
|
||
|
|
- `dialogs: tuple[Status, ...]`.
|
||
|
|
|
||
|
|
`open_ems()` may continue returning the existing nested lookup maps to callers,
|
||
|
|
but it builds them locally from `EmsStartup`. The public convenience shape is
|
||
|
|
therefore separated from the wire contract.
|
||
|
|
|
||
|
|
Treat `BrokerdStartup` as the brokerd equivalent of datad's `FeedInit`, but do
|
||
|
|
not reproduce `validate_backend()`'s long-lived dictionary compatibility
|
||
|
|
branches. Add a broker initialization validator beside the clearing protocol
|
||
|
|
that checks backend name, unique/non-empty accounts, position account
|
||
|
|
membership, `position.broker == startup.broker`, unique
|
||
|
|
`(broker, account, symbol)` position keys, and mode-specific requirements.
|
||
|
|
Give malformed startup its own registered initialization error so failures
|
||
|
|
retain a useful remote exception type before strict Tractor decoding takes
|
||
|
|
over. Each account name must be non-empty; the accounts collection may be
|
||
|
|
empty only for `paper_fallback`. Paper sessions announce exactly
|
||
|
|
`('paper',)`. EMS readiness also requires at least one account.
|
||
|
|
|
||
|
|
Replace `open_brokerd_dialog()`'s manual probing with one broker order-control
|
||
|
|
endpoint resolver. It may use `get_eps(mod, 'brokerd')` as input, but it must
|
||
|
|
select exactly one of `open_trade_dialog` or the temporary legacy
|
||
|
|
`trades_dialogue` alias and must not confuse unrelated `norm_trade` support
|
||
|
|
with a trading dialog. The runtime validator proves startup; the conformance
|
||
|
|
suite proves request/event behavior. Neither mechanism should import or enable
|
||
|
|
datad modules inside the credentialed broker actor.
|
||
|
|
|
||
|
|
### Payload aliases and Tractor limits
|
||
|
|
|
||
|
|
The endpoint-wide payload aliases include startup, stream traffic, and the
|
||
|
|
final `None` result:
|
||
|
|
|
||
|
|
```python
|
||
|
|
EmsPayload = EmsStartup | EmsRequest | EmsEvent | None
|
||
|
|
BrokerdPayload = BrokerdStartup | BrokerdRequest | BrokerdEvent | None
|
||
|
|
```
|
||
|
|
|
||
|
|
Apply broad endpoint specs and narrow receive-side specs as follows:
|
||
|
|
|
||
|
|
| Actor side | Endpoint `pld_spec` | Narrow receive spec |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| emsd `_emsd_main()` | `EmsPayload` | `EmsRequest` around the client command loop |
|
||
|
|
| order client | inherited from endpoint | `EmsEvent` around trade event receives |
|
||
|
|
| backend/paper `open_trade_dialog()` | `BrokerdPayload` | `BrokerdRequest` around backend request loops |
|
||
|
|
| emsd broker relay | inherited from endpoint | `BrokerdEvent` around broker event receives |
|
||
|
|
|
||
|
|
The endpoint payload spec supplies the broad decoder used for `Started`, stream,
|
||
|
|
and return payloads. `ctx.started(..., validate_pld_spec=True)` can also perform
|
||
|
|
local started-value validation. Ordinary `MsgStream.send()` does not validate
|
||
|
|
locally: malformed stream data raises `tractor.MsgTypeError` when the peer
|
||
|
|
receives/decodes it. `ctx.pld_rx.limit_plds(spec=...)` narrows that receiving
|
||
|
|
direction so a valid message from the wrong direction still fails there.
|
||
|
|
|
||
|
|
Do not add either decorator spec until the corresponding boundary has no
|
||
|
|
remaining legacy sender. The final enforcement must follow the proven pattern
|
||
|
|
in `remote_gap_overlays()` and Tractor's own payload-limiting tests.
|
||
|
|
|
||
|
|
### Provider metadata
|
||
|
|
|
||
|
|
Provider-native diagnostics remain inside adapters and their logs. The
|
||
|
|
clearing wire schema has no raw-data extension point or `Any` field.
|
||
|
|
Source backend, broker timestamp, symbol, account, order ID, request ID,
|
||
|
|
and execution ID have explicit typed fields. Errors can carry a typed
|
||
|
|
request. Position summaries contain no mutable accounting history.
|
||
|
|
|
||
|
|
## Ordering and State Semantics
|
||
|
|
|
||
|
|
Typed messages alone do not repair ordering defects. Define these invariants
|
||
|
|
before enforcement:
|
||
|
|
|
||
|
|
1. A backend sends exactly one `BrokerdOrderAck` for each accepted new order.
|
||
|
|
2. The ack establishes `oid <-> reqid` before status/fill events are exposed
|
||
|
|
to EMS whenever the provider API permits that ordering.
|
||
|
|
3. A backend adapter that receives provider events before it can send the ack
|
||
|
|
buffers them by `reqid` and flushes them after ack.
|
||
|
|
4. EMS keeps a bounded defensive pre-ack buffer because actor scheduling
|
||
|
|
can still expose ordering bugs. The first reducer bounds event count
|
||
|
|
and fails loudly on overflow without dropping retained executions.
|
||
|
|
Add wall-clock expiry in its asynchronous owner when porting EMS;
|
||
|
|
the pure reducer does not claim an age bound.
|
||
|
|
5. A cancel received before ack marks the dialog `cancel_called`. After
|
||
|
|
replaying buffered events, the first ack sends one typed
|
||
|
|
`BrokerdCancel` if the dialog is still active.
|
||
|
|
6. Duplicate ack, fill, and terminal status events are idempotent or rejected
|
||
|
|
deterministically. Fill deduplication uses `fill_id`. They never resurrect
|
||
|
|
a closed dialog.
|
||
|
|
7. `closed` and `canceled` are terminal. A late fill may be recorded and
|
||
|
|
reported according to an explicit test, but it cannot recreate mutable
|
||
|
|
active state.
|
||
|
|
8. Existing external orders enter through `BrokerdOpenOrder` with the backend's
|
||
|
|
stable namespaced `oid` and are registered before broadcast.
|
||
|
|
9. Initial open-order discovery is complete only after
|
||
|
|
`BrokerdOpenOrdersComplete`; client readiness cannot infer completion from
|
||
|
|
an empty or temporarily quiet stream.
|
||
|
|
10. Order status is routed by dialog and FQME. Account authorization and
|
||
|
|
position privacy remain a separately scoped subscription design because
|
||
|
|
`_emsd_main()` currently receives no account authorization input. Tests in
|
||
|
|
this series document the current position broadcast and must not claim
|
||
|
|
unauthorized-account isolation.
|
||
|
|
|
||
|
|
## Implementation Phases
|
||
|
|
|
||
|
|
### Phase 0: Characterize the Existing Boundary
|
||
|
|
|
||
|
|
Add tests that record current intended behavior before restructuring:
|
||
|
|
|
||
|
|
- new order, update, cancel, ack, open, partial fill, close, and position flow;
|
||
|
|
- cancel-before-ack;
|
||
|
|
- status-before-ack;
|
||
|
|
- existing provider order discovery;
|
||
|
|
- bad account, zero size, and backend submission error as typed domain errors;
|
||
|
|
- bad action/tag/field type as protocol decode failures, not `BrokerdError`;
|
||
|
|
- startup with live credentials, no credentials, paper account, persisted
|
||
|
|
positions, and no positions;
|
||
|
|
- Binance startup containing a negative/short position;
|
||
|
|
- IB startup containing at least two accounts with distinct contracts;
|
||
|
|
- zero, one, and multiple existing orders followed by the explicit snapshot
|
||
|
|
completion event;
|
||
|
|
- disconnect while requests or events are in flight.
|
||
|
|
|
||
|
|
Use fake streams and fake provider clients; do not require public market data
|
||
|
|
or credentials. Capture known undesirable outcomes as explicit expected
|
||
|
|
failures only when a same-phase fix would obscure the baseline.
|
||
|
|
|
||
|
|
Gate: every behavior changed later has a deterministic assertion describing
|
||
|
|
the old behavior and the target behavior.
|
||
|
|
|
||
|
|
### Phase 1: Define and Audit the Wire Schemas
|
||
|
|
|
||
|
|
Introduce the strict schema in `piker/clearing/_schema.py` alongside the
|
||
|
|
legacy `._messages` imports. Qualify it independently before moving each
|
||
|
|
producer/consumer pair together. Remove the legacy module after every
|
||
|
|
endpoint and public consumer has migrated; do not add dictionary bridges.
|
||
|
|
|
||
|
|
Replace mutable literal defaults with factories. Resolve the accidental tuple
|
||
|
|
default on `BrokerdStatus.account`. Remove or complete the unusable
|
||
|
|
`Error.from_status()` path. Add explicit startup types and
|
||
|
|
`BrokerdOpenOrder`. Add the broker initialization error/validator without a
|
||
|
|
legacy dictionary fallback.
|
||
|
|
|
||
|
|
Inventory every current mutation of `Order`, `Status`, and embedded requests.
|
||
|
|
Introduce a mutable internal dialog-state record and construct frozen wire
|
||
|
|
snapshots from it. Replace mutation-based `Struct.copy(update=...)` usage with
|
||
|
|
new-instance replacement that is proven to work for frozen structs.
|
||
|
|
|
||
|
|
Add actor-free serialization tests using the same msgpack semantics as
|
||
|
|
Tractor:
|
||
|
|
|
||
|
|
- each concrete type round-trips with exact type equality;
|
||
|
|
- each union decodes to the correct concrete type by tag;
|
||
|
|
- nested `Order`, broker event, position, `Decimal`, and optional
|
||
|
|
identifier fields round-trip;
|
||
|
|
- unknown tag, missing required field, wrong literal, wrong primitive type,
|
||
|
|
malformed nested struct, and duplicate/ambiguous tag fail;
|
||
|
|
- unknown fields fail from the first use of the new schema;
|
||
|
|
- request and event aliases do not accept each other's members.
|
||
|
|
|
||
|
|
Gate: all protocol unions have unique tags and can be decoded without manual
|
||
|
|
dictionary inspection.
|
||
|
|
|
||
|
|
### Phase 2: Build a Backend Conformance Harness
|
||
|
|
|
||
|
|
Create reusable conformance helpers that exercise a backend request handler
|
||
|
|
with a memory-backed stream and fake provider client. The harness supplies the
|
||
|
|
same typed request cases and validates only typed events. Every request and
|
||
|
|
emitted event must be encoded and decoded through its declared union before
|
||
|
|
assertion; `isinstance` checks alone are insufficient because direct
|
||
|
|
`msgspec.Struct` construction does not validate annotated field types.
|
||
|
|
|
||
|
|
The shared suite must verify:
|
||
|
|
|
||
|
|
- startup is one `BrokerdStartup`, including typed fallback-to-paper;
|
||
|
|
- malformed startup fails the shared broker initialization validator;
|
||
|
|
- new order produces one correlated ack;
|
||
|
|
- update preserves `oid` and `reqid`;
|
||
|
|
- cancel uses the correlated request id;
|
||
|
|
- invalid account/size produces `BrokerdError` without killing the dialog;
|
||
|
|
- invalid action/tag/type is rejected by codec or Tractor before the handler;
|
||
|
|
- provider statuses normalize to the four broker states;
|
||
|
|
- partial and complete fills use `BrokerdFill` plus normalized statuses;
|
||
|
|
- emitted positions and existing orders are typed;
|
||
|
|
- every active live/paper startup emits one open-order completion event, even
|
||
|
|
for zero orders; `paper_fallback` delegates without one;
|
||
|
|
- duplicate fills are rejected by `fill_id`, while distinct equal-valued fills
|
||
|
|
remain valid;
|
||
|
|
- provider-native dictionaries never cross the test stream;
|
||
|
|
- cancellation closes all handler tasks and leaves no blocked sender;
|
||
|
|
- one malformed request fails at the protocol boundary once limits are
|
||
|
|
enabled.
|
||
|
|
|
||
|
|
Separate backend-neutral contract assertions from provider translation
|
||
|
|
fixtures. Provider fixtures should be the smallest recorded websocket/API
|
||
|
|
payload required to prove normalization. Include regressions for Binance's
|
||
|
|
tuple-shaped error reason, string fill size, short startup position, and
|
||
|
|
float-vs-`Decimal` open-order price; Kraken's `filled` status and open-order
|
||
|
|
price; and IB multi-account startup association.
|
||
|
|
|
||
|
|
Gate: the harness can fail a deliberately nonconforming fake backend for each
|
||
|
|
contract rule.
|
||
|
|
|
||
|
|
### Phase 3: Port the Paper Engine as the Reference Backend
|
||
|
|
|
||
|
|
Make paper `open_trade_dialog()` the reference implementation of
|
||
|
|
`BrokerdPayload`:
|
||
|
|
|
||
|
|
- return `BrokerdStartup`;
|
||
|
|
- consume `BrokerdRequest` instances directly;
|
||
|
|
- emit only `BrokerdEvent` instances;
|
||
|
|
- remove `BrokerdOrder(**request_msg)` and dictionary pattern dispatch;
|
||
|
|
- allocate a distinct deterministic `fill_id`/transaction id instead of
|
||
|
|
reusing `oid` as `tid`;
|
||
|
|
- separate its order protocol tests from market-feed simulation;
|
||
|
|
- allow deterministic synthetic ticks to drive fill tests without a public
|
||
|
|
feed.
|
||
|
|
|
||
|
|
Do not enable endpoint `pld_spec` yet if live backends share code paths that
|
||
|
|
still send legacy payloads.
|
||
|
|
|
||
|
|
Gate: the complete broker conformance suite passes for paper without network,
|
||
|
|
credentials, sleeps, or teardown suppression.
|
||
|
|
|
||
|
|
### Phase 4: Port Live Backend Adapters Individually
|
||
|
|
|
||
|
|
Port one backend per independently reviewable change. A backend is complete
|
||
|
|
only when its provider translation fixtures and shared conformance suite pass.
|
||
|
|
|
||
|
|
Recommended order:
|
||
|
|
|
||
|
|
1. Binance: replace raw request matching, type startup/fallback, replace
|
||
|
|
existing-order `Status` with `BrokerdOpenOrder`, and map `fill` to fill plus
|
||
|
|
`open/closed` events.
|
||
|
|
2. Kraken: replace raw request matching, type startup/fallback, replace
|
||
|
|
existing-order `Status`, map `filled` to fill plus `closed`, and formalize
|
||
|
|
the too-fast edit/cancel behavior.
|
||
|
|
3. IB: replace raw request indexing, type startup, replace existing-order
|
||
|
|
`Status`, and put an explicit sequencing gate between asyncio provider
|
||
|
|
events and ack publication.
|
||
|
|
|
||
|
|
For every backend, core fields are extracted before constructing the event.
|
||
|
|
The provider's original message stays inside its adapter and logs.
|
||
|
|
Keep brokerd's enabled RPC module set minimal and do not reintroduce
|
||
|
|
datad modules into the credentialed actor.
|
||
|
|
|
||
|
|
Each adapter must preserve a provider execution id as `fill_id`, emit the
|
||
|
|
initial open-order completion event, and populate a stable namespaced `oid` for
|
||
|
|
external orders. Binance must retain negative positions in startup. IB startup
|
||
|
|
must associate each position with the correct account/client rather than loop
|
||
|
|
state from the last account.
|
||
|
|
|
||
|
|
Gate: paper, Binance, Kraken, and IB all pass the same offline conformance
|
||
|
|
contract; no backend emits a dictionary or client-facing `Status` to EMS.
|
||
|
|
|
||
|
|
### Phase 5: Convert EMS Broker Translation and State
|
||
|
|
|
||
|
|
Change `open_brokerd_dialog()` to consume `BrokerdStartup` and return a typed
|
||
|
|
relay object. Remove startup tuple/string branching and
|
||
|
|
`BrokerdPosition(**msg)` reconstruction.
|
||
|
|
|
||
|
|
Remove the current `client_ready` dependency from initial broker-event
|
||
|
|
processing. Start the broker relay as soon as its stream opens, consume and
|
||
|
|
reduce `BrokerdOpenOrder` events through `BrokerdOpenOrdersComplete`, and only
|
||
|
|
then construct `EmsStartup` and call `_emsd_main().started()`. The resulting
|
||
|
|
startup dialogs contain the completed initial snapshot. Events arriving before
|
||
|
|
the first client stream exists remain in dialog state/history for startup or
|
||
|
|
later delivery rather than blocking the broker sender or being discarded.
|
||
|
|
|
||
|
|
Split the current large translation loop into a small typed event reducer plus
|
||
|
|
async effects:
|
||
|
|
|
||
|
|
- dispatch with class patterns, not mapping patterns;
|
||
|
|
- update correlation/dialog state in a deterministic function or narrowly
|
||
|
|
scoped state object;
|
||
|
|
- keep that state mutable internally but emit only new frozen `Status`
|
||
|
|
snapshots;
|
||
|
|
- return typed client events and typed broker requests to send;
|
||
|
|
- keep stream broadcast and logging in the async shell;
|
||
|
|
- implement bounded pre-ack buffering and terminal-state idempotence;
|
||
|
|
- translate `BrokerdOpenOrder` to `Status` in one place;
|
||
|
|
- stop reading routing fields from `broker_details`.
|
||
|
|
|
||
|
|
The reducer must be directly testable with event sequences and no actors.
|
||
|
|
|
||
|
|
Gate: all order-state permutations pass actor-free tests, and
|
||
|
|
`translate_and_relay_brokerd_events()` contains no struct reconstruction from
|
||
|
|
dictionaries.
|
||
|
|
|
||
|
|
### Phase 6: Convert the Client Boundary
|
||
|
|
|
||
|
|
Change `process_client_order_cmds()` to receive `EmsRequest` directly and use
|
||
|
|
class-pattern dispatch. Return `EmsStartup` from `_emsd_main()` and construct
|
||
|
|
the existing `open_ems()` convenience maps client-side.
|
||
|
|
|
||
|
|
Make `OrderClient.send()` and `send_nowait()` accept `EmsRequest` only. If a
|
||
|
|
mapping convenience API is required by a known caller, put it in an explicit
|
||
|
|
constructor that performs local msgspec validation; do not put dictionaries on
|
||
|
|
the actor stream. Audit UI consumers and tests so receive loops work with
|
||
|
|
already-decoded `Status` and `BrokerdPosition` instances.
|
||
|
|
|
||
|
|
Replace `OrderClient.update*()` and UI mutations with construction of a new
|
||
|
|
`Order`. Audit mutations of `Status.req`, `Status.resp`, and embedded orders in
|
||
|
|
order mode and clearing code; all should update internal state or replace a
|
||
|
|
wire value, never mutate a frozen payload.
|
||
|
|
|
||
|
|
Gate: there are no `Order(**cmd)`, `Cancel(**cmd)`, `Status(**msg)`, or
|
||
|
|
`BrokerdPosition(**msg)` conversions on the EMS client boundary.
|
||
|
|
|
||
|
|
### Phase 7: Enable Tractor Payload Enforcement
|
||
|
|
|
||
|
|
Enable one boundary at a time:
|
||
|
|
|
||
|
|
1. Add `BrokerdPayload` to every paper/live `open_trade_dialog()` decorator.
|
||
|
|
2. Limit backend receive scopes to `BrokerdRequest`.
|
||
|
|
3. Limit EMS's broker receive scope to `BrokerdEvent`.
|
||
|
|
4. Add `EmsPayload` to `_emsd_main()`.
|
||
|
|
5. Limit EMS's client receive scope to `EmsRequest`.
|
||
|
|
6. Limit order-client receive scopes to `EmsEvent`.
|
||
|
|
7. Verify every producer satisfies the already-strict wire schemas.
|
||
|
|
|
||
|
|
At each step, test invalid startup, stream, wrong-direction, and return
|
||
|
|
payloads. Assert where decoding occurs, `tractor.MsgTypeError` provenance,
|
||
|
|
context cancellation, and bounded teardown rather than assuming every sender
|
||
|
|
raises locally. Add an actor-free metadata test that each paper/Binance/Kraken/
|
||
|
|
IB endpoint's `_tractor_context_meta['pld_spec']` resolves to `BrokerdPayload`
|
||
|
|
and `_emsd_main()` resolves to `EmsPayload`.
|
||
|
|
|
||
|
|
Gate: no valid wrong-direction type or malformed payload reaches business
|
||
|
|
logic, and malformed traffic cannot wedge the actor tree.
|
||
|
|
|
||
|
|
### Phase 8: Real Actor and Lifecycle Qualification
|
||
|
|
|
||
|
|
Add test-only Tractor context endpoints in a dedicated test fixture module to
|
||
|
|
exercise actual encoding, process boundaries, and cancellation. Do not mock
|
||
|
|
`MsgStream` in this layer.
|
||
|
|
|
||
|
|
The real-actor matrix includes:
|
||
|
|
|
||
|
|
- valid startup and bidirectional exchange for both protocol families;
|
||
|
|
- exact concrete types observed after remote decode;
|
||
|
|
- invalid `Started`, stream send, wrong-direction send, and return payload;
|
||
|
|
- optional local `Started` validation and peer receive-side stream/return
|
||
|
|
validation;
|
||
|
|
- child failure before and after `ctx.started()`;
|
||
|
|
- client cancellation while backend send is blocked;
|
||
|
|
- backend cancellation while EMS waits for an event;
|
||
|
|
- simultaneous stream close and terminal status;
|
||
|
|
- repeated open/close cycles with no stale subscribers, tasks, actors, or SHM;
|
||
|
|
- two clients with distinct dialogs and no cross-delivery of order status;
|
||
|
|
- deterministic ack/status and cancel/ack race barriers;
|
||
|
|
- brokerd restart/reconnect policy, explicitly failing if unsupported rather
|
||
|
|
than hanging.
|
||
|
|
|
||
|
|
Use Trio events/channels as barriers. Do not use timing sleeps to force races.
|
||
|
|
Use bounded `fail_after()` scopes around every scenario. Do not use
|
||
|
|
`run_and_tollerate_cancels()` for these tests; expected cancellation must be
|
||
|
|
asserted precisely.
|
||
|
|
|
||
|
|
Gate: the offline real-actor suite passes repeatedly under the repository's
|
||
|
|
supported Tractor spawn methods and fails loudly within a bounded timeout.
|
||
|
|
|
||
|
|
### Phase 9: Remove Compatibility Paths
|
||
|
|
|
||
|
|
After all strict tests pass:
|
||
|
|
|
||
|
|
- delete mapping-pattern IPC dispatch and `Struct(**wire_dict)` conversions;
|
||
|
|
- delete the `'paper'` startup sentinel and startup tuple handling;
|
||
|
|
- delete backend emission of client `Status`;
|
||
|
|
- delete obsolete status aliases such as `fill` and `filled`;
|
||
|
|
- delete legacy `trades_dialogue` lookup if no backend implements it;
|
||
|
|
- remove stale dictionary type annotations and compatibility comments;
|
||
|
|
- remove expected failures introduced in Phase 0;
|
||
|
|
- update backend author documentation with the protocol aliases and
|
||
|
|
conformance command.
|
||
|
|
|
||
|
|
Gate: searches for the removed patterns are empty or have an explicit local,
|
||
|
|
non-IPC justification.
|
||
|
|
|
||
|
|
## Test Layout
|
||
|
|
|
||
|
|
Use focused flat modules consistent with the existing test tree:
|
||
|
|
|
||
|
|
| Test module | Responsibility | Network |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| `tests/test_ems_messages.py` | union tags, codec round-trips, invalid schemas, strict fields | never |
|
||
|
|
| `tests/test_brokerd_protocol.py` | shared backend conformance and provider translation fixtures | never |
|
||
|
|
| `tests/test_ems_state.py` | reducer transitions, correlation, races, idempotence, routing | never |
|
||
|
|
| `tests/test_ems_ipc.py` | real Tractor actor payload limits and lifecycle | never |
|
||
|
|
| `tests/test_ems.py` | public `open_ems()` behavior, persistence, paper integration | deterministic by default |
|
||
|
|
| backend-specific live qualification | authenticated smoke tests only | opt-in |
|
||
|
|
|
||
|
|
The default CI lane runs every actor-free and offline actor test. Authenticated
|
||
|
|
or public-network checks use explicit markers and are never the only coverage
|
||
|
|
for a protocol behavior.
|
||
|
|
|
||
|
|
## Required Test Matrix
|
||
|
|
|
||
|
|
### Schema validation
|
||
|
|
|
||
|
|
- every valid concrete message;
|
||
|
|
- every startup mode;
|
||
|
|
- `int` and `str` request identifiers;
|
||
|
|
- zero, positive, and negative sizes where semantically permitted;
|
||
|
|
- `Decimal` prices;
|
||
|
|
- optional and populated typed request/event causes;
|
||
|
|
- unknown/missing/wrong tags;
|
||
|
|
- unknown fields rejected from schema introduction;
|
||
|
|
- wrong literal and primitive field types;
|
||
|
|
- malformed nested order/event/position;
|
||
|
|
- a request sent in the event direction and vice versa.
|
||
|
|
|
||
|
|
### Dialog transitions
|
||
|
|
|
||
|
|
- submit -> ack -> pending -> open;
|
||
|
|
- submit -> ack -> fill -> open -> fill -> closed;
|
||
|
|
- submit -> ack -> canceled;
|
||
|
|
- submit -> cancel -> ack -> broker cancel -> canceled;
|
||
|
|
- submit -> provider error;
|
||
|
|
- status -> ack reordered delivery;
|
||
|
|
- fill -> ack reordered delivery;
|
||
|
|
- duplicate ack;
|
||
|
|
- duplicate `fill_id` and distinct equal-valued fills;
|
||
|
|
- duplicate and conflicting terminal statuses;
|
||
|
|
- late fill after close;
|
||
|
|
- update before and after ack;
|
||
|
|
- cancel and update for unknown dialog;
|
||
|
|
- existing external open order;
|
||
|
|
- position update with and without an active order.
|
||
|
|
|
||
|
|
### Backend conformance
|
||
|
|
|
||
|
|
- paper, Binance, Kraken, and IB request acceptance;
|
||
|
|
- account rejection;
|
||
|
|
- provider status normalization;
|
||
|
|
- provider exception to `BrokerdError` conversion;
|
||
|
|
- open-order conversion;
|
||
|
|
- zero-order and populated open-order snapshot completion;
|
||
|
|
- startup account/position conversion;
|
||
|
|
- Binance negative-position startup and IB multi-account association;
|
||
|
|
- provider metadata isolation;
|
||
|
|
- backend handler cancellation and stream closure.
|
||
|
|
|
||
|
|
### Actor semantics
|
||
|
|
|
||
|
|
- valid typed startup and streams;
|
||
|
|
- local `Started` and peer receive-side `MsgTypeError` provenance;
|
||
|
|
- malformed startup cancels context cleanly;
|
||
|
|
- malformed stream traffic cancels both ends cleanly;
|
||
|
|
- caller and callee cancellation;
|
||
|
|
- no swallowed teardown errors;
|
||
|
|
- no actor/task/channel leaks;
|
||
|
|
- repeated execution to expose scheduling-sensitive failures.
|
||
|
|
|
||
|
|
### Routing and isolation
|
||
|
|
|
||
|
|
- event reaches the originating dialog subscribers;
|
||
|
|
- unrelated FQME subscriber does not receive it;
|
||
|
|
- current all-client position broadcast is explicitly characterized as a
|
||
|
|
known limitation pending an account authorization/subscription schema;
|
||
|
|
- disconnected clients are removed from subscriber/dialog tables;
|
||
|
|
- reconnect receives only the documented startup snapshot and active dialogs.
|
||
|
|
|
||
|
|
## CI and Qualification Gates
|
||
|
|
|
||
|
|
Run gates in increasing cost order:
|
||
|
|
|
||
|
|
1. schema and reducer unit tests;
|
||
|
|
2. backend conformance tests with fake provider clients;
|
||
|
|
3. real Tractor IPC tests;
|
||
|
|
4. deterministic paper EMS integration;
|
||
|
|
5. the broader clearing/service regression set;
|
||
|
|
6. repeated race tests under each supported spawn method;
|
||
|
|
7. opt-in live read-only startup for Binance, Kraken, and IB;
|
||
|
|
8. manually authorized live order qualification with minimal size.
|
||
|
|
|
||
|
|
No live credential or public-network result can waive an offline failure.
|
||
|
|
Never place an order as part of default CI.
|
||
|
|
|
||
|
|
## Commit Boundaries
|
||
|
|
|
||
|
|
Keep the implementation reviewable and bisectable:
|
||
|
|
|
||
|
|
1. add the refreshed dependency baseline, strict schemas, pure reducer,
|
||
|
|
lifecycle guards, and offline/actor protocol qualification;
|
||
|
|
2. characterize and port paper together with its EMS consumer;
|
||
|
|
3. port each live adapter and its paired EMS handling with provider
|
||
|
|
translation fixtures and execution-identity tests;
|
||
|
|
4. migrate client/EMS and UI consumers together;
|
||
|
|
5. enforce each migrated endpoint pair with directional limits and
|
||
|
|
lifecycle/race/failure qualification;
|
||
|
|
6. remove legacy paths and document backend requirements.
|
||
|
|
|
||
|
|
Every commit must leave deterministic tests green. The first boundary
|
||
|
|
keeps legacy endpoints operational while independently qualifying the
|
||
|
|
new protocol. Subsequent migrations move both ends together; no commit
|
||
|
|
requires a raw-dictionary bridge into the strict schema.
|
||
|
|
|
||
|
|
## Rollback Strategy
|
||
|
|
|
||
|
|
Revert a backend port with its paired EMS changes. Once enforcement
|
||
|
|
begins, include the matching decorator and receive limits in that
|
||
|
|
rollback; never leave one actor side strict while restoring a
|
||
|
|
dictionary sender.
|
||
|
|
|
||
|
|
No long-lived persisted wire data requires a schema migration. Accounting and
|
||
|
|
ledger files remain outside this protocol change. If a strictness regression
|
||
|
|
is discovered, repair the producer or roll back the complete endpoint pair.
|
||
|
|
Do not widen the schema to accept arbitrary dictionaries or unknown fields.
|
||
|
|
|
||
|
|
## Exit Criteria
|
||
|
|
|
||
|
|
The formal EMS messaging upgrade is complete when:
|
||
|
|
|
||
|
|
- all client/EMS and EMS/backend IPC payloads are tagged structs;
|
||
|
|
- paper, Binance, Kraken, and IB share one broker startup/request/event
|
||
|
|
contract;
|
||
|
|
- both Tractor context families declare broad `pld_spec` aliases and narrow
|
||
|
|
each receive direction;
|
||
|
|
- startup, stream, and return payload failures produce tested
|
||
|
|
`MsgTypeError` behavior;
|
||
|
|
- no broker status outside `pending/open/canceled/closed` crosses IPC;
|
||
|
|
- fills and existing orders use dedicated variants;
|
||
|
|
- no business logic reconstructs wire dictionaries into message structs;
|
||
|
|
- ack/status/cancel races are deterministic and bounded;
|
||
|
|
- multi-client order routing is tested and the unresolved position-broadcast
|
||
|
|
privacy limitation is explicit;
|
||
|
|
- default EMS tests run without network or credentials;
|
||
|
|
- actor teardown failures are asserted rather than tolerated;
|
||
|
|
- backend authors have one conformance suite and protocol definition to follow.
|
||
|
|
|
||
|
|
## Primary References
|
||
|
|
|
||
|
|
- `piker/clearing/_messages.py`: current EMS and broker message structs.
|
||
|
|
- `piker/clearing/_ems.py`: broker dialog opening, translation, client command
|
||
|
|
processing, and `_emsd_main()`.
|
||
|
|
- `piker/clearing/_client.py`: public EMS client and startup handling.
|
||
|
|
- `piker/clearing/_paper_engine.py`: paper reference backend.
|
||
|
|
- `piker/brokers/binance/broker.py`: Binance request and event adapter.
|
||
|
|
- `piker/brokers/kraken/broker.py`: Kraken request and event adapter.
|
||
|
|
- `piker/brokers/ib/broker.py`: IB request and event adapter.
|
||
|
|
- `piker/data/validate.py`: existing datad-side `FeedInit` backend validation
|
||
|
|
direction to improve upon rather than duplicate.
|
||
|
|
- `piker/ui/_remote_ctl.py`: in-tree `pld_spec` and per-direction
|
||
|
|
`limit_plds()` precedent.
|
||
|
|
- `piker/ui/_gaps.py`: in-tree tagged union precedent.
|
||
|
|
- `tests/test_ems.py`: current network-dependent clearing coverage and empty
|
||
|
|
order tests.
|
||
|
|
- `/home/goodboy/repos/tractor/tests/msg/test_pldrx_limiting.py`: canonical
|
||
|
|
payload limiting and `MsgTypeError` behavior.
|
||
|
|
- `/home/goodboy/repos/tractor/tests/msg/test_ext_types_msgspec.py`: canonical
|
||
|
|
msgspec codec behavior.
|
||
|
|
- `/home/goodboy/repos/tractor/tests/test_context_stream_semantics.py`:
|
||
|
|
context stream cancellation and serialization semantics.
|
||
|
|
|
||
|
|
|
||
|
|
Relevant issue/design threads:
|
||
|
|
|
||
|
|
- https://github.com/goodboy/tractor/issues/196
|
||
|
|
- https://github.com/goodboy/tractor/issues/365
|
||
|
|
- https://github.com/goodboy/tractor/issues/514
|
||
|
|
- https://github.com/goodboy/tractor/pull/376
|
||
|
|
- https://github.com/pikers/piker/issues/514
|
||
|
|
- https://github.com/pikers/piker/issues/297
|
||
|
|
- https://github.com/pikers/piker/issues/466
|