ADR-0012: Strategy developer experience¶
Status: accepted (2026-09); section 7 superseded by ADR-0013
FastMM is pre-1.0 with no external users, so API changes are clean breaks with no deprecated shims.
Context¶
- Hooks fail silently. The engine detects hooks with
requires, so a wrong signature is never called and never reported.docs/adding-a-strategy.mdlists wrong signatures (on_trade,on_book_ticker) and leaves outon_connectionand eight context methods. - Instrument hooks are inconsistent.
on_bookgets(ctx, id, book);on_trade,on_book_tickerandon_option_tickerget(ctx, msg)and may receive instruments that are not in the table, so strategies repeatctx.instruments().contains(id). - Nothing tells a strategy that quoting is possible again.
set_quotesis ignored while quoting is disabled (control pull, kill, reconcile), and strategies still record the mid as quoted. ResumeQuotes, ResetKill and Reconcile End only flip flags. - Fills are awkward.
on_fill(ctx, OmsUpdate, OrderFillMsg); every strategy repeatsu.known ? u.order.instrument : InstrumentId{}. - Fixed-point math is verbose. Strategies convert doubles in
on_start, keep centi-bps members and writeInt128casts by hand. The example callsQty::from_doubleper event. - Registration is spread over four places (
apps/fastmm-live/runner_<s>.cpp,live_runners.hpp,live_runners.cpp,src/backtest/registrations.cpp).FASTMM_REGISTER_STRATEGYrelies on static initialisers, which static archives drop. - External projects cannot trade live without forking. The install exports core, sim, backtest, codecs, net and venues, but not the live session code or a strategies library.
- There is no test harness for strategies, and the no-registration path
bt::run_backtest<S>(cfg, source)is not documented. - Python can run backtests of C++ strategies but cannot define strategies.
- Docs drift.
adding-a-venue.mddescribes a registration scheme and feed signatures that do not exist;schema.hpplists abinance_futuresvenue kind that has no connector.
Two engine defects surfaced during review and are fixed before any API work:
- Reconcile holes. Reconcile Begin pulls quotes; a requote that meets orders still waiting for their cancel ack is skipped without remembering the target, and
reconcile_open_orderclears an in-flight PendingCancel. Orders a strategy sends on channelLivebefore the open-orders snapshot are marked Canceled byreconcile_endand their ack is then ignored, leaving an order at the venue that the OMS no longer tracks.reconcile_endupdates reach the strategy but not the QuoteManager. - Late fills are not booked. For a recently terminal order the OMS reports
knownwithout an order snapshot, so the engine books the fill on an invalid instrument and it never reaches position, fees or PnL.
Smaller defects: StrategyContext::pull_all_quotes skips mark_decision(); direct orders may use a user_tag in the QuoteManager tag range; clamp_to_touch exists twice with different behaviour, and the level-0 uncross fix is copied into all three strategies; the journal config_hash hashes the config file, not the effective configuration.
Constraints that do not change: no virtual calls and no allocation on the hot path (ADR-0009); one engine template for live, sim and replay; replay hashes stay the determinism proof (ADR-0010); static libraries (ADR-0006).
Decision¶
1. Hooks: plain member functions, checked at compile time¶
A new include/fastmm/strategies/hooks.hpp holds one X-macro table (name, expected call, message) that drives dispatch, the checks and the reference table in the docs.
| Hook | Signature |
|---|---|
on_start, on_stop |
(auto& ctx) |
on_book |
(auto& ctx, InstrumentId id, const auto& book) |
on_book_ticker |
(auto& ctx, InstrumentId id, const BookTickerMsg& m) |
on_trade |
(auto& ctx, InstrumentId id, const TradeMsg& m) |
on_option_ticker |
(auto& ctx, InstrumentId id, const OptionTickerMsg& m) |
on_fill |
(auto& ctx, const Fill& fill) |
on_order_update |
(auto& ctx, const OmsUpdate& u) |
on_timer |
(auto& ctx, TimerId id, std::uint64_t tag) |
on_connection |
(auto& ctx, const ConnectionStateMsg& m) |
on_quoting |
(auto& ctx, bool enabled) (new) |
Rules:
- All hooks return
void;template <class Ctx>spelling is equivalent toauto&.noexceptis recommended, not enforced (hooks already run insidenoexceptengine code). - Instrument-scoped hooks fire only for instruments in the table, so
ctx.book(id)is valid. on_fillfires after position and fees are updated and before theon_order_updateof the same execution, for every fill on an instrument in the table, including late fills and fills for unknown orders.on_connectionfires for every state change. For non-Livestates the engine first pulls that venue's quotes and, on the market-data channel, clears its books.on_quoting(enabled)reports changes ofctx.quoting_enabled(). The engine compares the value before and after each dispatched event, fired timer andstart(), and calls the hook after the triggering hook has returned and all flags are final (for reconcile, afterreconciling_is cleared and the quotes paused at Begin are resumed, so the strategy may replace them). It never fires from inside a context call, so a kill switch tripped byset_quotesorsendcannot re-enter the strategy. It does not fire at start:on_startreadsctx.quoting_enabled()(dry-run starts disabled). Connection pulls do not changequoting_enabled();on_connectioncovers them.on_stopdoes not pull quotes; shutdown's cancel-all does.
Fill is a view built on the stack by the engine. Its pointers are valid only during the call; do not store them.
struct Fill {
InstrumentId instrument; // the order's instrument, else the fill message's
Side side; Price price; Qty qty; // this execution
Qty position_delta; // signed change of the position (differs from qty when commission is
// charged in the base asset)
Notional fee; // quote currency as booked
bool fee_converted = true; // false: commission in a third asset, not booked (counted in stats)
Liquidity liquidity;
bool known = false; // an order snapshot is available (`update->order` is populated)
bool late = false; // the order was already terminal when the fill arrived
bool order_done = false; // the order reached a terminal state with this fill
const OmsUpdate* update = nullptr; // non-null when known
const OrderFillMsg* msg = nullptr; // always set
};
Checking (verified on gcc 13.3 and clang 18.1):
- A name probe detects any member with a hook's name (function, template, data member, static, inherited, private). If the name exists and the engine's call does not compile,
static_assertfails withfastmm: on_trade has the wrong signature or is not public; expected void on_trade(auto& ctx, InstrumentId id, const TradeMsg& m). Each hook has its own assert, so one build reports all of them. - A near-miss list (
on_fills,on_trades,on_order_book,on_tick,on_bbo,on_order,on_execution,on_disconnect,onBook, ...) produces a warning through a[[deprecated]]marker ("on_fills is not a hook; did you mean on_fill?"). It is a warning because a private helper can legitimately use such a name;FASTMM_WERRORbuilds turn it into an error, and a strategy can silence it withstatic constexpr bool fastmm_allow_near_miss_names = true;. - The check runs in the
Engineconstructor. Strategy headers may addstatic_assert(fastmm::verify_strategy<MyMM>());, which uses stand-in context and book types. Engine::start()logs the implemented set once:strategy basic_mm hooks: start book fill timer connection quoting.- Documented limits: a
finalclass loses the name probe (the call check remains); a hook that is ambiguous across two bases is reported as a wrong signature; anauto-returning hook must not be checked withverify_strategy(the stand-in types instantiate its body); private hooks are rejected, also withfriend Engine(see Deviations); implicit conversions are accepted (on_timer(auto&, TimerId, int)compiles).
Rejected: a CRTP base with no-op defaults (typos compile and call the default; the engine can no longer skip empty hooks), one overloaded on(ctx, Event) (overloads cannot be enumerated, a generic overload swallows everything), a single on_event visitor (users write the dispatch), an explicit HookSet declaration (repeats what the code already says).
2. Context API¶
| Group | API |
|---|---|
| Time, reference data | now(), instrument(id), instruments(), contains(id) |
| Market data | book(id) |
| Portfolio | position(id), portfolio() (realized, unrealized, fees, net) |
| Quoting | set_quotes(id, q) -> bool (false: suppressed while quoting is disabled), pull_quotes(id), pull_all_quotes(), working_quote(id, side, level) -> const Order* |
| Direct orders | send(req) -> Result<ClientOrderId, RejectReason>, cancel(id), replace(id, px, qty), order(id) -> const Order*, open_qty(id, side), oms() |
| Timers | every(period, tag) -> TimerId, once(delay, tag) -> TimerId, cancel_timer(id) |
| Control | quoting_enabled(), killed(), request_stop() |
| Randomness | rng() (seeded, replay-deterministic) |
NewOrderRequest::limit(id, side, px, qty)with.post_only(),.reduce_only(),.ioc().ctx.sendrejects auser_taginside the QuoteManager range withRejectReason::InvalidTag. The check lives in the context path, not insubmit_new, which quotes also use.InvalidTagis appended to the OMS block ofRejectReason; existing values keep their numbers (they are journaled).- Every order-API method, including
pull_all_quotes, callsmark_decision(). - Removed:
add_timer(period, bool, tag)andinstrument_count(). - Logging stays the
FASTMM_LOG_*macros. Logs are not journaled; strategy logic must not depend on them.
3. Fixed-point helpers¶
In core/fixed_point.hpp and a new strategies/quoting.hpp, constexpr and integer-only on the hot path:
Ratio = Fixed<RatioTag>(1.0 = 1e8 raw, 1 bp = 10'000 raw).Fixed<T> * Ratiomultiplies throughInt128with one truncation toward zero.ratio(num, den),Ratio::from_bps(double)(startup only),to_bps()(diagnostics).- Exact literals in
fastmm::literals:100.25_px,0.01_qty(up to 8 decimals),5_bps,0.25_bps(up to 4 decimals). Too many decimals is a compile error. mid(bid, ask),microprice(Level bid, Level ask),spread_ratio(bid, ask),away_from(ref, side, dist),inventory_allows(side, position, qty, limit),Instrument::ticks(n).DesiredQuotes::bid(px, qty)/ask(px, qty)(drop non-positive values, returnbool),DesiredQuotes::uncross(tick),keep_passive(q, best_bid, best_ask, tick)(BasicMM's whole-ladder shift). OptionsMM keeps its own level-0 clamp, which is a different rule, and usesuncross.
A new umbrella header include/fastmm/strategy.hpp includes what a strategy needs: fixed point and literals, instrument, quotes and helpers, params, hooks, log macros.
The BasicMM formula port is bit-identical: mid.raw * (cbps * 100) / 1e8 truncates exactly like the old mid.raw * cbps / 1e6, including the skew sign. Intended differences, each re-baselined with an explanation in its change: bps configs with more than two decimals gain precision; a non-positive ask is dropped instead of rejected; on_fill now reaches strategies for late and unknown fills; late fills now move positions and therefore skew.
4. Parameters¶
FASTMM_PARAM stays the single declaration (field, default, range, doc). It gains:
Price,QtyandNotionalfields, parsed exactly.FASTMM_PARAM_BPS(name, ...)(fieldRatio, configured in bps) andFASTMM_PARAM_MS(name, ...)(fieldDuration, configured in ms). Config key names and values stay as they are.- Exact parsing accepts exponent notation, because TOML floats reach the parser through fmt (
2e-05) and Python throughrepr. A value is rejected only if more than 8 decimals (4 for bps) remain after applying the exponent. ParamDesc::parse(void*, string_view) -> optional<string>andformat(const void*)replaceset(double)andget.double def/min/maxstay for display and Python.- Schema type names:
int,double,bool,decimal(Price, Qty, Notional),bps,ms. Python mapsdecimalandbpsto float andmsto int;python/tests/test_smoke.pyis updated. - An optional
std::optional<std::string> validate() conston the params struct, run byconfigure()after all keys are applied (cross-field checks such asquote_qty <= max_inventory). StrategyBase::params()is const-only andparams_becomes private.
Rejected: Boost.PFR (dependency, no docs or ranges), descriptor tuples (names written twice), schema files (two sources of truth).
5. Registration: one function per strategy library¶
// mm/strategies.hpp
namespace mm { void register_strategies(fastmm::StrategyRegistry& r); }
// strategies.cpp: the only registration file an author writes
#include "fastmm/strategies/factories.hpp"
#include "mm/microprice_mm.hpp"
#include "mm/strategies.hpp"
void mm::register_strategies(fastmm::StrategyRegistry& r) {
fastmm::register_strategy<mm::MicropriceMM>(r); // Sim + Replay + Live
}
strategies/module.hppdeclaresStrategyModule,register_strategy<S>(r), the factory templatessim_factory<S>,replay_factory<S>,live_factory<S>and theFASTMM_INSTANTIATE_STRATEGY(S, kind)macro. It never instantiatesEngine. The factories are declaredtemplate <class S>(nottemplate <StrategyLike S>) and checkStrategyLike<S>withstatic_assertin their definitions: GCC and Clang mangle constrained templates differently.factories.hppincludesfactory_sim.hpp,factory_replay.hppandfactory_live.hpp, so the file above instantiates all three engines. Taking a factory's address only emits a reference, so a missing instantiation is a link error naming the factory.- The app references the registration function, so the linker pulls it and its instantiations out of static archives.
StrategyRegistry::try_addreturnsAdded,AlreadyPresent(same schema and factory),ConflictorInvalid.register_strategythrows onConflict; CLIs exit 3 with the message. Tests that register fakes use a localStrategyRegistry, not the global one.- Built-in strategies are always registered; a module list adds to them. Names stay flat with conflict detection. Only a strategy's owner instantiates it; others call the owner's function.
FASTMM_REGISTER_STRATEGYis removed.- Built-ins use an internal CMake function that generates one file per (strategy, transport) so the nine engine instantiations compile in parallel. It is not installed or documented until an external project needs it; a strategy that should not run live is not registered by that project's live app.
Rejected: static self-registration (dropped by archives, fails at runtime), X-macro strategy lists (external projects would edit FastMM), cli::live<A, B>() variadics (all engines in main.cpp, not shareable with Python), dlopen plugins (ADR-0006, ABI coupling, arbitrary code in a trading process).
6. Runtime libraries and entry points¶
fastmm::core ─ fastmm::sim ─ fastmm::strategies (built-ins) ─ fastmm::backtest (+ cli::backtest, cli::replay)
fastmm::net ─ fastmm::venues ─ fastmm::live (run_live + cli::live) ── links fastmm::strategies
namespace fastmm::cli {
int live(int argc, char argv, std::initializer_list<StrategyModule> modules = {});
int backtest(int argc, char argv, std::initializer_list<StrategyModule> modules = {});
int replay(int argc, char argv, std::initializer_list<StrategyModule> modules = {});
}
// apps/fastmm-live/main.cpp
int main(int argc, char argv) { return fastmm::cli::live(argc, argv); }
apps/fastmm-live/{live_backend,main}.cppmove tosrc/live/{session,cli}.cpp;LiveBackendandmake_live_runnermove toinclude/fastmm/live/live_backend.hpp. Live factories need onlyfastmm::core.runner_*.cpp,live_runners.*andsrc/backtest/registrations.cppare deleted.- The backtest and replay CLI bodies move into
fastmm::backtest. All three apps become 3-line mains. fastmm-livegains--strategy <name>and--param k=v; changing the strategy clears[strategy.params](asfastmm-backtestdoes). The journalconfig_hashhashes the effective configuration after overrides. Error prefixes use the program name; log tags stayfastmm-live:.cli::liveinstalls process-wide SIGINT/SIGTERM handlers. Deferred: aLiveSessionclass without signal handling, to replace the integration tests' own wiring.fastmm::strategiesandfastmm::livejoin the install export. Supported consumers use the same compiler as the FastMM build:find_package(fastmm)against an install prefix, oradd_subdirectory/FetchContent. Installing binaries and configs, cross-compiler consumers, fat LTO objects and version-compatibility policy are deferred until the repository is public.examples/external-project/replacestests/install-consumerand is a copyable template: one strategy header,strategies.cpp, live/backtest/replay mains, a unit test using the harness (section 8). CI builds it against the install prefix, lists strategies, runs a synthetic backtest, runs its live app for 15 s against the build tree'sfastmm-sim-exchange, and replays the journal.
7. Python strategies (v1)¶
- Model. A
fastmm.Strategysubclass with the same hook names and argument order as C++ (on_book(ctx, inst, book),on_trade(ctx, inst, trade),on_fill(ctx, fill),on_quoting(ctx, enabled), ...) andParam(default, min=, max=, doc=)descriptors. Undefined hooks are never called. - Bridge. One C++ adapter,
PyStrategy, compiled only into_core(python/src/). It is a normalStrategyLikeclass, so the engine, risk, OMS, QuoteManager, journal and outbound hash are unchanged. It caches the bound functions and a hook bitmask, reuses one view object per instrument and event type, and calls with vectorcall. A view read outside its callback raisesStaleViewError(one integer compare). - Numbers. Prices and quantities are available as floats (
book.mid) and exact raw int64 (book.mid_raw).set_quotesrounds floats to tick and lot (bids down, asks up);set_quotes_rawtakes raw ints. NoDecimal. - Context. Mirrors section 2 in snake_case (
now_ns,every,once,random()from the engine RNG). - GIL. Held for the whole run; released briefly every 1,024 hooks, where
PyErr_CheckSignals()also runs, so Ctrl-C works on the main thread. - Errors. A raising hook stores the exception, pulls all quotes and requests stop.
EngineHooksgains a nullablestoppedcallback honoured bySimDriveronly (this also makesrequest_stop()work in C++ backtests).ReplayDriveralways drains the journal, because stopping early would change the replay hash; a failed adapter goes inert instead.run_backtestraisesfastmm.StrategyErrorwith the traceback as__cause__and the partial result as.result. - Entry point.
run_backtest(cfg, data=None, strategy=None, params=None)wherestrategyis a registered name, aStrategysubclass or an unused instance. Backward compatible. - Scope. In-process backtests only. Deferred: replay of Python strategies, process-pool sweeps,
fastmm.fixed/fastmm.testinghelpers, research series (record), a C++ skeleton generator, and any live use (first a hybrid where Python publishes slow targets to a C++ strategy through shared memory, then an opt-infastmm-live-pylimited to the sim exchange and testnets).fastmm-livenever links libpython and the wheel stays net-free. - Custom C++ strategies in Python. No runtime loading into the prebuilt wheel (ABI coupling). Build
_corefrom source withFASTMM_PYTHON_STRATEGY_DIRS=<dir;...>(deferred with the rest). - Proof.
examples/python/strategies/basic_mm_exact.py, a raw-int port of BasicMM, reproducestests/fixtures/journals/sample_1000.sha256in CI.
8. Quick start, test harness and documentation¶
Quick start. The shortest path is one strategy header plus a main that calls bt::run_backtest<MyMM>(cfg, source), built with FetchContent: no registration, under 30 lines. It is the first page of the docs and is compiled and run in CI.
Test harness. include/fastmm/testing/strategy_harness.hpp (tier 1, in fastmm::sim): StrategyHarness<S> wraps a real Engine<S, SimClock, SimTransport, InlineFeed> with one instrument table and exposes book(...), trade(...), fill(...), disconnect()/reconnect(), pull_quotes()/resume_quotes(), advance(duration), and working_orders(). Authors test hooks without writing fake contexts; the docs' strategy API test and the external project use it.
Documentation. Markdown rendered on GitHub is canonical (the repository is private; a site generator and Doxygen are deferred until it is public).
- Layout:
docs/getting-started/,docs/tutorials/,docs/how-to/{strategies,venues,operations}/,docs/reference/,docs/explanation/,docs/contributing/,docs/adr/. - Tutorial "Your first market maker" in
examples/cpp/tutorial/: write the strategy, unit-test it with the harness, backtest, register, backtest and replay from the CLI, trade on the sim exchange with a disconnect fault, then Binance Demo (--dry-run, then a short keyed run with tight risk limits, Ctrl-C andcancel_all ok). Shell steps live inscripts/docs/tutorial.sh; CI runs the steps up to the sim exchange. - Reference: strategy API (hooks, context,
Fill, messages, params, helpers, harness), fixed point, public headers (docs/api/public-headers.txt; tier 1 strategy, testing and backtest API, tier 2 venue and codec extension API, everything else internal), CLI, configuration, journal format, status file, glossary, Python API.reference/cli.mdandreference/configuration.mdare generated from--helpandschema.hppby tools with a--checkmode. - How-to: register, test, patterns (inventory limits, timers, stale books,
on_quoting,set_quotesvssend), recorded-data backtests, sweeps; add a venue (rewritten from the real connectors, with a conformance checklist), add a binary protocol, record fixtures; run on testnets, go-live checklist, kill switch and shutdown, journals and PnL reconciliation (the Demo session's report becomestools/pnl_report.py), latency tuning, troubleshooting keyed by log text. - Explanation: event flow, determinism, risk model.
- Style: one page type per page, second person, present tense, British spelling (as today), commands copy-pasteable from the repo root, explicit units, no hand-copied code longer than three lines.
Docs checks in CI:
tools/doc_snippets.py --check: fenced blocks marked<!-- snippet: path#region -->match their source region (the tool rewrites them, so GitHub shows real code).tests/docs/strategy_api_doc_test.cpp: an all-hooks strategy with the documented signatures runs in the harness and asserts every hook fired;static_asserts pin each documented context method's type. The reference tables are snippets of this file.- Generated
cli.mdandconfiguration.mdare current; everyconfigs/*.tomlloads without warnings. - Every header in the public manifest compiles alone.
- The
examplesandtutorialctest labels run the C++ examples, the quick start and the external project; the tutorial script runs after the gcc-release build. - Relative links and anchors resolve (offline).
Consequences¶
- Strategies, test fakes and strategy docs break once. Golden outbound hashes are recorded on
mainbefore the change; each step keeps them or re-baselines them with the reason in the change. - A wrong hook signature becomes a readable compile error and a missing instantiation a link error.
- External projects get a supported path to live trading without forking, with a tier 1 API that must stay stable within a minor version.
- Compile-fail tests (
tests/compile_fail/, labelcompile_fail) join both compiler presets; each case has a control build that must compile and matches the expected message. - Python hooks are slower than C++: the integer BasicMM port adds about 3.1 µs per
on_bookoron_fillcall, and a synthetic backtest runs 1.31 M events/s against 2.02 M for C++basic_mm(Python API). That is acceptable for research and is why Python strategies stay out of live trading. binance_futuresis removed fromschema.hppuntil a connector exists.
Implementation¶
Implemented in 2026-09 in this order: golden hashes, engine fixes, hooks and context, fixed point and parameters, registration and runtime, Python strategies, docs.
Deviations from the decision¶
NewOrderRequest::limitreturns aLimitOrderbuilder (also with.tag(n)) that converts toNewOrderRequest, becausepost_onlyandreduce_onlyare data members.- The hook call check runs in
hooks.hpp, so the engine andverify_strategyagree and a private hook is rejected even withfriend Engine. - Near-miss warnings are emitted inside
hooks.hpp, so a consumer that includes FastMM as a system header does not see them; wrong-signature errors are unaffected. set_quotesalso returns false for an instrument outside the table; fills for such instruments are counted inEngineStats::unknown_instrument_fills.- A late fill gets the order's terminal record:
Fill::knownis true andupdate->ordercarries only id, instrument, side, state and filled quantity. - Exponents are parsed by a new
Fixed::parse;Fixed::from_decimalstays strict for venue strings. fastmm::literalsis an inline namespace;Ratio * Ratiois defined, andfrom_bps/to_bpsareFixedmembers constrained to theRatiotag.DesiredQuotes::bid/askalso drop a non-positive quantity and levels beyondkMaxQuoteLevels, so a zero-quantity level (quote_qtybelow the lot) is no longer sent.configure()applies keys to a copy and replaces the parameters only aftervalidate()passes.- AvellanedaStoikov's seconds-valued parameters and OptionsMM's
max_deltaandmax_vegastaydouble; no built-in strategy has avalidate(). - The
basic_mm/coupledandbasic_mm/l2_queuegolden hashes were re-baselined: their test market'shalf_spread_bps = 0.003had rounded to 0 in the centi-bps code. register_strategy<S, Transports>(r)exists for FastMM's own tests; the documented API isregister_strategy<S>(r).EngineConfigmoved tocore/engine_config.hppso registration headers do not includeengine.hpp, andStrategyRegistry::addwas removed.- Each command line lists only the strategies of its own transport;
--list-strategies --format jsonwas added. - The external project's unit test uses plain checks because the install does not export doctest;
-DFASTMM_SOURCE_DIR=switches it toadd_subdirectory. - Python hooks receive an
Instrumentvalue; sides and liquidity are integers, order states and reject reasons strings, and timers take nanoseconds. - Python
sendandreplaceraiseOrderRejected;cancelreturnsFalsefor an order that already ended. - Python
KeyboardInterruptandSystemExitpropagate with.resultinstead of being wrapped inStrategyError, and signals are also checked every 4,096 driver steps. - A Python strategy's journal name is
py:<QualName>, truncated to 31 characters. - The quick start sets one synthetic-market value, because the default market gives no fills to a quote 0.005 bps from the mid.
[backtest]and[sim]inreference/configuration.mdstay hand-written because the schema treats them as free-form.- Deferred from section 8: how-to pages for testing, patterns, recorded-data backtests, sweeps, binary protocols, fixtures and latency tuning; a log catalogue check; Markdown linting; a scheduled Binance Demo dry run.