fix-lab

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT

README

fix-lab

A FIX 4.4 session and order flow between two real engines, and a matching engine driven by those messages: logon, heartbeats, TestRequest, sequence numbers, gap recovery with ResendRequest and SequenceReset, logout; NewOrderSingle, OrderCancelRequest, ExecutionReport, OrderCancelReject; a limit order book with price-time priority and partial fills.

Both ends are QuickFIX/Go v0.9.12, used as published. This repository adds the matching engine, a tap that can lose messages on purpose, and the tools around them.

This is what runs the live page at https://pigfox.com/demos/fix-lab. That site imports this module's lab, wire, client, exchange and results packages rather than keeping a copy, and its benchmark table is read from results/summary.json.

The instruments are fictional. ACORN.LAB and BIRCH.LAB have made-up reference prices; nothing here reads, mirrors or resembles a market, and no order goes anywhere but this process's own book.

Run it

You need Docker with Compose. No Go toolchain, no account, no key, no .env.

git clone https://github.com/pigfox/fix-lab
cd fix-lab
docker compose up -d exchange          # the acceptor + matching engine on 127.0.0.1:9878, two bots
docker compose run --rm fixcli         # an initiator in a second container; type commands

fixcli logs on and prints every message on the wire in both directions, with each tag named from the FIX 4.4 dictionary. Commands:

buy ACORN.LAB 99.90 10       a limit NewOrderSingle
sell BIRCH.LAB 25.05 5
cancel CLI1-1                an OrderCancelRequest for that ClOrdID
orders                       your orders as the exchange reported them
seq                          your next outbound and expected inbound MsgSeqNum
drop                         withhold your next outbound message (a sequence gap)
silent / talk                withhold every outbound message / stop withholding
quit                         Logout and exit

Use a different SenderCompID per client: docker compose run --rm fixcli -comp CLI2. Two clients can trade with each other: one rests a buy inside the bots' spread, the other sells at that price.

docker compose logs -f exchange shows the top of each book every 30 seconds. docker compose down stops everything; nothing is kept.

Without Docker (Go 1.27): go run ./cmd/exchange and go run ./cmd/fixcli.

Watch a recovery

A lost message. Type drop, then send an order. The tap withholds it; the order's MsgSeqNum is spent on the client, but the exchange never sees it. The next message from the client (another order, or the next Heartbeat) arrives one number too high, and the exchange's engine answers with ResendRequest (2) for the missing number. The client's engine resends the order from its message store with PossDupFlag (43) = Y, and the exchange processes it. Had the lost message been a Heartbeat, the client would answer with SequenceReset (4) GapFillFlag (123) = Y instead, since admin messages are not resent.

A silent client. Type silent. After 1.2 heartbeat intervals with nothing from the client, the exchange sends TestRequest (1). The client's engine answers with a Heartbeat carrying the TestReqID, but the tap withholds it too. After a further 1.2 intervals the exchange's engine ends the session. QuickFIX/Go disconnects at that point without sending a Logout, and the exchange cancels whatever the session had resting.

Every one of those steps is the engines'. The tap only forwards whole messages or withholds them; it never writes, edits or answers anything.

What is in it

Package What it does
book One instrument's limit order book: price-time priority, each trade at the resting order's price, partial fills, cancels. Integer ticks, no floats.
exchange The acceptor's application. Takes D and F, answers 8 and 9. Each fill is reported to both sides, each on its own session, with one match ID in SecondaryExecID (527). Orders must be limit, within 20% of the reference price, 1 to 1,000 units. Resting orders are canceled when their session ends.
client One initiator session, with its own view of its orders built only from the ExecutionReports it receives.
wire Reads FIX messages off a stream, names every tag and enumerated value from QuickFIX/Go's FIX 4.4 dictionary, and the tap.
store QuickFIX/Go's in-memory message store with a lock, so sequence numbers can be read while a session runs.
lab Everything in one process: the exchange on a loopback port, bots that are ordinary FIX sessions, and one tapped session per visitor.
spec QuickFIX/Go's spec/FIX44.xml, byte for byte (digest pinned), under its own license in spec/LICENSE-QUICKFIX.
results The committed benchmark, embedded.

Two bots together keep four levels a side on each book, half a percent of the reference price apart, starting half a percent from it, and each puts an order back at the same price when it fills. Anything priced inside that first half percent rests as the best bid or ask.

Tests

go test -race ./...

client/recovery_test.go runs two real engines over real TCP with the tap between them: a dropped order recovered by ResendRequest and a PossDup resend, with both stores agreeing afterwards; a dropped Heartbeat answered with a gap fill; silence answered with a TestRequest and then the end of the session; ResetSeqNumFlag on logon; two sessions crossing and both receiving the trade; cancel and cancel-reject; rejected orders; and cancel-on-logout. book covers price-time priority, partial fills, sweeps and cancels.

