🦀 miniFIX Tutorial | Part 04

September 18, 2025 by CryptoPatrick Rust Tutorials

Custom FIX Specifications and Code Generation with miniFIX

Introduction

While standard FIX specifications (4.0, 4.1, 4.2, 4.4, etc.) cover most trading scenarios, many organizations need custom fields and message types. Exchanges like Coinbase often extend FIX with proprietary fields for their specific business needs. miniFIX supports custom specifications through QuickFIX XML format and provides code generation capabilities to create type-safe Rust definitions.

What You’ll Learn

  • How to define custom FIX specifications using QuickFIX XML
  • Loading custom dictionaries at runtime
  • Code generation for type-safe custom fields
  • Working with exchange-specific FIX extensions

The Problem: Exchange-Specific Fields

Standard FIX doesn’t cover all the fields that modern exchanges need. For example, Coinbase might need:

  • TradeID (1003): Unique identifier for each trade
  • AggressorIndicator (1057): Whether the trade was buyer or seller initiated
  • Custom message types for cryptocurrency-specific operations

QuickFIX XML Specification

miniFIX uses QuickFIX XML format to define custom specifications. Here’s how you might define Coinbase-specific fields:

<fix major="4" minor="2" servicepack="0">
  <header>
    <field name="BeginString" required="Y"/>
    <field name="BodyLength" required="Y"/>
    <field name="MsgType" required="Y"/>
    <!-- Standard header fields -->
  </header>
  
  <messages>
    <message name="ExecutionReport" msgtype="8" msgcat="app">
      <field name="TradeID" required="N"/>
      <field name="AggressorIndicator" required="N"/>
      <!-- Other fields -->
    </message>
  </messages>
  
  <fields>
    <field number="1003" name="TradeID" type="STRING"/>
    <field number="1057" name="AggressorIndicator" type="BOOLEAN"/>
    <!-- Standard fields -->
  </fields>
  
  <trailer>
    <field name="CheckSum" required="Y"/>
  </trailer>
</fix>

Loading Custom Specifications

miniFIX can load custom specifications at runtime from XML:

use minifix::prelude::*;
use minifix::tagvalue::Decoder;

const COINBASE_QUICKFIX_SPEC: &str = include_str!("coinbase_quickfix.xml");

fn create_coinbase_decoder() -> Decoder {
    let fix_dictionary = Dictionary::from_quickfix_spec(COINBASE_QUICKFIX_SPEC)
        .expect("Failed to load Coinbase FIX specification");
    Decoder::new(fix_dictionary)
}

Code Generation for Type Safety

For better type safety and IDE support, you can generate Rust code from your custom specification. The build script handles this:

// build.rs
use minifix::Dictionary;
use std::fs::File;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let quickfix_spec = include_str!("src/coinbase_quickfix.xml");
    let dictionary = Dictionary::from_quickfix_spec(quickfix_spec)
        .expect("Failed to parse QuickFIX spec");
    
    let settings = minifix::codegen::Settings::default();
    let rust_code = minifix::codegen::gen_definitions(&dictionary, &settings);
    
    let path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap())
        .join("coinbase_fields.rs");
    let mut file = File::create(path)?;
    file.write_all(rust_code.as_bytes())?;
    
    Ok(())
}

Using Generated Code

The generated code provides type-safe access to your custom fields:

// Include the generated code
#[allow(dead_code)]
#[rustfmt::skip]
mod gdax;  // Generated from coinbase_quickfix.xml

use minifix::prelude::*;
use minifix::tagvalue::Decoder;

const FIX_MESSAGE_EXEC_REPORT: &[u8] = 
    b"8=FIX.4.2|9=21|35=8|1003=123|1057=Y|10=090|";

fn main() {
    let mut decoder = create_coinbase_decoder();
    decoder.config_mut().separator = b'|';
    
    let msg = decoder
        .decode(FIX_MESSAGE_EXEC_REPORT)
        .expect("Invalid FIX message");
    
    // Type-safe access to standard fields
    assert_eq!(msg.get(gdax::BEGIN_STRING), Ok(gdax::BeginString::Fix42));
    assert_eq!(msg.get(gdax::MSG_TYPE), Ok(gdax::MsgType::ExecutionReport));
    
    // Type-safe access to custom fields
    assert_eq!(msg.get(gdax::TRADE_ID), Ok("123"));
    assert_eq!(msg.get(gdax::AGGRESSOR_INDICATOR), Ok(true));
}

