use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use rsta::backtest::{Action, BacktestConfig, Backtester, Context, Quantity, Strategy};
use rsta::indicators::trend::Sma;
use rsta::indicators::{Candle, Indicator};
use rsta::signals::{CrossDown, CrossUp, Signal, SignalEvent};
fn data_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/btc_usd_daily.csv")
}
fn load_btc() -> Vec<Candle> {
let path = data_path();
let file = File::open(&path).unwrap_or_else(|e| panic!("open {}: {e}", path.display()));
let mut out = Vec::new();
for line in BufReader::new(file).lines() {
let line = line.expect("read line");
if line.is_empty() {
continue;
}
let mut cols = line.split(',');
let ts: u64 = cols.next().unwrap().parse().unwrap();
let o: f64 = cols.next().unwrap().parse().unwrap();
let h: f64 = cols.next().unwrap().parse().unwrap();
let l: f64 = cols.next().unwrap().parse().unwrap();
let c: f64 = cols.next().unwrap().parse().unwrap();
let v: f64 = cols.next().unwrap().parse().unwrap();
out.push(Candle {
timestamp: ts,
open: o,
high: h,
low: l,
close: c,
volume: v,
});
}
out
}
struct SmaCrossover {
fast: Sma,
slow: Sma,
cross_up: CrossUp,
cross_down: CrossDown,
}
impl SmaCrossover {
fn new(fast_period: usize, slow_period: usize) -> Self {
Self {
fast: Sma::new(fast_period).unwrap(),
slow: Sma::new(slow_period).unwrap(),
cross_up: CrossUp::new(),
cross_down: CrossDown::new(),
}
}
}
impl Strategy for SmaCrossover {
fn on_candle(&mut self, candle: &Candle, _ctx: &Context) -> Action {
let fast = <Sma as Indicator<f64, f64>>::next(&mut self.fast, candle.close).unwrap();
let slow = <Sma as Indicator<f64, f64>>::next(&mut self.slow, candle.close).unwrap();
let (Some(f), Some(s)) = (fast, slow) else {
return Action::Hold;
};
let up = self.cross_up.next((f, s));
let down = self.cross_down.next((f, s));
match (up, down) {
(Some(SignalEvent::Long), _) => Action::EnterLong(Quantity::AllCash),
(_, Some(SignalEvent::Short)) => Action::Exit,
_ => Action::Hold,
}
}
}
fn print_metrics(label: &str, m: &rsta::backtest::Metrics) {
println!("=== {label} ===");
println!(" final equity : {:>14.2}", m.final_equity);
println!(" total return : {:>14.2}%", m.total_return * 100.0);
println!(" max drawdown : {:>14.2}%", m.max_drawdown * 100.0);
println!(" sharpe : {:>14.3}", m.sharpe);
println!(" trades : {:>14}", m.trade_count);
println!(" win rate : {:>14.2}%", m.win_rate * 100.0);
println!(" profit factor : {:>14.3}", m.profit_factor);
}
fn main() {
let candles = load_btc();
println!(
"loaded {} BTC daily candles ({}..{})",
candles.len(),
candles.first().map(|c| c.timestamp).unwrap_or(0),
candles.last().map(|c| c.timestamp).unwrap_or(0),
);
let config = BacktestConfig {
initial_cash: 10_000.0,
fee_rate: 0.001, slippage: 0.0001,
..Default::default()
};
let bt = Backtester::new(config);
struct BuyAndHold {
entered: bool,
}
impl Strategy for BuyAndHold {
fn on_candle(&mut self, _c: &Candle, _ctx: &Context) -> Action {
if !self.entered {
self.entered = true;
Action::EnterLong(Quantity::AllCash)
} else {
Action::Hold
}
}
}
let bh = bt.run(&candles, &mut BuyAndHold { entered: false });
print_metrics("buy & hold", &bh.metrics);
println!();
let mut strat = SmaCrossover::new(20, 50);
let result = bt.run(&candles, &mut strat);
print_metrics("SMA(20) / SMA(50) crossover", &result.metrics);
}