-
Notifications
You must be signed in to change notification settings - Fork 0
feat: FIX-like text protocol adapter (#29) #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
587778d
0c3b401
beec2d0
59f0fc3
872600a
0201d54
3e4c8e3
52de5b8
d4be2da
9c68039
4aec1d0
3905059
68fe197
cb8f99c
dfa4da2
6ef5015
31070b1
06b7675
ee5ea4e
f213ee7
2abb9ca
4a2aa67
2199820
6e8a302
5093beb
1599e5d
4fc2f35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,15 +52,16 @@ Do not rely on prior chat memory. | |
| - **Next action:** no active milestone. Highest-value remaining work is non-code and gated: | ||
| issue #94 (independent external review — needs a human reviewer) and issue #90 (full | ||
| cache-counter PMU evidence — needs a PMU microarchitecture that exposes cache events, e.g. | ||
| x86_64). Low-signal backlog: #32 (flamegraph), #29 (FIX adapter). | ||
| x86_64). #29 (FIX-like text protocol adapter) is delivered in this PR | ||
| (`feat/fix-text-protocol-adapter`). Low-signal backlog: #32 (flamegraph). | ||
| - **Blockers:** issue #90 is now a *cache-counter* PMU gap, not a host-access gap — this bare-metal | ||
| Apple M2 exposes real `cycles`/`instructions`/`branches`/`branch-misses` but its PMU does not | ||
| implement `cache-references`/`cache-misses`; closing it needs a PMU microarchitecture that exposes | ||
| cache events (x86_64, or an ARM server core). Issue #94 remains open for independent external | ||
| review (human-gated). Hardware NIC/offload latency measurement still requires suitable wired NIC | ||
| hardware, driver support, timestamping/offload/RSS access, and a measured packet workload; the | ||
| current `wld0` Wi-Fi capability observation is not NIC-offload latency evidence. Legacy backlog | ||
| still includes #32 and #29. Issues #95, #28, and #26 were closed by PR #112. | ||
| still includes #32 (#29 delivered in this PR). Issues #95, #28, and #26 were closed by PR #112. | ||
|
|
||
| --- | ||
|
|
||
|
|
@@ -386,6 +387,24 @@ Lower priority: | |
| the bare-metal Fedora Asahi host (aarch64) from the clean committed tree (`Dirty inputs: no`). | ||
| This is a software cpu-clock sampling hot-symbol profile, not a latency/throughput claim; full | ||
| hardware cache-PMU evidence stays in #90. Do not merge from automation; human squash-merges. | ||
| - [2026-06-21] Issue #29 FIX-like text protocol adapter (`feat/fix-text-protocol-adapter`, stacked | ||
| on the flamegraph branch). Added `include/qsl/protocol/fix.hpp` + `src/protocol/fix.cpp`: a | ||
| `tag=value` SOH-framed adapter over the SAME internal structs as the binary codec, with genuine | ||
| FIX framing (8 BeginString / 9 BodyLength / 35 MsgType / … / 10 mod-256 CheckSum) for the | ||
| client→gateway order path — NewOrderSingle (35=D)→`NewOrder` and OrderCancelRequest | ||
| (35=F)→`CancelOrder`. Decoding is total/deterministic/`noexcept` (fixed field table, | ||
| `std::from_chars`, `string_view`; no heap on decode) and reports every malformed input through a | ||
| `FixError` taxonomy mirroring the binary `DecodeError`. Documented, deliberate simplifications: | ||
| Symbol (55) carries the numeric SymbolId; Price (44) carries integer ticks and is always present, | ||
| making `NewOrder↔FIX` a lossless bijection like the binary codec (never float for price). | ||
| `tests/unit/test_fix_protocol.cpp` mirrors the binary required tests and adds a **cross-codec | ||
| equivalence** test (binary and FIX decode the same order to identical structs across all | ||
| Side×OrdType×TIF), a byte-pinned fixture (checksum 164 / body-length 50), and rejection of | ||
| malformed framing / unsupported BeginString / unknown-or-wrong MsgType / BodyLength mismatch / | ||
| CheckSum mismatch / missing field / invalid field / invalid enum / out-of-range / oversized. Docs | ||
| in `docs/fix_protocol.md` (+ pointer from `docs/binary_protocol.md`). `make check` 260/260 and | ||
| `make asan` 260/260 clean (the parser handles untrusted text). Closes #29. Do not merge from | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This entry marks #29 as closed, but the resume/current-state anchors still list #29 as open backlog in Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+405
to
+406
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new history entry records the FIX adapter as the latest work with Useful? React with 👍 / 👎. |
||
| automation; human squash-merges. | ||
| - [2026-06-03] M35: implemented a multi-client TCP connection-scaling load test (`scripts/socket_load.sh`, `make socket-load`, Linux-only) driving N concurrent `qsl-client`s against the portable TCP and epoll (M34) gateways; `results/socket_load_summary.txt` is Docker-generated and constrained. A `/code-review` (3 finder agents) caught and fixed real measurement-integrity bugs before the PR: a failed trial's `wall=0` no longer poisons the reported best (only trials whose gateway served count toward the min); the `completed` column reports the WORST per-trial completion, not the last, so partial/total trial failures are surfaced rather than masked; a per-client `timeout` bounds a hang if the gateway dies; and `QSL_LOAD_TRIALS` is validated. Post-PR hardening uses fresh monotonic ports per gateway start, retries transient startup/serve failures on new ports, and refuses to write a partial artifact unless `QSL_LOAD_ALLOW_PARTIAL=1` is set intentionally; the refreshed artifact records `Dirty tree: no`. The scaling-shape claim remains constrained to loopback connection setup, not a demonstrated production-capacity advantage for either transport. Deferred follow-up: a shared `scripts/lib` to remove the dirty-tree / `wait_ready` / gateway-stop duplication across the three socket scripts. | ||
| - [2026-06-03] M35: started after M34 (#98) squash-merged (commit 9e3750b). Scope: multi-client load / socket-pressure testing of the gateway/feed path (TCP/UDP stress, socket-buffer pressure, connection scaling, backpressure) building on M34's epoll multi-client path and M30's socket tooling. Constraints: scripts/tests document load shape + environment; results must distinguish kernel/socket pressure from user-space engine cost; no production-capacity claims (honest constrained-environment framing, like M29/M30). | ||
| - [2026-06-04] M35: PR #100 squash-merged to `main` as a86b701 after all CI jobs and review checks were green. M35 is now landed; original M36 NUMA remains deferred until the repository-health refactor analysis is completed or explicitly skipped by the human. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| # FIX-like Text Protocol Adapter | ||
|
|
||
| A human-readable `tag=value` text protocol alongside the [binary protocol](binary_protocol.md), | ||
| mapping the **same internal message structs**. Implemented in `include/qsl/protocol/fix.hpp` and | ||
| `src/protocol/fix.cpp`; tested in `tests/unit/test_fix_protocol.cpp`. This is the optional FIX | ||
| adapter tracked by issue #29. | ||
|
|
||
| It is **FIX-like**, not a full FIX engine: it implements the genuine FIX framing and the | ||
| client→gateway order path, deliberately scoped to what mirrors the binary codec. | ||
|
|
||
| ## Why it exists | ||
|
|
||
| Real venues speak FIX as well as binary protocols. Showing a second, independently-validated wire | ||
| format over one internal model demonstrates a clean protocol boundary: the engine does not care | ||
| which encoding produced a `NewOrder`. The strongest invariant the tests assert is that a binary | ||
| frame and a FIX message for the same order **decode to identical internal structs**. | ||
|
|
||
| ## Framing | ||
|
|
||
| A message is a sequence of `tag=value` fields, each terminated by the **SOH** byte (`0x01`). The | ||
| adapter uses the standard FIX envelope: | ||
|
|
||
| ```text | ||
| 8=FIX.4.2 | 9=<BodyLength> | 35=<MsgType> | <business fields...> | 10=<CheckSum> | | ||
| ``` | ||
|
|
||
| (`|` denotes SOH.) | ||
|
|
||
| - **`8` BeginString** must be `FIX.4.2`; anything else is `UnsupportedBeginString`. | ||
| - **`9` BodyLength** is the byte count from the field after tag 9 through the SOH before tag 10. | ||
| A mismatch is `BodyLengthMismatch`. | ||
| - **`10` CheckSum** is the mod-256 sum of every byte up to the SOH before tag 10, formatted as | ||
| exactly three zero-padded digits. A mismatch is `ChecksumMismatch`. | ||
|
|
||
| ## Messages | ||
|
|
||
| ### NewOrderSingle (`35=D`) → `NewOrder` | ||
|
|
||
| | Tag | FIX name | Internal field | Encoding | | ||
| |-----|---------------|----------------|----------| | ||
| | 34 | MsgSeqNum | sequence no. | carried like the binary header `seq_no` (validated, not stored in the body struct) | | ||
| | 11 | ClOrdID | `order_id` | decimal | | ||
| | 55 | Symbol | `symbol` | decimal `SymbolId` (see simplifications) | | ||
| | 54 | Side | `side` | `1`=Buy, `2`=Sell | | ||
| | 38 | OrderQty | `quantity` | decimal | | ||
| | 40 | OrdType | `type` | `1`=Market, `2`=Limit | | ||
| | 44 | Price | `price` | integer ticks (see simplifications) | | ||
| | 59 | TimeInForce | `tif` | `1`=GTC, `3`=IOC | | ||
|
|
||
| ### OrderCancelRequest (`35=F`) → `CancelOrder` | ||
|
|
||
| | Tag | FIX name | Internal field | Notes | | ||
| |-----|--------------|----------------|-------| | ||
| | 34 | MsgSeqNum | sequence no. | as above | | ||
| | 41 | OrigClOrdID | `order_id` | the order being cancelled | | ||
| | 11 | ClOrdID | — | required by FIX; validated on decode, echoes `order_id` on encode (no separate cancel-request id is modelled) | | ||
| | 55 | Symbol | `symbol` | decimal `SymbolId` | | ||
|
|
||
| ## Deliberate simplifications | ||
|
|
||
| These are documented departures from strict FIX, chosen so the adapter stays a deterministic, | ||
| lossless map onto the simulator's internal model: | ||
|
|
||
| - **Symbol (tag 55) carries the numeric `SymbolId`** in decimal, not a ticker string — the engine | ||
| keys on `SymbolId`, so mapping to a string table would only add a lossy layer. | ||
| - **Price (tag 44) carries integer ticks and is always present**, including market orders. The | ||
| project never represents price as a float, and `NewOrder` always has a `price` field; carrying it | ||
| losslessly makes `NewOrder ↔ FIX` a true bijection over the internal struct, exactly like the | ||
| binary codec. (Strict FIX uses a decimal price and forbids tag 44 on market orders.) | ||
|
|
||
| ## Error model | ||
|
|
||
| Decoding is total and deterministic: it never throws, allocates nothing on the decode path (a | ||
| fixed field table, `std::from_chars`, `std::string_view`), and reports every malformed input via | ||
| `FixError` rather than undefined behavior — mirroring the binary codec's `DecodeError` discipline. | ||
|
|
||
| `FixError`: `None`, `Malformed`, `UnsupportedBeginString`, `UnknownMsgType`, `MissingField`, | ||
| `InvalidField`, `BodyLengthMismatch`, `ChecksumMismatch`, `InvalidEnumValue`, `OutOfRange`. | ||
|
|
||
| ## Determinism and testing | ||
|
|
||
| `tests/unit/test_fix_protocol.cpp` mirrors the binary codec's required tests and adds FIX-specific | ||
| ones: | ||
|
|
||
| - round-trip for NewOrderSingle and OrderCancelRequest; | ||
| - **cross-codec equivalence**: binary and FIX decode the same order to identical structs across all | ||
| Side × OrdType × TIF combinations; | ||
| - a **byte-pinned fixture** (`8=FIX.4.2|9=50|35=D|…|10=164|`) so any change to field order or the | ||
| checksum/body-length computation fails the build; | ||
| - rejection of malformed framing, unsupported BeginString, unknown/wrong MsgType, BodyLength | ||
| mismatch, CheckSum mismatch, missing required fields, non-numeric fields, invalid enum codes, | ||
| out-of-range integers, and oversized messages; | ||
| - signed/extreme `int64` price and `uint64` id/seq round-trips. | ||
|
|
||
| The adapter is also covered by the ASan/UBSan preset (`make asan`), since it parses untrusted text. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When resuming from this commit, this current-state anchor still tells agents that #32 (flamegraph) is remaining even though this stack is based on the flamegraph commit and the same file already records the flamegraph artifact as completed later in the history section. Because
/resumeis driven from these anchors, the next session can be sent back to a delivered issue; remove #32 from the backlog here or mark it as delivered in the current-state text.Useful? React with 👍 / 👎.