πŸ¦€ miniFIX Tutorial | Part 06

September 20, 2025 by CryptoPatrick Rust Tutorials

Building an Interactive Trading Client with miniFIX and Tokio

Introduction

Modern trading systems require interactive, user-friendly interfaces that can handle real-time market data and order management. This post explores building an interactive trading client using miniFIX with Tokio for async operations, combined with a command-line interface for trader interaction.

What You’ll Learn

  • Creating interactive trading applications with miniFIX
  • Integrating async Rust (Tokio) with FIX protocols
  • Building user prompts and input validation
  • Code generation for trading-specific field types
  • Designing modular trading client architecture

The Vision: An Interactive Trading Terminal

Imagine a terminal application where traders can:

  • Enter new orders interactively
  • Choose from validated options for sides, order types, etc.
  • See real-time confirmations
  • Manage their trading session

Project Structure

The trading client uses code generation to create type-safe field definitions:

// Generated from FIX specification
#[allow(dead_code)]
#[rustfmt::skip]
mod generated_fix42;

use core::fmt;
use minifix::definitions::fix42;
use minifix::FieldType;
use generated_fix42 as strum_fix42;
use strum::IntoEnumIterator;

Interactive User Prompts

The heart of an interactive trading client is gathering user input safely and intuitively:

use inquire::{Select, Text, CustomType};

#[derive(Debug, Clone)]
enum UserAction {
    EnterOrder,
    ViewPositions,
    CancelOrder,
    Quit,
}

fn prompt_user_action() -> anyhow::Result<UserAction> {
    let options = vec![
        UserAction::EnterOrder,
        UserAction::ViewPositions, 
        UserAction::CancelOrder,
        UserAction::Quit,
    ];
    
    let selection = Select::new("What would you like to do?", options)
        .prompt()?;
        
    Ok(selection)
}

Order Entry Flow

Let’s build a comprehensive order entry flow with validation:

fn prompt_symbol() -> anyhow::Result<String> {
    Text::new("Symbol (e.g., AAPL, TSLA):")
        .with_validator(|input: &str| {
            if input.len() >= 1 && input.len() <= 12 && input.chars().all(|c| c.is_ascii_alphanumeric()) {
                Ok(())
            } else {
                Err("Symbol must be 1-12 alphanumeric characters")
            }
        })
        .prompt()
        .map_err(Into::into)
}

fn prompt_side() -> anyhow::Result<fix42::Side> {
    let options = vec![
        ("Buy", fix42::Side::Buy),
        ("Sell", fix42::Side::Sell),
    ];
    
    let selection = Select::new("Side:", options)
        .prompt()?;
        
    Ok(selection.1)
}

fn prompt_order_type() -> anyhow::Result<fix42::OrdType> {
    let options = vec![
        ("Market", fix42::OrdType::Market),
        ("Limit", fix42::OrdType::Limit),
        ("Stop", fix42::OrdType::Stop),
        ("Stop Limit", fix42::OrdType::StopLimit),
    ];
    
    let selection = Select::new("Order Type:", options)
        .prompt()?;
        
    Ok(selection.1)
}

fn prompt_time_in_force() -> anyhow::Result<fix42::TimeInForce> {
    let options = vec![
        ("Day", fix42::TimeInForce::Day),
        ("Good Till Cancel", fix42::TimeInForce::GoodTillCancel),
        ("Immediate or Cancel", fix42::TimeInForce::ImmediateOrCancel),
        ("Fill or Kill", fix42::TimeInForce::FillOrKill),
    ];
    
    let selection = Select::new("Time in Force:", options)
        .prompt()?;
        
    Ok(selection.1)
}

fn prompt_price() -> anyhow::Result<rust_decimal::Decimal> {
    CustomType::<rust_decimal::Decimal>::new("Price:")
        .with_error_message("Please enter a valid price")
        .prompt()
        .map_err(Into::into)
}

fn prompt_quantity() -> anyhow::Result<u64> {
    CustomType::<u64>::new("Quantity:")
        .with_error_message("Please enter a valid quantity")
        .prompt()
        .map_err(Into::into)
}

Order Management Structure

Define structures to manage orders and trading state:

use uuid::Uuid;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;

#[derive(Debug, Clone)]
pub struct OrderRequest {
    pub client_order_id: String,
    pub symbol: String,
    pub side: fix42::Side,
    pub order_type: fix42::OrdType,
    pub time_in_force: fix42::TimeInForce,
    pub quantity: u64,
    pub price: Option<Decimal>,
    pub account: String,
    pub sender_comp_id: String,
    pub target_comp_id: String,
}