Benefits of Code Generation

  1. Compile-time Safety: Invalid field access caught at compile time
  2. IDE Support: Auto-completion and documentation for all fields
  3. Type Conversion: Automatic conversion between string and native types
  4. Enum Values: Type-safe enums for fields with limited values
  5. Documentation: Generated docs include field descriptions

Advanced Customization

Custom Data Types

You can define custom validation and conversion for specific field types:

<fields>
  <field number="1003" name="TradeID" type="STRING">
    <description>Unique trade identifier</description>
  </field>
  <field number="9001" name="CryptoAmount" type="AMT">
    <description>Amount in cryptocurrency units</description>
  </field>
</fields>

Message Validation

Custom specifications can include validation rules:

<message name="NewOrderSingle" msgtype="D" msgcat="app">
  <field name="Symbol" required="Y"/>
  <field name="Side" required="Y"/>
  <field name="OrderQty" required="Y"/>
  <field name="CryptoAmount" required="N"/>
  <group name="NoAllocs" required="N">
    <field name="AllocAccount" required="Y"/>
    <field name="AllocQty" required="Y"/>
  </group>
</message>

Complete Example

Here’s a complete example using a custom Coinbase specification:

#[allow(dead_code)]
#[rustfmt::skip]
mod coinbase;

use minifix::prelude::*;
use minifix::tagvalue::Decoder;

const COINBASE_SPEC: &str = r#"
<fix major="4" minor="2">
  <fields>
    <field number="8" name="BeginString" type="STRING"/>
    <field number="35" name="MsgType" type="STRING"/>
    <field number="1003" name="TradeID" type="STRING"/>
    <field number="1057" name="AggressorIndicator" type="BOOLEAN"/>
  </fields>
</fix>
"#;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load custom specification
    let dictionary = Dictionary::from_quickfix_spec(COINBASE_SPEC)?;
    let mut decoder = Decoder::new(dictionary);
    decoder.config_mut().separator = b'|';
    
    // Sample message with custom fields
    let message = b"8=FIX.4.2|35=8|1003=TRADE123|1057=Y|10=123|";
    let msg = decoder.decode(message)?;
    
    // Process standard and custom fields
    println!("Begin String: {:?}", 
        String::from_utf8_lossy(msg.get(8)?));
    println!("Message Type: {:?}", 
        String::from_utf8_lossy(msg.get(35)?));
    println!("Trade ID: {:?}", 
        String::from_utf8_lossy(msg.get(1003)?));
    println!("Aggressor: {:?}", 
        String::from_utf8_lossy(msg.get(1057)?));
    
    Ok(())
}

Production Considerations

Version Management

Keep track of specification versions:

struct CoinbaseSpec {
    version: &'static str,
    spec: &'static str,
}

const COINBASE_V1: CoinbaseSpec = CoinbaseSpec {
    version: "1.0",
    spec: include_str!("coinbase_v1.xml"),
};

const COINBASE_V2: CoinbaseSpec = CoinbaseSpec {
    version: "2.0", 
    spec: include_str!("coinbase_v2.xml"),
};

Error Handling

Robust error handling for custom specs:

fn load_custom_dictionary(spec: &str) -> Result<Dictionary, Box<dyn std::error::Error>> {
    Dictionary::from_quickfix_spec(spec)
        .map_err(|e| format!("Failed to load custom FIX spec: {}", e).into())
}

Testing Custom Fields

Comprehensive testing for custom specifications:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_custom_fields() {
        let mut decoder = create_coinbase_decoder();
        let message = b"8=FIX.4.2|35=8|1003=TEST123|1057=N|10=090|";
        
        let msg = decoder.decode(message).unwrap();
        assert_eq!(msg.get(1003), Ok(b"TEST123"));
        assert_eq!(msg.get(1057), Ok(b"N"));
    }
}

Best Practices

  1. Document Extensions: Clearly document all custom fields and their usage
  2. Version Control: Track specification changes and maintain backward compatibility
  3. Validation: Implement thorough validation for custom fields
  4. Testing: Test custom fields extensively, including edge cases
  5. Performance: Consider the performance impact of complex custom validations

Next Steps

Custom FIX specifications enable you to:

  • Support exchange-specific protocols
  • Implement proprietary trading algorithms
  • Create specialized financial instruments
  • Build custom reporting and compliance systems

miniFIX’s flexibility with custom specifications makes it suitable for any FIX-based financial application, from simple order routing to complex multi-asset trading platforms.


Next: Part 05 - JSON Format Conversion

'I write to understand as much as to be understood.' —Elie Wiesel
(c) 2024 CryptoPatrick