Benchmark

./scripts/bench.sh

Three runs, each against a freshly started exchange container, with the bench in a second container over TCP. Orders alternate buy and sell, one unit each, at one price, so they trade with each other and the book stays small. Ack latency is from just before the client hands a NewOrderSingle to its engine to the moment its engine hands it the ExecutionReport New: one clock, a round trip. Percentiles are exact, by nearest rank, over every measured order.

The committed runs are in results/SUMMARY.md, built by go run ./cmd/summarize from the raw files in results/raw, with the machine and date. CI rebuilds the summary and fails if it differs.

Throughput falls past four sessions on this machine, and the reason is in the engine. In QuickFIX/Go v0.9.12, when a session's socket writer is busy, sendQueued(false) cannot hand off a queued message and notifyMessageOut() re-signals the same session loop at once, so the loop polls until the writer is free. A CPU profile of eight sessions put 64% of CPU in runtime.selectgo under stateMachine.SendAppMessages. With more polling loops than CPUs, they take the time the writers need. The figures are the engine as published.

License

MIT, see LICENSE. spec/FIX44.xml is QuickFIX/Go's, under the QuickFIX Software License in spec/LICENSE-QUICKFIX.

Directories

Path Synopsis
Package book is a limit order book for one instrument: price-time priority, partial fills, and cancels.
Package book is a limit order book for one instrument: price-time priority, partial fills, and cancels.
Package client is the initiator side: one QuickFIX/Go initiator session that logs on to the exchange, sends NewOrderSingle and OrderCancelRequest, and keeps its own view of its orders from the ExecutionReports it receives.
Package client is the initiator side: one QuickFIX/Go initiator session that logs on to the exchange, sends NewOrderSingle and OrderCancelRequest, and keeps its own view of its orders from the ExecutionReports it receives.
cmd
bench command
Command bench measures order throughput and acknowledgement latency against a running exchange over real TCP:
Command bench measures order throughput and acknowledgement latency against a running exchange over real TCP:
exchange command
Command exchange runs the acceptor: the matching engine behind a FIX 4.4 acceptor on a TCP port, with liquidity bots so the books are never empty.
Command exchange runs the acceptor: the matching engine behind a FIX 4.4 acceptor on a TCP port, with liquidity bots so the books are never empty.
fixcli command
Command fixcli is an initiator you drive from the keyboard.
Command fixcli is an initiator you drive from the keyboard.
summarize command
Command summarize builds results/summary.json and results/SUMMARY.md from the raw run files in results/raw, and from nothing else:
Command summarize builds results/summary.json and results/SUMMARY.md from the raw run files in results/raw, and from nothing else:
Package exchange is the acceptor side: a QuickFIX/Go acceptor whose application is a matching engine with one order book per instrument.
Package exchange is the acceptor side: a QuickFIX/Go acceptor whose application is a matching engine with one order book per instrument.
Package fixmsg holds the FIX 4.4 tags and message types this lab uses, the price format, and small helpers for building and reading messages with QuickFIX/Go.
Package fixmsg holds the FIX 4.4 tags and message types this lab uses, the price format, and small helpers for building and reading messages with QuickFIX/Go.
Package lab runs the whole thing inside one process: the exchange on a loopback port, a few liquidity bots that are ordinary FIX sessions, and one initiator session per visitor, each behind its own tap.
Package lab runs the whole thing inside one process: the exchange on a loopback port, a few liquidity bots that are ordinary FIX sessions, and one initiator session per visitor, each behind its own tap.
Package results carries the committed benchmark measurements, so anything that shows them — pigfox.com's /demos/fix-lab among them — reads this exact file rather than a retyped copy of its numbers.
Package results carries the committed benchmark measurements, so anything that shows them — pigfox.com's /demos/fix-lab among them — reads this exact file rather than a retyped copy of its numbers.
Package spec carries the FIX 4.4 data dictionary that names every tag this lab shows.
Package spec carries the FIX 4.4 data dictionary that names every tag this lab shows.
Package store is QuickFIX/Go's in-memory message store with a lock around it, so the sequence numbers can be read while the session runs.
Package store is QuickFIX/Go's in-memory message store with a lock around it, so the sequence numbers can be read while the session runs.
Package wire reads FIX messages off a byte stream, names every tag in them, and sits between two FIX engines as a tap that can lose messages on purpose.
Package wire reads FIX messages off a byte stream, names every tag in them, and sits between two FIX engines as a tap that can lose messages on purpose.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL