Encoding FIX Messages with miniFIX: Creating New Order Singles
Introduction
While decoding FIX messages is essential for processing incoming data, encoding messages is equally important for sending orders, confirmations, and other trading instructions. miniFIX provides a powerful, type-safe encoder that makes creating FIX messages straightforward while maintaining the performance and reliability needed for financial applications.
What You’ll Learn
- How to set up a FIX message encoder
- Creating structured messages with proper headers and trailers
- Working with different data types (strings, integers, decimals, enums)
- Using high-precision decimal types for financial calculations
- Best practices for message construction
Setting Up the Encoder
Let’s start by creating a basic FIX encoder and constructing a New Order Single message:
use decimal::d128;
use minifix::prelude::*;
use minifix::tagvalue::Encoder;
use rust_decimal_macros::dec;
fn main() {
let mut encoder = Encoder::default();
let mut buffer = Vec::new();
// Start building a FIX 4.4 ExecutionReport message
let mut msg = encoder.start_message(b"FIX.4.4", &mut buffer, b"D");
// The message builder is now ready for field assignment
}
The start_message() method creates a message builder that handles:
- Proper FIX header construction
- Automatic body length calculation
- Checksum computation
- Field ordering according to FIX specifications
Building a New Order Single
Let’s construct a complete New Order Single (35=D) message with all the essential fields:
use decimal::d128;
use minifix::prelude::*;
use minifix::tagvalue::Encoder;
use rust_decimal_macros::dec;
fn create_new_order_single() {
let mut encoder = Encoder::default();
let mut buffer = Vec::new();
let mut msg = encoder.start_message(b"FIX.4.4", &mut buffer, b"D");
// Standard header fields
msg.set(fix44::MSG_SEQ_NUM, 215);
msg.set(fix44::SENDER_COMP_ID, "CLIENT12");
msg.set(fix44::TARGET_COMP_ID, "BROKER_SYS");
// Order identification
msg.set(fix44::ACCOUNT, "Marcel");
msg.set(fix44::CL_ORD_ID, "13346");
// Order instructions
msg.set(
fix44::HANDL_INST,
fix44::HandlInst::AutomatedExecutionOrderPrivateNoBrokerIntervention,
);
msg.set(fix44::ORD_TYPE, fix44::OrdType::Limit);
msg.set(fix44::SIDE, fix44::Side::Buy);
msg.set(fix44::TIME_IN_FORCE, fix44::TimeInForce::Day);
// Financial details with high precision
msg.set(fix44::PRICE, dec!(150.08)); // Using rust_decimal
msg.set(fix44::PRICE_DELTA, d128!(32.99)); // Using decimal128
// Complete the message
let (encoded_message, _) = msg.done();
println!("Encoded FIX message:");
println!("{}", String::from_utf8_lossy(encoded_message));
}
Working with Different Data Types
miniFIX’s encoder supports various data types with automatic conversion:
String Fields
// Static strings
msg.set(fix44::SENDER_COMP_ID, "CLIENT12");
// Dynamic strings
let order_id = format!("ORDER_{}", chrono::Utc::now().timestamp());
msg.set(fix44::CL_ORD_ID, order_id);
// Byte arrays
msg.set(fix44::SENDER_COMP_ID, b"CLIENT12");
Numeric Fields
// Integers
msg.set(fix44::MSG_SEQ_NUM, 215_u32);
msg.set(fix44::ORDER_QTY, 1000_i64);
// Floating point (not recommended for prices)
msg.set(fix44::SOME_FIELD, 123.45_f64);
Financial Decimals
For financial applications, precise decimal arithmetic is crucial:
use rust_decimal_macros::dec;
use decimal::d128;
// Using rust_decimal (recommended for most cases)
msg.set(fix44::PRICE, dec!(150.08));
msg.set(fix44::ORDER_QTY, dec!(1000));
// Using decimal128 for extreme precision
msg.set(fix44::PRICE_DELTA, d128!(32.99));
Enum Fields
miniFIX provides type-safe enums for standardized field values:
// Order side
msg.set(fix44::SIDE, fix44::Side::Buy);
// or
msg.set(fix44::SIDE, fix44::Side::Sell);
// Order type
msg.set(fix44::ORD_TYPE, fix44::OrdType::Limit);
msg.set(fix44::ORD_TYPE, fix44::OrdType::Market);
// Time in force
msg.set(fix44::TIME_IN_FORCE, fix44::TimeInForce::Day);
msg.set(fix44::TIME_IN_FORCE, fix44::TimeInForce::ImmediateOrCancel);
Advanced Message Construction
Timestamps
use chrono::{DateTime, Utc};
let now = Utc::now();
let timestamp = now.format("%Y%m%d-%H:%M:%S%.3f").to_string();
msg.set(fix44::TRANSACT_TIME, timestamp);
// Or using miniFIX timestamp utilities
msg.set(fix44::TRANSACT_TIME, minifix::utils::current_timestamp());
Conditional Fields
// Add fields conditionally
if order_type == OrderType::Limit {
msg.set(fix44::PRICE, limit_price);
}
if let Some(stop_price) = stop_price {
msg.set(fix44::STOP_PX, stop_price);
}
Repeating Groups
// TODO: Example of encoding repeating groups
// This would show how to encode groups like NoPartyIDs, NoAllocs, etc.
Complete Production Example
Here’s a more comprehensive example that might be used in a production trading system:
use decimal::d128;
use minifix::prelude::*;
use minifix::tagvalue::Encoder;
use rust_decimal_macros::dec;
use chrono::Utc;
#[derive(Debug)]
pub struct OrderRequest {
pub client_order_id: String,
pub symbol: String,
pub side: Side,
pub order_type: OrderType,
pub quantity: rust_decimal::Decimal,
pub price: Option<rust_decimal::Decimal>,
pub account: String,
}
#[derive(Debug)]
pub enum Side { Buy, Sell }
#[derive(Debug)]
pub enum OrderType { Market, Limit }
pub fn encode_new_order_single(
order: &OrderRequest,
seq_num: u32,
sender_comp_id: &str,
target_comp_id: &str,
) -> Vec<u8> {
let mut encoder = Encoder::default();
let mut buffer = Vec::new();
let mut msg = encoder.start_message(b"FIX.4.4", &mut buffer, b"D");
// Header fields
msg.set(fix44::MSG_SEQ_NUM, seq_num);
msg.set(fix44::SENDER_COMP_ID, sender_comp_id);
msg.set(fix44::TARGET_COMP_ID, target_comp_id);
// Current timestamp
let timestamp = Utc::now().format("%Y%m%d-%H:%M:%S%.3f").to_string();
msg.set(fix44::SENDING_TIME, timestamp);
// Order details
msg.set(fix44::CL_ORD_ID, &order.client_order_id);
msg.set(fix44::SYMBOL, &order.symbol);
msg.set(fix44::ACCOUNT, &order.account);
msg.set(fix44::ORDER_QTY, order.quantity);
// Side
match order.side {
Side::Buy => msg.set(fix44::SIDE, fix44::Side::Buy),
Side::Sell => msg.set(fix44::SIDE, fix44::Side::Sell),
}
// Order type and price
match order.order_type {
OrderType::Market => {
msg.set(fix44::ORD_TYPE, fix44::OrdType::Market);
},
OrderType::Limit => {
msg.set(fix44::ORD_TYPE, fix44::OrdType::Limit);
if let Some(price) = order.price {
msg.set(fix44::PRICE, price);
}
},
}
// Standard instructions
msg.set(
fix44::HANDL_INST,
fix44::HandlInst::AutomatedExecutionOrderPrivateNoBrokerIntervention,
);
msg.set(fix44::TIME_IN_FORCE, fix44::TimeInForce::Day);
// Transaction time
let transact_time = Utc::now().format("%Y%m%d-%H:%M:%S%.3f").to_string();
msg.set(fix44::TRANSACT_TIME, transact_time);
let (encoded_message, _) = msg.done();
encoded_message.to_vec()
}
fn main() {
let order = OrderRequest {
client_order_id: "ORDER_12345".to_string(),
symbol: "AAPL".to_string(),
side: Side::Buy,
order_type: OrderType::Limit,
quantity: dec!(100),
price: Some(dec!(150.25)),
account: "ACCOUNT_001".to_string(),
};
let fix_message = encode_new_order_single(
&order,
42,
"TRADING_SYSTEM",
"PRIME_BROKER"
);
println!("π€ Encoded FIX Message:");
println!("{}", String::from_utf8_lossy(&fix_message));
// You would typically send this over a network connection
// send_to_broker(&fix_message).await?;
}
Message Structure
The encoded message follows standard FIX structure:
8=FIX.4.4|9=122|35=D|34=42|49=TRADING_SYSTEM|52=20241201-10:30:15.123|56=PRIME_BROKER|1=ACCOUNT_001|11=ORDER_12345|21=1|40=2|44=150.25|54=1|55=AAPL|38=100|59=0|60=20241201-10:30:15.123|10=072|
Where:
8=FIX.4.4: Begin string9=122: Body length (calculated automatically)35=D: Message type (New Order Single)34=42: Message sequence number- … (body fields)
10=072: Checksum (calculated automatically)
Performance Considerations
Buffer Reuse
let mut encoder = Encoder::default();
let mut buffer = Vec::with_capacity(1024); // Pre-allocate
// Reuse buffer for multiple messages
buffer.clear();
let mut msg1 = encoder.start_message(b"FIX.4.4", &mut buffer, b"D");
// ... build message
buffer.clear();
let mut msg2 = encoder.start_message(b"FIX.4.4", &mut buffer, b"F");
// ... build another message
Batch Encoding
fn encode_orders(orders: &[OrderRequest]) -> Vec<Vec<u8>> {
let mut encoder = Encoder::default();
let mut buffer = Vec::with_capacity(512);
let mut messages = Vec::with_capacity(orders.len());
for (i, order) in orders.iter().enumerate() {
buffer.clear();
let message = encode_new_order_single(order, i as u32 + 1, "SYS", "BRK");
messages.push(message);
}
messages
}
Best Practices
- Validate Input: Always validate order parameters before encoding
- Use Decimal Types: Use
rust_decimalordecimalfor financial amounts - Handle Timestamps: Use consistent timestamp formatting
- Reuse Buffers: Reuse buffers for better performance
- Error Handling: Implement proper error handling for encoding failures
- Testing: Thoroughly test encoded messages with your counterparties
Common Patterns
Order Amendments
// Cancel/Replace (35=G)
let mut msg = encoder.start_message(b"FIX.4.4", &mut buffer, b"G");
msg.set(fix44::ORIG_CL_ORD_ID, "ORDER_12345");
msg.set(fix44::CL_ORD_ID, "ORDER_12345_AMEND");
// ... other fields
Order Cancellation
// Order Cancel Request (35=F)
let mut msg = encoder.start_message(b"FIX.4.4", &mut buffer, b"F");
msg.set(fix44::ORIG_CL_ORD_ID, "ORDER_12345");
msg.set(fix44::CL_ORD_ID, "ORDER_12345_CANCEL");
// ... other fields
Next Steps
Message encoding is fundamental to FIX trading systems. With miniFIX’s type-safe encoder, you can:
- Build order management systems
- Create trading algorithms
- Implement risk management systems
- Develop market making applications
The combination of type safety, performance, and precision makes miniFIX ideal for mission-critical financial applications where correctness is paramount.