CH
CalcHub
Back to Guides
Intermediate

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 systems

Cargo.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