bts-rs 1.0.11

Backtest your trading strategy.
Documentation
mod utils;

use std::{error::Error as StdError, sync::Arc};

use bts_rs::prelude::*;
use bts_rs::errors::Error;
use ta::{indicators::*, *};

fn main() -> Result<(), Box<dyn StdError>> {
    let data = utils::example_candles();
    let initial_balance = 1_000.0;
    let candles = Arc::from_iter(data);
    let mut bts = Backtest::new(candles.clone(), initial_balance, None)?;
    let mut ema = ExponentialMovingAverage::new(100)?;
    let mut macd = MovingAverageConvergenceDivergence::default();

    struct TimeframeAggregator;

    impl Aggregation for TimeframeAggregator {
        fn factors(&self) -> &[usize] {
            // return (1) the normal candle
            // aggregate 4 & 8 candles
            &[1, 4, 8]
        }
    }

    bts.run_with_aggregator(&TimeframeAggregator, |bt, candles| {
        let candle_one = candles.first().ok_or(Error::CandleNotFound)?;
        let _candle_four = candles.get(1);
        let _candle_eight = candles.get(2);

        if let Some(_c) = _candle_four {
            // first candle appears at the 5rd iteration
        }
        if let Some(_c) = _candle_eight {
            // first candle appears at the 9rd iteration
        }

        let close = candle_one.close();
        let output = ema.next(close);
        let MovingAverageConvergenceDivergenceOutput { histogram, .. } = macd.next(close);

        let balance = bt.free_balance()?;
        // 21: minimum to trade
        let amount = balance.how_many(2.0).max(21.0);

        if balance > (initial_balance / 2.0) && close > output && histogram > 0.0 {
            let quantity = amount / close;
            let order = (
                OrderType::Market(close),
                OrderType::TrailingStop(close, 2.0),
                quantity,
                OrderSide::Buy,
            );
            bt.place_order(candle_one, order.into())?;
        }

        Ok(())
    })?;

    #[cfg(not(feature = "metrics"))]
    {
        let first_price = candles.first().unwrap().close();
        let last_price = candles.last().unwrap().close();

        let n = candles.len();
        println!("trades {n}");

        let new_balance = bts.balance();
        let t_balance = bts.total_balance();
        let new_balance_perf = initial_balance.change(new_balance);
        let t_balance_perf = initial_balance.change(t_balance);
        println!("performance {new_balance:.2}/{t_balance:.2} ({new_balance_perf:.2}%/{t_balance_perf:.2}%)");

        let buy_and_hold = (initial_balance / first_price) * last_price;
        let buy_and_hold_perf = first_price.change(last_price);
        println!("buy and hold {buy_and_hold:.2} ({buy_and_hold_perf:.2}%)");
    }

    #[cfg(feature = "metrics")]
    {
        let metrics = Metrics::from(&bts);
        println!("{metrics}");
    }

    #[cfg(feature = "draws")]
    {
        let options = DrawOptions::default()
            .draw_output(DrawOutput::Svg("bts.svg".to_owned()))
            .show_volume(true);
        let draw = Draw::from(&bts).with_options(options);
        draw.plot()?;
    }

    Ok(())
}