fix-codec-rs 0.1.0

High-performance FIX protocol encoder/decoder for low-latency trading systems
Documentation
  • Coverage
  • 8.7%
    94 out of 1080 items documented0 out of 33 items with examples
  • Size
  • Source code size: 192.02 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 27.12 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 21s Average build duration of successful builds.
  • all releases: 21s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • ledongthuc/fix-codec-rs
    11 1 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • ledongthuc

fix-codec-rs

A high-performance FIX (Financial Information Exchange) protocol encoder/decoder library written in Rust, designed for HFT and low-latency trading systems.

We target to encode and decode level, no session or application level for this protocol.

Tested with FIX version 4.2 and 4.4

Features

  • Zero-copy decoding — field values are byte slices into the original buffer, no allocation on the hot path
  • Reusable decoder/encoder — single instance across thousands of messages, amortizes allocation cost
  • SmallVec inline storage — 95%+ of messages fit in inline stack storage (32-field default), avoiding heap allocation entirely
  • SIMD-accelerated scanning — uses memchr for fast = and SOH delimiter search
  • Lazy sorted index — O(log n) find() via binary search, built only on first use
  • Repeating groups — full support for nested groups, both FIX 4.2 and FIX 4.4 specifications
  • Auto checksum/body length — automatic tag 9 and tag 10 computation during encoding (toggleable)
  • 500+ tag constants — comprehensive coverage of FIX 4.2 and FIX 4.4 tag definitions

Installation

Add to your Cargo.toml:

[dependencies]
fix-codec-rs = { git = "https://github.com/ledongthuc/fix-codec-rs" }

Usage

Decoding

use fix_codec_rs::decoder::Decoder;
use fix_codec_rs::tag;

fn main() {
    // Create a reusable decoder — allocate once, reuse across messages
    let mut decoder = Decoder::new();

    let raw = b"8=FIX.4.2\x019=73\x0135=D\x0149=CLIENT\x0156=BROKER\x0134=1\x0152=20240101-12:00:00\x0111=ORD001\x0155=AAPL\x0154=1\x0138=100\x0144=150.00\x0140=2\x0110=128\x01";

    let msg = decoder.decode(raw).unwrap();

    // Access fields by index (O(1))
    for field in msg.fields() {
        println!("Tag {}: {:?}", field.tag, field.value);
    }

    // Lookup by tag (O(log n) binary search, index built lazily on first call)
    if let Some(field) = msg.find(tag::SYMBOL) {
        println!("Symbol: {}", std::str::from_utf8(field.value).unwrap());
    }

    if let Some(field) = msg.find(tag::ORDER_QTY) {
        println!("Qty: {}", std::str::from_utf8(field.value).unwrap());
    }
}

Decoding with Validation

use fix_codec_rs::decoder::Decoder;

let mut decoder = Decoder::new();
let raw = b"8=FIX.4.2\x019=73\x0135=D\x0149=CLIENT\x0156=BROKER\x0134=1\x0152=20240101-12:00:00\x0111=ORD001\x0155=AAPL\x0154=1\x0138=100\x0144=150.00\x0140=2\x0110=128\x01";

let msg = decoder.decode(raw).unwrap();

// Validate body length (tag 9) and checksum (tag 10)
msg.validate_body_length().unwrap();
msg.validate_checksum().unwrap();

Encoding

use fix_codec_rs::decoder::Decoder;
use fix_codec_rs::encoder::Encoder;

let mut decoder = Decoder::new();
let mut encoder = Encoder::new();

let raw = b"8=FIX.4.2\x019=73\x0135=D\x0149=CLIENT\x0156=BROKER\x0134=1\x0152=20240101-12:00:00\x0111=ORD001\x0155=AAPL\x0154=1\x0138=100\x0144=150.00\x0140=2\x0110=128\x01";

let msg = decoder.decode(raw).unwrap();

// Encode back to wire format — tag 9 and tag 10 are recomputed automatically
let mut out = Vec::new();
encoder.encode(&msg, &mut out).unwrap();

Encoding with Auto-Calculation Disabled

use fix_codec_rs::encoder::Encoder;

let mut encoder = Encoder::new();

// Preserve original tag 9 and tag 10 values without recomputing
encoder.disable_auto_calculate_body_length(true);
encoder.disable_auto_calculate_checksum(true);

Pre-sizing for Large Messages

use fix_codec_rs::decoder::Decoder;
use fix_codec_rs::encoder::Encoder;

// Pre-allocate for messages with up to 64 fields (avoids reallocation)
let mut decoder = Decoder::with_capacity(64);

// Pre-allocate 1024-byte output buffer
let mut encoder = Encoder::with_capacity(1024);

Repeating Groups

use fix_codec_rs::decoder::Decoder;
use fix_codec_rs::group;
use fix_codec_rs::tag;

let mut decoder = Decoder::new();

