Journal format¶
The .fmj journal records every event a session's engine consumed, in consumption order, with the engine clock, plus a copy of every order message it sent. Format version 3; readers also open versions 1 and 2. The decision and its history are in ADR-0010; the code is include/fastmm/core/journal.hpp.
All integers are little-endian. Prices, quantities and notionals are raw fixed-point int64 (1e-8), timestamps are int64 nanoseconds since the Unix epoch.
File layout¶
file := header (256 B)
| instrument[instrument_count] (128 B each)
| config (config_bytes, padded to 64)
| params (param_table_bytes, padded to 64)
| meta (meta_bytes, padded to 64)
| block* | trailer
block := block header (64 B) | message* (byte_len bytes)
header_bytesin the header is the offset of the first block.- Blocks are at most 1 MiB (
block_bytes); a message never straddles two blocks. - A clean shutdown appends a trailer: an empty block with flag bit 0 set. Without it the file was not closed cleanly; a reader drops a tail block whose checksum or length does not match.
JournalReader::complete()is true only for a file that ends in a trailer with no block discarded.fastmm-replayrefuses an incomplete file unless--allow-incompleteis given: its outbound stream stops short of what the session sent.
Durability¶
[engine] journal_sync chooses how far a write is pushed. The journal batches into 1 MiB blocks, so no record is durable at the instant the engine writes it.
journal_sync |
Every 100 ms | A record survives | Costs |
|---|---|---|---|
async (default) |
msync(MS_ASYNC) |
this process dying, as soon as its block is copied into the map | nothing measurable |
fdatasync |
msync(MS_SYNC) and fdatasync() |
power loss, once it is more than one tick old | one write-back of the dirty pages per tick |
A clean stop() always writes the trailer, msync(MS_SYNC)s, truncates the preallocated tail away and fdatasync()s, in both modes.
Extents are reserved with posix_fallocate, not ftruncate: a full filesystem fails when the next 64 MiB extent is reserved rather than with SIGBUS on the first store into a sparse page. JournalFileWriter::failed() latches that and every other write error, and fastmm-live polls it: a journal that cannot be written trips the kill switch, cancels everything and exits with code 5. sessions.journal_complete in the store records whether the file was closed cleanly.
Rotation and retention¶
[engine] journal_max_bytes rolls the file over when it reaches that size; 0 (the default) writes one file per session. A part only ends between blocks, so no message straddles two files. The first part keeps the configured name and the rest get a number before the extension:
Each part is a complete journal: it repeats the header, the instrument table, the configuration and the parameter table, and carries the same session_id. Sequence numbers continue across parts. The backtest's journal: source reads the parts of a session as one stream, given the first (data sources); fastmm-replay verifies a session journal one part at a time; the store records every part of a session in session_journals.
[engine] journal_retention_days deletes *.fmj files in [engine] journal_dir that were last written more than that many days ago, once, when a session starts; 0 (the default) keeps everything. Only the .fmj extension is removed, and a file it cannot remove is skipped with a warning.
Header¶
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 4 | magic |
FMJ1 |
| 4 | 4 | version |
3 (1 or 2 for old files) |
| 8 | 4 | header_bytes |
offset of the first block |
| 12 | 4 | instrument_count |
instruments that follow the header |
| 16 | 8 | session_id |
session identifier |
| 24 | 8 | start_ts_ns |
wall-clock start time |
| 32 | 8 | tsc0 |
TSC reading of the calibration anchor |
| 40 | 8 | tsc_ns0 |
wall-clock ns of that anchor |
| 48 | 8 | tsc_ns_per_cycle_q32 |
ns per TSC cycle, 32.32 fixed point |
| 56 | 8 | config_hash |
FNV-1a hash of the embedded configuration text |
| 64 | 8 | rng_seed |
the engine's RNG seed |
| 72 | 4 | message_version |
version of the message layouts |
| 76 | 4 | block_bytes |
maximum block size (1048576) |
| 80 | 32 | strategy |
strategy name, NUL-padded |
| 112 | 2 | session_epoch |
client order id epoch (v2) |
| 114 | 1 | quoting_enabled |
0 for a dry run behind a gateway; a dry run of its own quotes on paper (v2) |
| 115 | 1 | header_flags |
bit 0: the three session fields above are valid (v2); bit 1: the engine replaced quotes per venue (without it, a replay replaces only if every traded venue could, as the engine did then); bit 2: the session's venue feeds showed its own orders (a backtest with [backtest] own_orders_in_feed; a live session's always do) |
| 116 | 4 | config_bytes |
length of the configuration text; 0 = none (v2) |
| 120 | 8 | replace_venues |
bit v set when venue v traded with cancel-replace (v2) |
| 128 | 4 | config_crc32c |
CRC32C of the configuration text (v2) |
| 132 | 4 | param_count |
entries of the parameter table (v3) |
| 136 | 4 | param_table_bytes |
length of the parameter table; 0 = none (v3) |
| 140 | 4 | param_table_crc32c |
CRC32C of the parameter table (v3) |
| 144 | 4 | meta_bytes |
length of the strategy metadata; 0 = none (v3) |
| 148 | 4 | meta_crc32c |
CRC32C of the strategy metadata (v3) |
| 152 | 100 | reserved |
zero |
| 252 | 4 | crc32c |
CRC32C of bytes 0 to 251 |
The configuration is Config::effective_toml(): the configuration after command-line overrides (--strategy, --param) as deterministic TOML, without api_key, api_secret and api_passphrase, zero-padded to a multiple of 64 bytes. The instrument records are fastmm::Instrument (128 bytes).
The parameter table lists the strategy's parameters in schema order. Each entry is the type (uint8: 0 int, 1 double, 2 bool, 3 decimal, 4 bps, 5 ms), the name length (uint8) and the name; the table is zero-padded to a multiple of 64 bytes.
The strategy metadata is UTF-8 key=value lines, zero-padded to a multiple of 64 bytes. A Python strategy run live writes class (module:qualname), hot_source_sha256, fastmm, numba, llvmlite and python; fastmm.inspect_journal(path)["strategy_meta"] returns them as a dict.
Block header¶
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 4 | magic |
FMJB |
| 4 | 4 | byte_len |
payload bytes after this header |
| 8 | 8 | seq_first |
sequence number of the first message |
| 16 | 8 | seq_last |
sequence number of the last message |
| 24 | 4 | count |
messages in the block |
| 28 | 4 | crc32c |
CRC32C of the payload |
| 32 | 4 | flags |
bit 0: trailer |
| 36 | 28 | reserved |
zero |
Messages¶
Every message starts with the 64-byte EventHeader; its total length (len) is a multiple of 64.
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 4 | len |
message bytes including the header |
| 4 | 1 | type |
EventType |
| 5 | 1 | version |
message layout version |
| 6 | 1 | venue |
venue id |
| 7 | 1 | flags |
see below |
| 8 | 4 | instrument |
instrument id |
| 12 | 4 | reserved0 |
with kEngineTime: engine clock delta, signed int32 ns |
| 16 | 8 | seq |
journal sequence number, consumption order |
| 24 | 8 | venue_seq |
venue update id or sequence |
| 32 | 8 | exch_ts |
venue event time |
| 40 | 8 | recv_ts |
receive time |
| 48 | 8 | t0_cycles |
TSC at receive |
| 56 | 8 | t1_delta, t2_delta |
cycles to decode and to book apply, uint32 each |
| Flag bit | Name | Meaning |
|---|---|---|
| 0 | kSynthetic |
generated locally (a timer, a reconciliation fill, the simulator) |
| 1 | kReplayed |
read from a journal |
| 2 | kSnapshot |
a book message that carries a full snapshot |
| 3 | kOutbound |
a copy of an order message the engine sent (OutNewOrder, OutCancel, OutReplace) |
| 4 | kEngineTime |
a consumed event; reserved0 is the engine clock minus the previous engine-time record |
| 5 | kDropped |
an outbound copy the transport did not accept |
| 6, 7 | kTruncatedBids, kTruncatedAsks |
a book delta whose side kept only the levels nearest the touch of a longer venue update; a book drops its levels behind the last one carried |
Message layouts are the structs in include/fastmm/core/messages.hpp (EventType in core/enums.hpp): book snapshots and deltas, trades, book tickers, option tickers, L3 order adds, executions, cancels and replaces, order acks, rejects, cancel acks and rejects, fills, expiries, positions, timers, control commands, connection states, reconciliation records, latency samples, outbound orders, parameter updates, funding payments (FundingMsg, 128 bytes: the signed amount in the settlement asset at offset 64, the venue's funding id, the asset, and flag bit 0 when it came from the venue's history) balances (BalanceMsg, 128 bytes: free, locked, total, equity and maintenance at offsets 64 to 96, the asset at 104, flags at 113: bit 0 snapshot, bit 1 snapshot end, bit 2 account-wide margin) and a venue's mark, index and funding (PerpStateMsg, 128 bytes: mark and index price at 64 and 72, the funding rate (f64, per interval) at 80, the interval (ns) at 88, the next funding time at 96, open interest at 104, and at 112 the fields it carries: bit 0 mark, bit 1 index, bit 2 funding, bit 3 open interest).
Engine clock¶
- Every consumed event and every fired timer (a synthetic
Timermessage) carrieskEngineTimeand a delta from the previous such record. - An
EngineTimemessage (128 bytes) holds the absolute engine clock inengine_ts(offset 64) withkind(offset 72): 0Syncbefore an event whose delta does not fit in int32 ns (a gap of more than 2.1 s) or whose predecessor was lost, 1Start, 2Finish. - The engine clock may step backwards (TSC recalibration); replay sets the simulated clock to it.
- Sequence numbers count
EngineTimerecords too. - A
Timermessage with byte 68 (engine) set to 1 is the engine'smax_param_age_msdeadline, not a strategy timer.
Strategy state¶
A StrategyState message (type 30, 128 bytes followed by its bytes, padded to 64) records what a replay or a backtest can start from. It is never dispatched to the engine.
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 64 | 8 | id |
the n-th state the session wrote; the records of one state share it |
| 72 | 4 | total |
bytes of the part |
| 76 | 4 | offset |
where this record's bytes go in the part |
| 80 | 4 | bytes |
bytes after the 128-byte head, at most 16384 |
| 84 | 1 | kind |
0 Restored: the bytes the strategy was handed to restore() at the start; 1 Snapshot: its state() |
| 85 | 1 | part |
0 the strategy's bytes; 1 positions, 24 bytes each: instrument id (u32), venue (u8), 3 pad, quantity and average price (fixed point, i64) |
| 86 | 1 | flags |
bit 0 (Restored): the strategy took the bytes |
The engine writes the Restored state (its strategy part only) once, after on_start, when [strategy] state_file held something, and with [strategy] state_snapshot_interval_s a Snapshot of both parts that often (Configuration). recv_ts is the engine time it was taken at.
Parameter updates¶
A ParamUpdate message (448 bytes) is an engine input with new strategy parameter values; the engine assigns all of them at that event.
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 8 | 4 | instrument |
the target instrument; 0xFFFFFFFF for all instruments |
| 64 | 4 | count |
pairs used, at most 32 |
| 68 | 1 | origin |
0 the strategy's own publisher (or not recorded), 1 the control socket (fastmm-ctl param) |
| 72 | 8 | publish_seq |
the publisher's sequence number, from 1 |
| 80 | 64 | field |
uint16 each: the parameter's index in the parameter table |
| 144 | 40 | source |
who asked for it (fastmm-ctl param --source), NUL-terminated; empty when not given |
| 192 | 256 | value |
int64 each: the raw value |
origin and source sit in bytes that were padding before, so a journal written before them reads with origin 0 and an empty source.
A raw value is the value of an int or bool, raw fixed point for decimal and bps (1 bp = 10000), nanoseconds for ms and the IEEE-754 bits of a double. Replay maps each field through the parameter table to the replaying strategy's parameter with the same name and type, and drops a field that has none.
Tools¶
python3 tools/journal_dump.py <file.fmj> [--first 20] [--type OrderFill] [--no-crc]prints the header, instruments, parameter table, events and a count per type.python3 tools/pnl_report.py <file.fmj>computes fills, fees and PnL (Journals, replay and PnL).fastmm-replay --journal <file.fmj> --verifyreplays it (Determinism);--allow-incompletereplays a file the writer never closed.fastmm-pnlandfastmm.open_store()answer the same questions from the store, which is written alongside the journal and needs no replay.