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 tradeAggressorIndicator(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
- Compile-time Safety: Invalid field access caught at compile time
- IDE Support: Auto-completion and documentation for all fields
- Type Conversion: Automatic conversion between string and native types
- Enum Values: Type-safe enums for fields with limited values
- 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
- Document Extensions: Clearly document all custom fields and their usage
- Version Control: Track specification changes and maintain backward compatibility
- Validation: Implement thorough validation for custom fields
- Testing: Test custom fields extensively, including edge cases
- 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.