πŸ¦€ miniFIX Tutorial | Part 03

September 17, 2025 by CryptoPatrick Rust Tutorials

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 string
  • 9=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

  1. Validate Input: Always validate order parameters before encoding
  2. Use Decimal Types: Use rust_decimal or decimal for financial amounts
  3. Handle Timestamps: Use consistent timestamp formatting
  4. Reuse Buffers: Reuse buffers for better performance
  5. Error Handling: Implement proper error handling for encoding failures
  6. 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.


Next: Part 04 - Custom FIX Specifications

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