.floxrun strategy-trace format specification¶
Version 1.0, frozen 2026-05-08. This is the on-disk format flox uses to record what a strategy did during a run: the signals it produced, the orders it submitted, and the fills it received. Tape (.floxlog) records what came in from the exchange; this format records what the strategy emitted in response. The two are complementary and live side by side.
The spec is published so third-party tooling can read and write trace artifacts without depending on flox itself.
At a glance¶
A .floxrun is a directory of segment files plus a manifest. Each segment holds one record type (signals, order events, or fills) as length-prefixed CRC-checked frames. Frames carry both a wall-clock timestamp (when the strategy emitted the event) and a feed timestamp (the source tape event that triggered the decision), so a reader can line trace events up against any of the consumed tapes without guessing.
Everything is little-endian. All structures are 8-byte aligned. CRC32 uses the standard reflected polynomial 0xEDB88320 (ISO 3309). The on-disk layout, segment-header shape, frame-header shape, and CRC are intentionally identical to .floxlog so existing reader infrastructure ports over with minimal change.
Why a separate format¶
Tape is per-feed: one symbol from one exchange, one stream of trades and book updates as they arrived from the wire. A strategy run is per-run: it consumes N tapes, emits decisions that may span several symbols, and produces orders and fills that don't belong to any one feed.
Putting trace inside .floxlog was rejected for three reasons:
- A multi-symbol decision (for example, "BTC/ETH ratio crossed threshold, short BTC and long ETH") doesn't belong to either input tape. Writing it into one would lie about its scope; writing it into both would duplicate it.
- Tape is exchange-portable. A Bitget BTC tape should replay against any strategy. Mixing one strategy's signals into the tape couples them.
- Tape is a deterministic input format. Replay tooling should not have to filter a "trace" subtype out before feeding the strategy.
The trace lives alongside the tape, and the run's manifest references the tapes it was produced from.
Layout on disk¶
manifest.json is the index. It carries strategy identity, the list of consumed tapes with content hashes, the run's wall-clock window, and per-segment metadata. Segments are independently parseable; the manifest is an index, not a requirement.
Per-type segment files mean a tool that only cares about, say, fills can read just the fills segment without paying for signal decoding.
Magic numbers and constants¶
| Constant | Value | Meaning |
|---|---|---|
MAGIC_RUN_SEGMENT |
0x4E555246 ("FRUN") |
Run-segment header sentinel. |
FORMAT_VERSION |
1 |
Bump triggers a new .floxrun major. |
The frame-header magic, CRC algorithm, and compressed-block header (when used) are reused verbatim from .floxlog.
Run-segment file¶
A segment is a RunSegmentHeader followed by a stream of frames. Frames carry one record per the segment's record type. Compression is supported via the same CompressedBlock shape as .floxlog.
RunSegmentHeader (64 bytes, 8-byte aligned)¶
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 4 | magic |
MAGIC_RUN_SEGMENT. |
| 4 | 2 | version |
1. |
| 6 | 1 | flags |
Bitfield. See below. |
| 7 | 1 | record_kind |
1 Signal, 2 OrderEvent, 3 Fill. |
| 8 | 8 | created_ns |
Wall-clock nanoseconds when the segment was opened. |
| 16 | 8 | first_event_ns |
Earliest run_ts_ns in segment. |
| 24 | 8 | last_event_ns |
Latest run_ts_ns in segment. |
| 32 | 4 | event_count |
Total events written. |
| 36 | 4 | _pad0 |
Zero. |
| 40 | 8 | index_offset |
Byte offset of the index trailer; 0 if absent. |
| 48 | 1 | compression |
0 none, 1 LZ4. |
| 49 | 15 | reserved[15] |
Zero-filled. |
A reader that sees an unknown record_kind must reject the segment.
RunSegmentFlags¶
| Bit | Name | Meaning |
|---|---|---|
0x01 |
HasIndex |
Sparse index trailer present. Same shape as .floxlog. |
0x02 |
Compressed |
Frame stream partitioned into CompressedBlocks. |
0x04 |
Sorted |
Writer guarantees run_ts_ns is monotonically non-decreasing. |
Frame stream¶
A frame is the standard 12-byte FrameHeader (the same struct .floxlog uses) followed by size bytes of payload. Payload meaning depends on type:
type = 10→SignalRecordfollowed bypayload_lenbytestype = 11→OrderEventRecordfollowed bypayload_lenbytestype = 12→FillRecord(fixed-size, no trailing payload)
A run-segment file holds frames of one record_kind only. Mixing kinds within a segment is not allowed.
The FrameHeader from .floxlog is reused exactly:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | size |
| 4 | 4 | crc32 |
| 8 | 1 | type |
| 9 | 1 | rec_version |
| 10 | 2 | flags |
type for run frames lives in [10, 12] to leave space for tape frame types in [1, 9].
SignalRecord (48 bytes fixed + variable payload, 8-byte aligned)¶
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 8 | run_ts_ns |
Wall-clock nanoseconds when the strategy emitted the signal. |
| 8 | 8 | feed_ts_ns |
Timestamp of the tape event that triggered the decision; 0 if not applicable. |
| 16 | 4 | signal_id |
Numeric id from the strategy's signal registry. |
| 20 | 2 | name_len |
Bytes of UTF-8 signal name in the payload. |
| 22 | 2 | symbol_count |
Number of symbol_id entries in the payload. |
| 24 | 4 | payload_len |
Bytes of caller-defined payload after the trailing fields. |
| 28 | 4 | flags |
Bit 0x01 = decision is "enter", 0x02 = "exit", 0x04 = "rebalance". |
| 32 | 8 | strength_raw |
Signal strength, fixed-point with scale 1e8. |
| 40 | 8 | _reserved |
Zero. |
Followed in order by:
name_lenbytes of UTF-8 signal name (no terminator).symbol_count×uint32_t(4 bytes each) symbol ids.payload_lenbytes of caller-defined payload (typically a JSON blob; opaque to the format).- Padding to align the frame end to 8 bytes.
A single signal can carry several symbol ids because cross-symbol decisions are first-class. A pair-trade signal lists both symbols; a single-symbol signal lists one.
OrderEventRecord (64 bytes fixed + optional payload, 8-byte aligned)¶
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 8 | run_ts_ns |
Wall-clock when the event was emitted. |
| 8 | 8 | feed_ts_ns |
Triggering tape event timestamp; 0 if not applicable. |
| 16 | 8 | order_id |
Numeric order id from the executor's registry. |
| 24 | 8 | parent_signal_id |
Signal that produced this event; 0 if unrooted. |
| 32 | 8 | price_raw |
Fixed-point price, scale 1e8. 0 if not applicable. |
| 40 | 8 | qty_raw |
Fixed-point quantity, scale 1e8. |
| 48 | 4 | symbol_id |
|
| 52 | 1 | event_kind |
1 submit, 2 cancel, 3 modify, 4 ack, 5 reject, 6 expire. |
| 53 | 1 | side |
0 buy, 1 sell. |
| 54 | 1 | order_type |
0 market, 1 limit, 2 stop, 3 stop-limit. |
| 55 | 1 | _pad0 |
Zero. |
| 56 | 4 | reason_len |
Bytes of UTF-8 reason text in the payload (e.g. for reject). |
| 60 | 4 | flags |
Bit 0x01 = post-only, 0x02 = reduce-only, 0x04 = ioc. |
Followed by reason_len bytes of UTF-8 reason text and padding to 8-byte alignment.
FillRecord (64 bytes, 8-byte aligned, no payload)¶
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 8 | run_ts_ns |
Wall-clock when the fill was processed. |
| 8 | 8 | feed_ts_ns |
Triggering trade tape event; 0 if not applicable. |
| 16 | 8 | order_id |
Order this fill belongs to. |
| 24 | 8 | fill_id |
Exchange fill id; 0 if unknown. |
| 32 | 8 | price_raw |
Fixed-point price. |
| 40 | 8 | qty_raw |
Fixed-point quantity filled. |
| 48 | 8 | fee_raw |
Fixed-point fee in fee currency, scale 1e8. |
| 56 | 4 | symbol_id |
|
| 60 | 1 | side |
0 buy, 1 sell. |
| 61 | 1 | liquidity |
0 unknown, 1 maker, 2 taker. |
| 62 | 2 | _pad0 |
Zero. |
Sparse index¶
When flags.HasIndex is set, the RunSegmentHeader.index_offset points at a trailer whose layout is identical to .floxlog's SegmentIndexHeader followed by IndexEntry rows keyed by run_ts_ns.
Manifest¶
manifest.json is one JSON object:
{
"schema_version": 1,
"format_version": 1,
"strategy_id": "moving-average-cross",
"strategy_hash": "sha256:abc123...",
"run_started_ns": 1714123456000000000,
"run_ended_ns": 1714123556000000000,
"tape_refs": [
{
"path": "BTCUSDT.floxlog",
"content_hash": "sha256:def456...",
"first_event_ns": 1714123456000000000,
"last_event_ns": 1714123556000000000
}
],
"segments": [
{
"name": "signals-000000.bin",
"record_kind": "signals",
"size_bytes": 12345,
"first_event_ns": 1714123456000000000,
"last_event_ns": 1714123556000000000,
"event_count": 42
},
{ "name": "orders-000000.bin", "record_kind": "orders", "size_bytes": 6789, ... },
{ "name": "fills-000000.bin", "record_kind": "fills", "size_bytes": 4321, ... }
]
}
tape_refs[].path is relative to the .floxrun directory. A tooling layer that wants to verify that the run was produced against a specific tape compares content_hash against a hash of the referenced .floxlog directory.
strategy_hash is the hash of the strategy source plus its build inputs. A reader can use it to detect that two runs claiming the same strategy_id actually used different code.
Reader contract¶
A reader walks each segment file in isolation. The order of records inside a segment is defined by run_ts_ns. Across segments, a tool that wants a unified timeline merges by run_ts_ns. A tool that wants to align trace events with tape events merges trace records with tape records by feed_ts_ns.
A frame whose type is outside [10, 12] must be skipped. A frame whose rec_version is unknown must be rejected. A segment whose record_kind doesn't match the frame type it carries must be rejected.
Versioning¶
format_version = 1 is frozen. Adding a new record kind, a new flag, or a new field requires a minor bump and an explicit description of the backward-compatibility rule. Removing a field or changing a layout requires a major bump.
The trace format and the tape format version independently. A .floxrun 1.0 can reference a .floxlog 1.0 or a future .floxlog 1.1; the reader checks the version of each artifact separately.
Bundle integration¶
flox bundle pack produces a tarball containing strategy source, the consumed .floxlog directories, and a .floxrun directory. flox bundle replay extracts the tarball, replays the strategy against the included tapes, and compares the new .floxrun against the bundled one as the actual-vs-expected diff.
The bundle's outer layout is: