Building a Trading Strategy in Rust
Now let's build a complete trading strategy in Rust โ from reading data to generating signals to writing results. This is where Rust's speed and safety make a real difference.
The Strategy: Moving Average Crossover
Same strategy as our Python backtest, implemented in Rust: buy when the fast MA crosses above the slow MA, sell when it crosses below.
Complete Working Code
use csv::ReaderBuilder; use serde::{{Deserialize, Serialize}}; use std::error::Error; // โโโ Data structures โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ #[derive(Debug, Deserialize)] struct Bar {{ #[serde(rename = "Date")] date: String, #[serde(rename = "Close")] close: f64, }} #[derive(Debug, Clone)] enum Signal {{ Buy, Sell, Hold, }} #[derive(Debug, Serialize)] struct TradeRecord {{ date: String, action: String, price: f64, cumulative_return: f64, }} // โโโ Moving average โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ fn sma(prices: &[f64], window: usize) -> Vec<Option<f64>> {{ let mut result = Vec::with_capacity(prices.len()); for i in 0..prices.len() {{ if i + 1 < window {{ result.push(None); }} else {{ let sum: f64 = prices[i + 1 - window..=i].iter().sum(); result.push(Some(sum / window as f64)); }} }} result }} // โโโ Signal generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ fn generate_signals(fast_ma: &[Option<f64>], slow_ma: &[Option<f64>]) -> Vec<Signal> {{ let mut signals = Vec::with_capacity(fast_ma.len()); let mut in_position = false; for i in 0..fast_ma.len() {{ match (fast_ma[i], slow_ma[i]) {{ (Some(fast), Some(slow)) => {{ if fast > slow && !in_position {{ signals.push(Signal::Buy); in_position = true; }} else if fast < slow && in_position {{ signals.push(Signal::Sell); in_position = false; }} else {{ signals.push(Signal::Hold); }} }} _ => signals.push(Signal::Hold), }} }} signals }} // โโโ Performance calculation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ fn calculate_performance( bars: &[Bar], signals: &[Signal], ) -> (f64, f64, Vec<TradeRecord>) {{ let mut cumulative = 1.0; let mut max_cumulative = 1.0; let mut max_drawdown = 0.0; let mut in_position = false; let mut entry_price = 0.0; let mut records: Vec<TradeRecord> = Vec::new(); for (i, signal) in signals.iter().enumerate() {{ match signal {{ Signal::Buy => {{ entry_price = bars[i].close; in_position = true; records.push(TradeRecord {{ date: bars[i].date.clone(), action: "BUY".to_string(), price: bars[i].close, cumulative_return: (cumulative - 1.0) * 100.0, }}); }} Signal::Sell => {{ if in_position {{ let trade_return = (bars[i].close - entry_price) / entry_price; cumulative *= 1.0 + trade_return; if cumulative > max_cumulative {{ max_cumulative = cumulative; }} let dd = (cumulative - max_cumulative) / max_cumulative; if dd < max_drawdown {{ max_drawdown = dd; }} in_position = false; records.push(TradeRecord {{ date: bars[i].date.clone(), action: "SELL".to_string(), price: bars[i].close, cumulative_return: (cumulative - 1.0) * 100.0, }}); }} }} Signal::Hold => {{}} }} }} let total_return = (cumulative - 1.0) * 100.0; (total_return, max_drawdown * 100.0, records) }} // โโโ Write results to CSV โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ fn write_results(records: &[TradeRecord], path: &str) -> Result<(), Box<dyn Error>> {{ let mut writer = csv::Writer::from_path(path)?; for record in records {{ writer.serialize(record)?; }} writer.flush()?; Ok(()) }} // โโโ Main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ fn main() -> Result<(), Box<dyn Error>> {{ // Load data let mut reader = ReaderBuilder::new() .has_headers(true) .from_path("aapl_daily.csv")?; let bars: Vec<Bar> = reader.deserialize() .filter_map(|r| r.ok()) .collect(); println!("Loaded {{}} bars", bars.len()); // Extract close prices let closes: Vec<f64> = bars.iter().map(|b| b.close).collect(); // Calculate moving averages let fast_ma = sma(&closes, 50); let slow_ma = sma(&closes, 200); // Generate signals let signals = generate_signals(&fast_ma, &slow_ma); // Calculate performance let (total_return, max_dd, records) = calculate_performance(&bars, &signals); println!("=== Strategy Results ==="); println!("Total return: {{:.2}}%", total_return); println!("Max drawdown: {{:.2}}%", max_dd); println!("Total trades: {{}}", records.len()); // Buy and hold comparison let bh_return = (closes.last().unwrap() / closes[0] - 1.0) * 100.0; println!("Buy & hold: {{:.2}}%", bh_return); // Write trade log write_results(&records, "trade_results.csv")?; println!("Results written to trade_results.csv"); Ok(()) }}When Rust Shines
This strategy processes the entire dataset in microseconds. Imagine running this across 5,000 instruments with 20 years of minute-level data. In Python, that might take minutes. In Rust, it takes seconds. For systematic trading firms scanning thousands of instruments in real-time, that speed difference is everything.
Pattern Matching for Trade Logic
Rust's pattern matching is one of its best features for financial logic. It forces you to handle every case explicitly, reducing bugs.
// Pattern matching makes complex logic readable and safe fn categorise_trade(return_pct: f64) -> &'static str {{ match return_pct {{ r if r > 10.0 => "Big winner", r if r > 2.0 => "Small winner", r if r > -2.0 => "Flat", r if r > -10.0 => "Small loser", _ => "Big loser", }} }} // The compiler ensures you handle ALL cases โ no missing branches // This is incredibly valuable in production trading systemsCargo.toml for This Project
[package] name = "rust-strategy" version = "0.1.0" edition = "2021" [dependencies] csv = "1.3" serde = {{ version = "1.0", features = ["derive"] }} # Build with optimisations: # cargo build --release # The --release flag enables optimisations that make code 10-30x faster