// Market data snapshot with 2 MD entries
let raw = b"8=FIX.4.2\x019=100\x0135=W\x0149=SERVER\x0156=CLIENT\x01268=2\x01269=0\x01270=150.25\x01271=500\x01269=1\x01270=150.30\x01271=300\x0110=200\x01";

let msg = decoder.decode(raw).unwrap();

// Iterate MD entries using the built-in FIX 4.2 group spec
for entry in msg.groups(&group::fix42::MD_ENTRIES) {
    if let Some(price) = entry.find(tag::MD_ENTRY_PX) {
        println!("Price: {}", std::str::from_utf8(price.value).unwrap());
    }
    if let Some(size) = entry.find(tag::MD_ENTRY_SIZE) {
        println!("Size: {}", std::str::from_utf8(size.value).unwrap());
    }
}

Nested Groups

use fix_codec_rs::decoder::Decoder;
use fix_codec_rs::group;
use fix_codec_rs::tag;

let mut decoder = Decoder::new();
let raw = b"8=FIX.4.4\x019=...\x0135=AE\x01453=2\x01448=FIRM_A\x01447=D\x01452=1\x01539=1\x01524=TRADER1\x01448=FIRM_B\x01447=D\x01452=2\x0110=000\x01";

let msg = decoder.decode(raw).unwrap();

for party in msg.groups(&group::fix44::PARTY_IDS) {
    if let Some(id) = party.find(tag::PARTY_ID) {
        println!("Party: {}", std::str::from_utf8(id.value).unwrap());
    }
    // Access nested group within each party
    for nested in party.groups(&group::fix44::NESTED_PARTY_IDS) {
        if let Some(nid) = nested.find(tag::NESTED_PARTY_ID) {
            println!("  Nested: {}", std::str::from_utf8(nid.value).unwrap());
        }
    }
}

Custom Group Spec

use fix_codec_rs::group::GroupSpec;
use fix_codec_rs::tag;

// Define a custom repeating group
const MY_GROUP: GroupSpec = GroupSpec {
    count_tag: tag::NO_ALLOCS,      // Tag that holds the group count
    delimiter_tag: tag::ALLOC_ACCOUNT, // First tag of each instance
    member_tags: &[
        tag::ALLOC_ACCOUNT,
        tag::ALLOC_SHARES,
        tag::ALLOC_PRICE,
    ],
};

Benchmark

Benchmarks run with Criterion.rs on Apple M-series (arm64). Run your own with cargo bench.

Decode throughput

Message Fields Size Throughput
Tiny (1 field) 1 ~9 B ~600 ns/msg
New Order Single 8 ~73 B ~800 ns/msg
Execution Report 12 ~104 B ~1.0 µs/msg
Market Data Snapshot 20 ~100 B ~1.1 µs/msg

find() strategy: binary search vs linear scan

Strategy 1 lookup 4 lookups 8 lookups
Binary search lower lower lower
Linear scan lower higher higher

Binary search (via lazy sorted index) is the default. Break-even point is typically around 2–3 lookups per message.

Encode throughput

Message Throughput
New Order Single ~700 ns/msg
Execution Report ~900 ns/msg

Roundtrip (decode + encode)

Message Throughput
New Order Single ~1.5 µs/msg
Market Data Snapshot ~2.0 µs/msg

Run full benchmarks:

cargo bench
# Open HTML report
open target/criterion/report/index.html

Design Notes

Zero-copyMessage<'a> and Group<'a> hold references into the original input buffer. No string copies. Values are &[u8] slices; callers parse numeric/string values as needed.

Reusable decoderDecoder holds a SmallVec internally. Reuse the same instance across messages to avoid repeated allocation. The decoder clears internal state on each decode() call.

SmallVec inline storage — field offset storage fits 32 entries inline on the stack. Messages with more than 32 fields spill to the heap automatically.

Lazy sorted indexMessage::find() builds a sorted tag index on first call using OnceCell. Subsequent find() calls on the same message use binary search. If you only iterate with fields(), no sort ever happens.

Group specs are 'static — built-in GroupSpec values reference static tag slices. Zero overhead at runtime.

Supported FIX Versions

Version Tag Coverage Group Specs
FIX 4.2 450+ tags 19 groups
FIX 4.4 500+ tags 37 groups

Tag constants are in fix_codec_rs::tag. Group specs are in fix_codec_rs::group::fix42 and fix_codec_rs::group::fix44.

Dev Setup

Prerequisites

Clone and build

git clone https://github.com/ledongthuc/fix-codec-rs
cd fix-codec-rs
cargo build

Run tests

cargo test

Run benchmarks

cargo bench

Criterion generates an HTML report at target/criterion/report/index.html.

Contributing

Contributions are welcome. Please follow the process below:

  1. Fork the repository and create a feature branch from main.
  2. Write tests for any new behavior. All existing tests must pass.
  3. Run cargo clippy -- -D warnings and cargo fmt before submitting.
  4. Open a pull request with a clear description of the change and motivation.

For bug reports, open a GitHub issue with a minimal reproducing example — ideally a failing test case.

License

MIT