impl OrderRequest {
    fn new() -> Self {
        Self {
            client_order_id: Uuid::new_v4().to_string(),
            symbol: String::new(),
            side: fix42::Side::Buy,
            order_type: fix42::OrdType::Market,
            time_in_force: fix42::TimeInForce::Day,
            quantity: 0,
            price: None,
            account: String::new(),
            sender_comp_id: String::new(),
            target_comp_id: String::new(),
        }
    }
}

#[derive(Debug)]
pub struct TradingSession {
    pub orders: Vec<OrderRequest>,
    pub sequence_number: u32,
}

impl TradingSession {
    fn new() -> Self {
        Self {
            orders: Vec::new(),
            sequence_number: 1,
        }
    }
    
    fn next_sequence(&mut self) -> u32 {
        let seq = self.sequence_number;
        self.sequence_number += 1;
        seq
    }
}

Complete Order Entry Flow

Here’s how the complete interactive flow works:

async fn handle_enter_order(session: &mut TradingSession) -> anyhow::Result<()> {
    println!("πŸ“‹ New Order Entry");
    println!("==================");
    
    // Gather order details interactively
    let symbol = prompt_symbol()?;
    let side = prompt_side()?;
    let order_type = prompt_order_type()?;
    let time_in_force = prompt_time_in_force()?;
    let quantity = prompt_quantity()?;
    
    // Price is only needed for limit orders
    let price = match order_type {
        fix42::OrdType::Limit | fix42::OrdType::StopLimit => {
            Some(prompt_price()?)
        }
        _ => None
    };
    
    // System information
    let sender_comp_id = Text::new("SenderCompID:").prompt()?;
    let target_comp_id = Text::new("TargetCompID:").prompt()?;
    let account = Text::new("Account:").prompt()?;
    
    // Create order request
    let mut order = OrderRequest::new();
    order.symbol = symbol;
    order.side = side;
    order.order_type = order_type;
    order.time_in_force = time_in_force;
    order.quantity = quantity;
    order.price = price;
    order.sender_comp_id = sender_comp_id;
    order.target_comp_id = target_comp_id;
    order.account = account;
    
    // Display order summary
    display_order_summary(&order);
    
    // Confirm order
    let confirm = inquire::Confirm::new("Submit this order?")
        .with_default(false)
        .prompt()?;
    
    if confirm {
        // Encode and send order
        let fix_message = encode_new_order_single(&order, session.next_sequence())?;
        
        println!("βœ… Order submitted successfully!");
        println!("πŸ“€ FIX Message: {}", String::from_utf8_lossy(&fix_message));
        
        // Store in session
        session.orders.push(order);
        
        // In a real application, you would send this over a network connection
        // send_to_broker(&fix_message).await?;
    } else {
        println!("❌ Order cancelled");
    }
    
    Ok(())
}

fn display_order_summary(order: &OrderRequest) {
    println!("\nπŸ“Š Order Summary");
    println!("================");
    println!("Order ID: {}", order.client_order_id);
    println!("Symbol: {}", order.symbol);
    println!("Side: {:?}", order.side);
    println!("Type: {:?}", order.order_type);
    println!("Quantity: {}", order.quantity);
    if let Some(price) = order.price {
        println!("Price: ${}", price);
    }
    println!("Time in Force: {:?}", order.time_in_force);
    println!("Account: {}", order.account);
    println!();
}

FIX Message Encoding

Convert the interactive order into a FIX message:

use minifix::prelude::*;
use minifix::tagvalue::Encoder;

fn encode_new_order_single(
    order: &OrderRequest, 
    seq_num: u32
) -> anyhow::Result<Vec<u8>> {
    let mut encoder = Encoder::default();
    let mut buffer = Vec::new();
    
    let mut msg = encoder.start_message(b"FIX.4.2", &mut buffer, b"D");
    
    // Header
    msg.set(fix42::MSG_SEQ_NUM, seq_num);
    msg.set(fix42::SENDER_COMP_ID, &order.sender_comp_id);
    msg.set(fix42::TARGET_COMP_ID, &order.target_comp_id);
    
    // Timestamp
    let timestamp = Utc::now().format("%Y%m%d-%H:%M:%S%.3f").to_string();
    msg.set(fix42::SENDING_TIME, timestamp);
    
    // Order details
    msg.set(fix42::CL_ORD_ID, &order.client_order_id);
    msg.set(fix42::SYMBOL, &order.symbol);
    msg.set(fix42::SIDE, order.side);
    msg.set(fix42::ORD_TYPE, order.order_type);
    msg.set(fix42::TIME_IN_FORCE, order.time_in_force);
    msg.set(fix42::ORDER_QTY, order.quantity);
    msg.set(fix42::ACCOUNT, &order.account);
    
    // Price (if applicable)
    if let Some(price) = order.price {
        msg.set(fix42::PRICE, price);
    }
    
    // Standard fields
    msg.set(fix42::HANDL_INST, fix42::HandlInst::AutomatedExecutionOrderPrivateNoBrokerIntervention);
    
    let (encoded_message, _) = msg.done();
    Ok(encoded_message.to_vec())
}

