Skip to content

feat(fix): FIX protocol bridge behind the fix feature - #220

Closed
changshenhan wants to merge 1 commit into
joaquinbejar:mainfrom
changshenhan:feat/fix-bridge
Closed

feat(fix): FIX protocol bridge behind the fix feature#220
changshenhan wants to merge 1 commit into
joaquinbejar:mainfrom
changshenhan:feat/fix-bridge

Conversation

@changshenhan

Copy link
Copy Markdown

Summary

Adds a FIX protocol bridge to OrderBook-rs, connecting a wire-level FIX codec to the matching engine. Protocol-only by design — no session state, sequence numbers, or transport lives here; the caller owns those (e.g. a future fix-session layer).

New optional fix feature (default off, fully cfg-gated — zero dependency or behavior change for existing users):

  • NewOrderSingle (D)OrdType 2add_limit_order, OrdType 1submit_market_order. Converts Side (54), OrderQty (38), TimeInForce (59), Price (44) into pricelevel types; FIX decimal prices scale to integer ticks via the book's tick size.
  • OrderCancelRequest (F) — cancel by OrigClOrdID (tag 41).
  • OrderCancelReplaceRequest (G) — cancel-and-replace the original.
  • Deterministic ClOrdID mapping — tag 11 hashed with FNV-1a to a pricelevel::Id, so the same client order id always targets the same book order across place/cancel/replace.

Entry point

use orderbook_rs::fix::apply_fix_message;

let outcome = apply_fix_message(&book, &msg, /* tick_size */ 10)?;

apply_fix_message dispatches on MsgType (35) and returns a typed FixOutcome (Placed / Cancelled / Replaced), with FixBridgeError covering conversion, unsupported message types, and order-book rejections.

Testing

  • 13 new tests across fix::convert (side / TIF / price-to-ticks / deterministic id) and fix::engine (limit, market, cancel, cancel-and-replace incl. state assertions, missing-field, unsupported-type, missing-order paths).
  • cargo test --features fix690 + 26 + 470 + 49 all green; default build and --features fix build both clean.
  • cargo clippy --features fix --all-targets — zero warnings.

Docs

  • src/fix/mod.rs module docs with the supported message set and mapping semantics.
  • CHANGELOG.md entry under [Unreleased].
  • README.md "What's New in Version 0.13.0" section with usage snippet (version title is yours to adjust at release time).

Depends only on fix-codec v0.1 (small, std-only, MIT). No unsafe, no API breaks, default feature set unchanged.

Maps fix-codec-decoded FIX messages onto the matching engine via a new
optional `fix` feature (default off):

- NewOrderSingle (D): OrdType 2 -> add_limit_order, OrdType 1 ->
  submit_market_order; Side/TimeInForce/Qty/Price tags converted to
  pricelevel types (decimal prices scaled to integer ticks by tick_size).
- OrderCancelRequest (F): cancel by OrigClOrdID (tag 41).
- OrderCancelReplaceRequest (G): cancel-and-replace the original.
- ClOrdID (tag 11) maps deterministically to an order id via FNV-1a, so
  the same client id always targets the same book order across
  place/cancel/replace.
- Protocol-only: no session state, sequence numbers, or transport; the
  caller supplies those (e.g. a fix-session layer).

Fully cfg-gated: the module adds no dependency or behavior when `fix` is
off. Clippy-clean; all tests pass (690 + feature suite). CHANGELOG under
[Unreleased] and README v0.13.0 section updated.

Co-Authored-By: Claude <noreply@anthropic.com>
@joaquinbejar

Copy link
Copy Markdown
Owner

Thanks for the work here.

Before the code itself, a scoping decision that affects the whole PR: orderbook-rs keeps protocol codecs out of the matching engine, so a FIX bridge that pulls a codec dependency into this crate is not a shape we can take. There is also already a FIX implementation maintained under this project: IronFix, published on crates.io at 0.4.0 (ironfix-core, ironfix-tagvalue, ironfix-session, ironfix-engine), plus IronSBE for SBE.

If you want to keep going, three options, in the order I would pick them:

1. A separate bridge crate (preferred). A new repo depending on orderbook-rs and ironfix-engine, implementing IronFix's Application trait: from_app translates NewOrderSingle / OrderCancelRequest / OrderCancelReplaceRequest into SequencerCommand, and to_app emits ExecutionReports from SequencerResult. orderbook-rs gains no dependency, and SessionId gives you a per-session ClOrdID mapping, which removes the cross-client cancel exposure that a global ClOrdID hash carries.

2. Contribute the bridge to IronFix instead. Same thing, living on the protocol side, where the FIX session layer already is.

3. Contribute the protocol-agnostic seam here. No FIX in it: an inbound-decoder / outbound-encoder trait pair over SequencerCommand / SequencerResult, plus the two gaps any adapter needs, namely user attribution on the CancelOrder and UpdateOrder commands so ownership can be checked on cancel, and converging src/wire/ onto SequencerCommand rather than its current parallel message set. That is the piece that makes FIX, SBE, REST and WebSocket adapters possible without touching the engine again.

Happy to open the issues for option 3 if that is the one you want to take.

@changshenhan

Copy link
Copy Markdown
Author

Went with option 1 — the bridge is now a standalone crate: https://github.com/directwire/ironfix-orderbook-bridge

Shape follows your sketch:

  • BridgeApplication implements IronFix 0.4's Application trait; from_app translates NewOrderSingle / OrderCancelRequest / OrderCancelReplaceRequest into book operations, and ExecutionReports flow back per session via the router.
  • SessionId keys a per-session ClOrdID ↔ book order id registry, so a cancel can only resolve a ClOrdID registered by the same session — the cross-client cancel exposure is closed with an application-level reject (103=1), and there's a test proving Bob can't cancel Alice's order.
  • Every fill (maker + taker, both the add_order matching path and the market sweep) flows through the book's trade listener, so fills have a single reporting path keyed by book order id.
  • Zero floating point: Price (44) / OrderQty (38) parse into the book's native fixed-point integers.
  • Cancel-replace is cancel + new with the old ClOrdID consumed; a cancel of a fully filled order is rejected (103=99) since the book itself acks that as Ok — the bridge tracks CumQty to add the too-late semantics.
  • Since orderbook-rs has no live SequencerCommand executor, the crate exports execute() — the live counterpart of the ReplayEngine replay dispatcher — so a journalled command sequence replays deterministically. Session→Hash32 user attribution is FNV-1a, stable across runs.
  • The engine keeps zero FIX knowledge: it depends only on orderbook-rs types, so it stays reusable if a codec-free seam ever lands here (your option 3).

One note for the tracker: cancel_order on a fully filled order currently returns Ok on the book side. That's the gap the bridge papers over with its CumQty check; if the book ever rejects terminal-order cancels natively, the bridge check becomes redundant.

9 integration tests (session isolation, cross via listener, too-late cancel, replace identity consumption, tag-level error mapping, attribution stability), clippy + rustfmt clean. Happy to take feedback here or on the repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants