Releases: intrepidkarthi/orderbook
Release list
v0.26.0 — a second implementation, and a log that can forget
Forty-nine commits since v0.25.0, and the two arcs that dominate them are a second
implementation of the matching rules that disagrees with the engine on purpose, and a
write-ahead log that can finally forget. Between them they found six correctness
defects — three of which no test, fuzzer or race detector in this repository had
caught — and turned restart cost from a property of how long the venue has been up
into a byte budget an operator sets.
The measurement that corrects a published claim is in here too: distinct symbols do
not scale linearly across cores. They scale 2.24×.
A reference matcher, and the three defects it found
internal/refmatch is a limit order book written to be read — two sorted slices
scanned linearly, cancel by linear search, depth by a fold. No index, no pool, no
id-to-node map. It is slow on purpose and must never be optimised, because a model
with an index has the engine's bug class. It imports the standard library and nothing
else, and a test parses the files to keep it that way. pkg/types is the tempting
exception and it is refused: an invariant both sides inherit from the same seven lines
is an invariant nothing is checking.
Both sides produce one comparable Observation per command, compared whole with
reflect.DeepEqual rather than field by field — because a field-list comparator is
where a future field silently stops being checked. Twenty-one deliberate engine
mutations are all caught, and every shrunk reproduction is 1 to 4 commands. One seed
catches 8 of 18; the sweep catches 18 of 18, which is the argument for the sweep's
size.
It found three real defects. Each had more than one defensible answer, so each was
pinned as a failing test first and decided in
DIFFERENTIAL-FINDINGS.md before being repaired:
- A rejected fill-or-kill moved
LastTradePrice.matchrecorded the price from
the final print, the fill-or-kill branch reversed every one of those prints, and
nothing put the price back. The rule is now stated inrecordLast's doc comment:
LastTradePriceis the price of the last trade this venue published. - Under
ProRata, a taker meeting its own resting liquidity was skipped entirely.
matchProRatanever calledtakerSTP, so all five self-trade-prevention modes left
the taker's remainder resting across the spread — bid 100 / ask 99 on a
continuous book. A venue configuredALLOWdid not get the self-trade it asked for
and one configuredCANCEL_BOTHcancelled neither order: pro-rata was silently
overriding the venue's STP configuration, and declining unrelated accounts'
liquidity on the way. - A
REJECTEDcommand's event batch may now carry further events. A rejection
drops only the events describing state the engine actually undid, so aREJECTED
may be followed byCANCELED,REPLACED,ACCEPTEDorTRIGGERED. Consumers of
EventSinkmust apply them — see Breaking, below.
The harness's own guards were then sabotaged twelve ways, and the two that did not
behave as specified are written down rather than quietly fixed
(REFERENCE-MATCHER.md §10.3).
Three iceberg defects, one of which bricked checkpoints
- A refused iceberg made the venue's own snapshot unloadable.
ProcessIceberg
registered the iceberg before settling it and did not undo that when the settle
refused, soTakeSnapshotwrote an entry for an order that is not on the book and
LoadSnapshotrefuses such a snapshot outright. One refused iceberg and the venue
could never load a checkpoint again — not after further trading, not after a
restart. It also landed on this release's own upgrade path: the runbook tells an
operator to checkpoint after accepting a semantics mismatch, and measured end to
end, that produced a venue that would not restart. The repair is the invariant, not
the symptom — the registry holds icebergs whose displayed slice is resting. - A recovery from the journal alone lost every iceberg's hidden reserve. A nine-lot
iceberg shown three was journalled asquantity 3, display 3, and a replay rebuilt
it withhidden = 0. A venue recovering from its log alone came back with every
client's reserve gone — the path a venue takes when its snapshot is missing,
refused as corrupt, or below the retention floor. Recovery with a snapshot was
never affected. - A failing fill-or-kill corrupted an iceberg it consumed. Negative
FilledQty,
the entire hidden reserve displayed in the open, a shown size larger than the order's
own quantity, and its refill registration dropped. On the two-command reproduction, 9
total / 3 displayed becameFilledQty -6,RemainingQty 9, reserve 0, best ask
100:9. An iceberg exists to hide size; one rejected order from an unrelated account
leaked all of it, permanently.
Also fixed: admission measured an iceberg's displayed slice rather than its total,
so a client could switch off the fat-finger cap by choosing an order type — the audit
behind it found five admission checks measuring the wrong quantity
(ICEBERG-ADMISSION.md).
Restart cost becomes a number you choose
Two changes, in order. A restart no longer parses the part of the log its snapshot
already covers — every record is still read and CRC-verified, but only records past
the snapshot's sequence are decoded and retained. On a 500,000-record covered prefix
that is 1.66 s → 64 ms, and allocation goes flat in the prefix. The saving was
~26×, more than the design predicted, because the decode turned out to be ~97% of the
marginal cost of a covered record; the BENCHMARKS.md row that mispredicted it is
corrected.
Then the log rotates into segments, and a prefix of them can be deleted once a
verified snapshot covers it. Restart cost is now O(retained log), and the retained size
is a byte budget you set. The decision the slice turns on is that a segment declares
its base sequence in an 18-byte header rather than having one inferred from its
position — a renamed, copied or restored segment cannot quietly put records into the
wrong sequence space. Design, and every place the code disagreed with its own spec:
LOG-ROTATION.md §12.
That budget is not set by default, and a venue that leaves -wal-retain unset
still gets slower to restart every day it stays up. That is the honest state of it.
A journal that refuses rather than lying
Three of the changes above alter what matching does with the same input, so a log
recorded before them and recovered after them produces state that never existed on the
venue that wrote it. Until this release nothing on disk said so: recovery replayed the
log, started, and the book was wrong in a way nothing downstream flagged.
matching.SemanticsVersion stamps an equivalence class of builds — two builds
share a version if and only if, for every command sequence and configuration, they
produce the same trades, events, verdicts and book. It is 2 on this release.
A release version used as the stamp would refuse journals that replay identically on
every upgrade, and the response to a check that cries wolf is a permanent override.
The override names the versions it accepts rather than being a boolean, which is the
most important detail in it: -wal-semantics-mismatch-ok goes into a unit file during
one incident and stays for the life of the deployment.
-wal-accept-semantics 1 stops working the moment the number moves again.
What this deliberately does not cover is engine configuration: two builds at the
same semantics version with different ProRata, SelfTradePrevention, MaxOrders or
PriceBand replay the same log into different books and nothing notices. That gap is
arguably larger than the one this closes, and it is named in
SEMANTICS-VERSION.md §6 rather than implied to be handled.
The venue counts what it refuses
Sixteen metric families already said what the venue did; nothing said what it
dropped, and the durability path was untimed.
obgw_refused_total{reason} counts every refusal at the single funnel all fifty
refusal sites pass through, and increments before the encode, so it can never claim
a rejection the client did not get — measured equal end to end at 51,568 against 51,568
CmdRejects received. obgw_shed_unreported_total counts the shed with nobody left to
tell. obgw_wal_append_latency_ns and obgw_wal_sync_latency_ns are timed on opposite
sides of the group commit, and are 225× apart under -sync-every-command (17 µs against
3.9 ms) — a sync latency that is really an append latency reads healthy while fsync is
the slow thing. That also corrects the recovery point objective, which is
20 ms + p99 fsync and not the 20 ms ticker alone.
Cost: 82 ns and zero allocations per timing. Thirteen threshold rows in
RUNBOOKS.md, each with a normal value, a trouble value and an
action, because a metric nobody has a threshold for is a metric nobody looks at.
Adversarial review caught two of these before they shipped: a paging threshold of 1 s
standing against a histogram whose top bucket was 250 ms, so that tier could never fire
and an operator would have read healthy through an arbitrarily slow disk; and failed
logins invisible to a counter whose name implied it covered refusals.
Measured, and corrected
- Sharding scales 2.24×, not linearly. Two documents said distinct symbols "scale
linearly across cores" and nobody had measured it.BenchmarkShards_Scaling, pinned
to the four performance cores of an M4: 876 K ops/s at one book, 1.64 M at two
(1.88×), 1.97 M at four (2.24×) — and then nothing, 2.00 M at six and 1.99 M at
eight, inside the ±4% spread. Each shard is a pair of goroutines, so past the core
count the machine goes into the handoff. Books beyond that buy queue ...
v0.25.0 — checking its own claims
A documentation release, and every item in it is this project checking its own
claims rather than adding new ones. Two published allocation ratios were stale, one
of them flattering by 2×; a test count corrected yesterday was stale again today;
and the rule that catches all of this finally got written down instead of being
folklore in a test-file header.
Fixed
-
The published test count goes stale by construction, so it is now a floor. It
read 480 for several releases after it stopped being true, was corrected to an
exact 584 — and was stale again within a day. It now reads "over 600" with the
command to count them, for the same reason v0.19.0 deleted the hardcoded "latest
version" from the docs page rather than updating it. A floor can only ever become
an understatement. -
Two stale allocation ratios in BENCHMARKS.md.
Addinto
a growing book was published as 1.05 allocs/op against a measured 2.01 — the
page understated the cost of growing a book by half, which is the third correction
this file has taken and the third in the flattering direction. Cancel + replace
read 0.009 against a measured 0.0000, understating the engine instead.Neither is a regression: both reproduce identically on pre-session code. Both are
deterministic allocation counts rather than timings, and both are printed by
pkg/orderbook/alloc_test.goon every run — the figures were in the test log the
whole time and nobody re-read them. -
Config.ShardIndexmoved to the end of the struct. Appending cannot shift the
offsets of fields the match path reads; inserting can.
Added
-
TESTING.md — the rule the rest of the documentation rests
on, written down: a test does not count until it has been run against code
deliberately broken in the way it claims to detect. It was already the standing
rule for the replication drills, whose file header names the sabotage each was
verified against; this generalises it, because the same mistake has now been made
six times across five tests, in code written carefully by people who knew about
the rule.The case studies are the point, and they are all from this repository: a digest
test satisfied by a sequence counter, a checksum test satisfied by a magic number,
a timing test that first passed against a short-circuit and then failed against
correct code, a drill that blamed the wrong follower, and a test double more
permissive than the venue it stood in for. Each read correctly and would have
passed review. Linked from CONTRIBUTING's quality bar, where it changes what a
contributor is asked to have done rather than merely believed.
Upgrading: nothing to do — documentation and one struct field reordering, no API or wire changes.
Full changelog: v0.24.0...v0.25.0
v0.24.0 — labelled gauges, and a dashboard that could not connect
Two things the previous release named as unfinished, and one it did not know about.
The gauge work was the stated item; making it exposed that cmd/obdash had been
unable to connect to the gateway since the wire went to v4, and that every one of
its tests passed anyway because its own test double never checked the field the real
venue refuses on. That is the more useful of the two findings, and it is the one
that was not on the list.
Fixed
-
A constant-time-auth test that flaked, and would have taught people to ignore
it.TestAnUnknownAccountDoesTheSameWorkAsAWrongSecretsummed wall clock across
rounds, so it measured the work plus however long the scheduler kept the
goroutine off a core — and the second term is unbounded. Running the package
alongside the rest of the repo pushed the ratio to 4.19 against a threshold of 4,
twice, on code with no short-circuit in it. It now takes the floor of five
rounds: noise only ever adds time, so the minimum estimates the real cost while a
sum estimates the real cost plus the worst interference either arm happened to
meet. Against a genuine short-circuit the ratio is now ~8000 rather than barely
over 4. SOAK.md reached the same conclusion about heap growth for
the same reason: watch the floor, not the trend. -
cmd/obdashcould not connect to a wire-v4 gateway. It sent noSymbolon
its market-data subscribe, so the venue would have refused it outright — and its
own test double never validated the field, which is why every test passed. The
double is now as strict as the real venue: a test double more permissive than
the thing it stands in for certifies the wrong system.
Changed
-
Price gauges are a labelled family, one series per book.
observability.Collector.GaugeFamilyregisters a gauge that has one reading per
label set, with HELP and TYPE written once;orderbook_best_bid,
_best_ask,_spread,_last_trade_priceand_phasenow carrysymbol="…".
Countable gauges (queue depth, resting orders) stay bare and sum across books,
because a venue's queue depth is its queue depth — but a last trade price across
two instruments is not a number.They carry the label at a one-instrument venue too, which is a breaking change
for anything scraping the bare names. A metric whose label set depends on how the
venue happens to be configured is one no dashboard can be written against, since
series would appear and disappear as instruments were added.cmd/obdashselects
the series for its-symbol.
Upgrading: the price gauges (orderbook_best_bid, _best_ask, _spread, _last_trade_price, _phase) now carry a symbol label at every venue, including one-instrument ones. Anything scraping them by bare name needs updating; countable gauges are unchanged. cmd/obdash selects the series for its -symbol flag, which it now also sends when subscribing.
Full changelog: v0.23.0...v0.24.0
v0.23.0 — the multi-symbol gateway
The reference gateway catches up with the core. cmd/obgw was the last thing
standing between the multi-symbol design and a venue anyone can run, and converting
it found the bug that refactor was most likely to hide: buildOrder validated the
incoming symbol and then stamped the configured one onto the order, so at a
two-book venue every order silently landed on the first book. A refactor that
compiles is not a refactor that works.
Added
-
cmd/obgwserves a set of instruments.Config.SymbolsandConfig.DataDir;
one book per symbol — matching goroutine, command log, market-data feed, rate gate
— under a venue-wide account layer: oneRegistry, one publisher, one stream per
account. A client holds one session and sees one ordered conversation whatever mix
it trades, which is what makes a client id enough to name an order without also
naming a symbol.A one-book venue is unchanged: same config fields, same
WALPathand
SnapshotPath, same small dense ids, and the entire existing test suite passes
untouched. -
Mass cancel and
Queryfan across every book and aggregate — an account's orders
are its orders. Readiness takes the worst book, since a venue with one wedged
matcher is not ready however healthy the others are.
Fixed
- Routing a cancel needed the session to remember, not the registry to be asked.
wire.Cancelnames an order byClOrdIDalone, so the gateway must pick a book
without being told which. Resolving the engine order id up front to read its shard
field is the obvious move and it is wrong: the naming index is written by the
matching goroutine, so a cancel arriving while its own Enter is still queued
resolves to nothing and is refused for an order that is about to exist — the
orphaned-order defect SOAK.md measured at 12,843 orders in thirty
seconds. The session already knows: it read the Enter, and the Enter carried the
symbol.
Known limits
- Price gauges do not aggregate across books and report the first one. A last
trade price averaged over two instruments is not a number; the right answer is one
series per symbol, which needs label supportpkg/observabilitydoes not have.
Upgrading: nothing to do for a one-book venue — Config.Symbol, WALPath and SnapshotPath behave exactly as before, and ids are unchanged. For more than one instrument, set Config.Symbols and Config.DataDir.
Full changelog: v0.22.0...v0.23.0
v0.22.0 — the multi-symbol release
The multi-symbol release, and the third in a row where writing the spec first found
a defect that predated the feature. PRODUCTION-READINESS.md called a multi-symbol
venue "a routing layer you write." Checking the code instead of the sentence:
ShardsConfig had no way to supply a CommandLog, and durability, recovery and
replication all hang off it — so a sharded venue could not survive a restart at
all. That is not a routing layer, it is most of a venue.
The design decision everything else follows from is a refusal: there is no order of
events across symbols, because a venue-wide sequence needs a serialisation point
every command passes through, which is the bottleneck sharding exists to remove.
Each symbol is its own timeline; what that costs is listed in the spec rather than
discovered later. Ids are the one thing shared across books, and they are
partitioned rather than centralised precisely so that sharing costs no
coordination — a shared counter would have made a shard's ids depend on how its
traffic interleaved with every other shard's, so replaying one log alone would no
longer reproduce its own ids.
cmd/obgw still serves one instrument, and is now the only thing between this
design and a multi-symbol venue anyone can run.
Added
-
Multi-symbol identity (MULTI-SYMBOL.md, deliverables
all six). Order and trade ids partition into a 15-bit shard index and a 48-bit
per-shard counter, so anint64names an order at a venue with many books. A
shared counter would have been simpler and wrong: it makes a shard's ids depend on
how its traffic interleaved with every other shard's, so replaying one log alone
would no longer reproduce its own ids — trading the property this project is most
confident about for one it merely wants. Shard 0 composes to the sequence itself,
so every single-symbol deployment keeps the ids, snapshots, logs and golden vectors
it already had.matching.Manifestis the price: a durable, CRC-checkedsymbol -> indexmapping,
refused rather than repaired. Losing it is worse than losing a snapshot, which
costs only a replay. -
ShardsConfig.NewLog— the finding that turned a one-line gap into a spec.
ShardsConfighad no way to supply aCommandLog, and durability, recovery and
replication all hang off it, so a sharded venue could not survive a restart at
all.PRODUCTION-READINESS.mdcalled multi-symbol "a routing layer you write";
it was most of a venue. One log per shard, so recovery and the replication drills
are the existing single-symbol code paths run N times. -
Venue-wide
ClOrdIDadmission (Registry.IsLiveClOrdID). The naming index is
keyed by account and client id with no symbol, so at two instruments a repeat
overwrites the first and the account's next cancel retargets. -
examples/multisymbol— a two-book reference venue with a feed and a log per
shard, serving both books over sockets. It exists for the reason
examples/replicationdoes: the multi-symbol seams are each plausible on the page,
and this repository's record with seams claimed but never consumed is documented
and bad. Replication drill D8 covers a two-symbol venue, with a negative
control — a follower on the wrong shard index rebuilds the same orders under
different numbers, and the digest catches it.cmd/obgwis deliberately not converted: it still serves one instrument, and
it is now the only thing between this design and a multi-symbol venue anyone can
run. Converting it is one runner, one feed, one gate, one recovery path, sixteen
call sites and fifteen test files — its own arc, not a rider on a protocol change.
Changed
-
BREAKING: wire protocol v3 → v4.
MDSubscribegainsSymbol. It named an
incarnation and a sequence but no instrument, so a market-data connection could
only ever mean "the one book this venue serves". A subscription now selects
exactly one symbol and every message on that connection belongs to it, so no
other market-data payload changed — the regenerated golden vectors differ only
in their version byte. A subscription for an unserved instrument is refused with
MDRejectUnknownSymbolrather than quietly given the wrong book, which a
subscriber cannot detect for itself. -
BREAKING:
Shardsrefuses a second symbol without aManifest. It previously
gave every shard index 0 and served colliding ids silently — a failure whose only
symptom is two orders nobody can tell apart, discovered much later. Anyone running
Shardsmulti-symbol today was doing this unknowingly. -
Engine.Bustvalidates the shard field, so busting another symbol's trade is
ErrUnknownTradeinstead of annulling whichever local print shared the low bits.
Fixed
-
Replication drill D6 blamed the wrong follower, about one run in twelve. The
drill drove traffic untilShed() != 0and assumed the follower cut was the
wedged one. A follower that actually applies commands is slower than a wedged
socket — which merely fills a kernel buffer and costs the primary nothing until it
is full — so driven flat out, the healthy follower's own ship buffer overflowed
first and it was the one shed. The drill then reported "shedding the wedge broke
the healthy follower", the opposite of what had happened.Primary.ShedPeersnow attributes each cut to a peer address, which is the part
worth having beyond the test: a bare drop counter cannot tell an operator whether
a client stopped reading or a follower is merely running behind, and those need
opposite responses. D6 waits for the wedge specifically, asserts no other follower
was cut, and paces the tape against the healthy follower so there is one candidate
rather than two. 0 failures in 40 runs, against roughly 8% before; still fails
against a fanout that blocks instead of shedding.
Upgrading: this release contains two breaking changes. Any client built against wire v3 must be rebuilt (MDSubscribe gains Symbol), and anyone driving matching.Shards with more than one symbol must now supply a Manifest — previously every shard silently used index 0 and minted colliding ids.
Full changelog: v0.21.0...v0.22.0
v0.21.0 — the trade-bust release
The trade-bust release, and a reminder of why this project writes specs first: the
spec found a defect that had been shipping for four releases before a line of the
feature existed. An operator halt issued after the last checkpoint was never written
to the log, so a venue somebody had deliberately stopped came back open. Trade bust
needed a durable seam for control commands, went looking for one, and there wasn't
one.
The feature itself is mostly a list of things it refuses to do. A bust annuls a
print; it does not put the orders back, does not un-fire the stops the print
triggered, does not rewind the reference price, and does not amend the event that
reported the trade. Each of those is a test, because each looks like a bug until you
notice the book at bust time is not the book at trade time.
Added
-
Trade bust (
Engine.Bust, TRADE-BUST.md) — annulling a
print that has already been published. It is an appendedEventBustedreferring
backwards by trade id, never a rewrite, because the tape a follower replays has to
stay identical to the tape the primary produced. The surprising part is what it
deliberately does not do, and each of the four has a test: the busted orders are
not re-rested, the stops the print fired stay fired,LastTradePriceis not
rewound, and the trade event is not amended. A bust arrives after the market has
moved, and each of those undos would be a second wrong rather than a correction of
the first.The registry lives in the snapshot and therefore in the digest — two engines that
applied the same commands are equal only if they also agree on what settled. Drill
D7 is why that matters: a follower that drops the bust has a byte-identical book
and a different digest, which is the only reason the divergence is detectable at
all.marketdata.Feedis the consumer, publishingUpdateBustalongside the trade
id thatUpdateTradenever carried.Validation is identity-only: the engine refuses ids it never issued and says
nothing about price, size or counterparty, because it does not retain the trades it
printed. Duplicate busts are refused rather than swallowed.
Fixed
-
Control commands were applied but never written down.
Runner.logCommand
ended withdefault: return // control commands carry no book state; the snapshot covers them. The snapshot covers them as of the snapshot — so a halt, resume,
cancel-only or mark-price change issued after the last checkpoint was in no log,
recovery did not replay it, and a venue an operator had deliberately halted came
back Open, ready to trade, with nobody told. Shipped in every release since
control commands existed.It is the same reasoning error as the durability comment corrected in v0.20.0: a
guarantee stated against the wrong reference point. It surfaced because trade bust
needed a durable seam for control commands and the seam turned out not to exist —
CommandLognow carriesAppendHalt/Resume/CancelOnly/SetMark/Bust, and
TestControlCommandsSurviveRecoveryfails against the old code. Breaking for
anyone implementingmatching.CommandLogoutside this repository. -
The threat model claimed a trade-bust path that did not exist, and named the
wrong mechanism for it. THREAT-MODEL.md credited the WAL
spine with "clean trade-bust / replay" while
PRODUCTION-READINESS.md said there was no way to
amend a published trade. Writing the spec settled it: nobody had built one, and
replaying a log without the busted trade — the mechanism the row described —
rewrites history and hands every downstream consumer a tape that never happened. -
The published test count was 100 short.
PRODUCTION-READINESS.mdsaid 480 test
functions for several releases after the suite passed it; it is 584, and the line
now carries the command that produces the number so the next reader can check it
instead of believing it. The event-conformance suite is 23 scenarios, not 22.
Changed
-
Wire protocol v2 → v3: a trade now has a name.
ExecutedandMDTrade
reported price, quantity and aggressor but no identifier, so no message could ever
refer back to one specific print — which meant a venue with trade bust could annul
a fill it had never named, and no client could be told which one. Both payloads
gainTradeID(+8 bytes each), and two messages use it:Busted(U) on
order entry, private to the two counterparties, andMDBust(u) on market
data, public. Every other payload is byte-identical to v2 apart from the version
field itself — the discipline a bump is supposed to carry, and what the regenerated
golden vectors show.Breaking:
internal/wireis not importable, but any client built against v2
must be rebuilt.pkg/orderentry.MsggainsTradeIDandKindBusted.Routing the bust turned out to be harder than encoding it, and for a reason that
is the whole shape of this feature: by the time a bust arrives, both orders have
usually left the book.orderentry.Registryforgets an order the moment it fills
or cancels, so the obvious implementation — look the trade up among live orders —
delivers a bust to nobody in the common case. The Registry now keeps a bounded
memory of recent prints (SetFillMemory, default 65,536, about 26 seconds of tape
at the SOAK.md rate) purely so a bust can be routed, and one older than that memory
incrementsUnroutableBustsrather than vanishing — "we could not tell the client"
is an operational fact somebody has to act on. Size it to your bust window; CME's
is eight minutes.
Full changelog: v0.20.0...v0.21.0
v0.20.0 — the outside-review release
The first release whose corrections came from outside. A reader on
r/highfreqtrading read the WAL's durability comment closely enough to see it was
ordered against the wrong thing, and proposed the recovery test that would have
caught it; both are below, credited, because the alternative is pretending the
audit was internal. Shipping alongside them: the interactive tutorial, which
teaches the order book by making you every player in it, on the real engine.
Fixed
- The WAL's durability claim was ordered against the wrong thing. The package
comment said records are written write-ahead "so no acknowledged command is
lost." Write-ahead is ordered against apply, not against the acknowledgement a
client receives: append writes into a buffer, onlySyncsurvives the process,
and withobgwgroup-committing every 20ms an order could be acknowledged and
then vanish. The comment stated the opposite for four releases. A reader on
r/highfreqtrading pushed on exactly this and was right. The window is now stated
rather than denied, andobgw -sync-every-commandcloses it for anyone who
wants acknowledgement to follow durability — correct, and ~210× the cost,
because the fsync lands on the matching goroutine.
Added
-
Crash-at-every-boundary recovery test. The suite sampled five checkpoints
across a 2000-command tape and compared only the book; the new test kills at
every write and emit boundary and compares the trade tape too. Both halves are
verified against deliberately broken code: dropping every replayed cancel fails
the book assertion, and publishing replayed event batches in reverse order —
same event count, so the digest is untouched — fails the tape assertion while
the old test passes against the identical sabotage. Also proposed by the same
reader. -
The interactive tutorial (
web/learn.html,
TUTORIAL-SPEC.md) — learn the order book by being
every player in it, on the real engine. Two devices no static explainer has:
the ladder assembles itself as concepts arrive (chapter 1 opens on an empty
market, because a market is a list of intentions and the list starts empty),
and every objective is verified against the engine's actual book state — "get
filled before the rival" completes when the book proves it, not when a Next
button is pressed. Six chapters: the first seller, the wall builder, the
taker, the queue jumper, the whale (the same 8-lot order against a thin and a
deep book, with the measured slippage as the lesson), and the market maker,
quoting both sides of a live market with honest P&L — including the loss
case, when the price moves through the quotes. Browser-verified end to end;
the pacing fixes came from watching an impatient automated learner reset its
own queue position by re-quoting — which is itself the chapter's lesson.
Full changelog: v0.19.0...v0.20.0
v0.19.0 — the showcase release
The library learns to show itself — and stops calling itself production-grade.
- The live console — try it: the real engine compiled to WebAssembly matching a noise-trader market in your browser, with OFI/CVD/imbalance/Kyle-λ computed by the shipping
pkg/signalscode andpkg/surveillancewatching every event. Every panel is titled by the exact library call that produces it. Place a limit order and watch it rest; run a spoof or a flood and watch the shipping detectors name the account. The market bar streamsEngineSnapshot.Digest, so the determinism claim is falsifiable from the page. cmd/obdash— an operator dashboard that is deliberately an ordinary subscriber: the venue's own market-data wire plus its/metricspage, re-served over SSE. The venue gains no code, no port, no attack surface — and the protocol gets a consumer written from the format alone, outside its own test tree.- "Production-grade" is retired from every header. Production readiness is a property of a deployment, not of code; no deployment exists, and independent review stands at none — so the headers now carry only what the tests can prove, and docs/PRODUCTION-READINESS.md carries the honest account. The docs page's stale benchmark figures (published for twelve releases) were caught and corrected in the same pass.
Full detail: CHANGELOG.md, docs/CONSOLE-SPEC.md.
v0.18.0 — the replication release
The HA seams get the consumer that would have noticed if they were phantoms, and the edge stops holding plaintext.
EngineSnapshot.Digest— the crash-recovery fingerprint, promoted from a test helper to a contract.wal.SetOnAppend— the live log tail. Its first shape handed subscribers a pointer into state the matcher keeps mutating; the drills' first-racerun caught it, and the hook now hands record bytes — the wire is byte-identical to the log.examples/replication— the drilled primary-backup reference: async shipping with a measured loss window, the incarnation fence on promotion, drills D1–D6 in CI, and a failover runbook. Split-brain prevention stays deliberately yours.- Digests at rest —
orderentry.HashedAccounts,sha256:credential entries, andobgw -hash-secret. A memory disclosure now yields digests, not passwords; rotation, revocation and expiry remain honestly absent.
Full detail: CHANGELOG.md, docs/REPLICATION.md — including §8, what building it found versus what the spec predicted.
v0.6.0 — market-integrity release
The market-integrity release: a research-grounded threat model and the defenses it prioritized, plus durable persistence. Try it live: https://intrepidkarthi.github.io/orderbook/
Highlights
- Durable WAL (
pkg/wal) — write-ahead command log + snapshots + torn-tail-safe recovery. - Threat model (docs/THREAT-MODEL.md) — 27 real order-book attacks → detection → defense → core-vs-layer, each tied to a named enforcement case.
- In-core pre-trade risk controls — fat-finger/dust caps, per-account order cap, minimum resting time, client-order-id idempotency, mark-price step + depth bounds, chunked liquidation, timed band-breach pause, randomized iceberg peaks, notional-overflow guard,
HALTED/RESUMEDevents. - Surveillance (
pkg/surveillance) — order-to-trade ratio, marking-the-close, ramping, pinging, and cross-book detectors. - Call-auction session (
pkg/auction) — open/close/recovery with a replay-safe randomized close. - Enforcing gateway (
pkg/gateway) — token-bucket rate gate + taker speed bump.
Breaking
Engine.SetMarkPricenow returnserror(rejects a mark update violatingMaxMarkStep/MinMarkDepth).
Full notes: CHANGELOG.md