Main Application Loop

The main loop handles user interaction and session management:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    println!("πŸš€ miniFIX Interactive Trading Client");
    println!("=====================================");
    
    let mut session = TradingSession::new();
    
    loop {
        match prompt_user_action()? {
            UserAction::EnterOrder => {
                if let Err(e) = handle_enter_order(&mut session).await {
                    eprintln!("❌ Error entering order: {}", e);
                }
            }
            UserAction::ViewPositions => {
                display_orders(&session.orders);
            }
            UserAction::CancelOrder => {
                // TODO: Implement order cancellation
                println!("🚧 Order cancellation not implemented yet");
            }
            UserAction::Quit => {
                println!("πŸ‘‹ Goodbye!");
                break;
            }
        }
        
        println!(); // Add spacing
    }
    
    Ok(())
}

fn display_orders(orders: &[OrderRequest]) {
    if orders.is_empty() {
        println!("πŸ“­ No orders in this session");
        return;
    }
    
    println!("πŸ“‹ Current Orders");
    println!("=================");
    for (i, order) in orders.iter().enumerate() {
        println!("{}. {} {} {} @ {}", 
            i + 1,
            order.symbol,
            match order.side {
                fix42::Side::Buy => "BUY",
                fix42::Side::Sell => "SELL",
                _ => "UNKNOWN"
            },
            order.quantity,
            order.price.map(|p| p.to_string()).unwrap_or_else(|| "MARKET".to_string())
        );
    }
}

Enhanced Features

Input Validation

fn validate_symbol(symbol: &str) -> Result<(), String> {
    if symbol.is_empty() {
        return Err("Symbol cannot be empty".to_string());
    }
    
    if symbol.len() > 12 {
        return Err("Symbol too long (max 12 characters)".to_string());
    }
    
    if !symbol.chars().all(|c| c.is_ascii_alphanumeric()) {
        return Err("Symbol must contain only alphanumeric characters".to_string());
    }
    
    Ok(())
}

Configuration Management

#[derive(Debug, serde::Deserialize)]
pub struct ClientConfig {
    pub sender_comp_id: String,
    pub target_comp_id: String,
    pub default_account: String,
    pub fix_version: String,
}

impl ClientConfig {
    fn load() -> anyhow::Result<Self> {
        let config_str = std::fs::read_to_string("client_config.toml")?;
        toml::from_str(&config_str).map_err(Into::into)
    }
}

Async Network Integration

use tokio::net::TcpStream;

pub struct FixConnection {
    stream: TcpStream,
    encoder: Encoder,
    decoder: Decoder,
}

impl FixConnection {
    pub async fn connect(address: &str) -> anyhow::Result<Self> {
        let stream = TcpStream::connect(address).await?;
        let dictionary = Dictionary::fix42();
        
        Ok(Self {
            stream,
            encoder: Encoder::default(),
            decoder: Decoder::new(dictionary),
        })
    }
    
    pub async fn send_order(&mut self, order: &OrderRequest, seq_num: u32) -> anyhow::Result<()> {
        let message = encode_new_order_single(order, seq_num)?;
        
        use tokio::io::AsyncWriteExt;
        self.stream.write_all(&message).await?;
        
        Ok(())
    }
}

Production Considerations

  1. Error Recovery: Implement robust error handling and recovery mechanisms
  2. Session Management: Handle FIX session logistics (logon, heartbeats, sequence numbers)
  3. Persistence: Store orders and session state persistently
  4. Security: Implement proper authentication and encryption
  5. Monitoring: Add logging and metrics for production use

Benefits

  • Type Safety: Compile-time checking of FIX field usage
  • User Experience: Intuitive command-line interface with validation
  • Async Performance: Non-blocking I/O for real-time trading
  • Extensibility: Easy to add new order types and features
  • Testing: Interactive interface makes manual testing straightforward

Next Steps

This interactive trading client provides a foundation for:

  • Algorithmic trading frontends
  • Risk management interfaces
  • Market making tools
  • Training and simulation systems

The combination of miniFIX’s type safety, Tokio’s async performance, and interactive CLI makes it suitable for both development and production trading environments.


Next: Part 07 - Production Patterns

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