use std::{
collections::{VecDeque, vec_deque::Iter},
sync::Arc,
};
#[cfg(feature = "metrics")]
use crate::metrics::*;
use crate::{
PercentCalculus,
engine::*,
errors::{Error, Result},
};
#[cfg(test)]
impl Iterator for Backtest {
type Item = Candle;
fn next(&mut self) -> Option<Self::Item> {
let candle = self.data.get(self.index).cloned();
self.index += 1;
candle
}
}
pub trait Aggregation {
fn factors(&self) -> &[usize];
fn aggregate(&self, candles: &[&Candle]) -> Result<Candle> {
if candles.is_empty() {
return Err(Error::CandleDataEmpty);
}
let first_candle = candles.first().ok_or(Error::CandleNotFound)?;
let last_candle = candles.last().ok_or(Error::CandleNotFound)?;
let open = first_candle.open();
let close = last_candle.close();
let uptrend_open = if open > close { open } else { close };
let uptrend_close = if open > close { close } else { open };
let high = candles.iter().map(|c| c.high()).fold(uptrend_open, f64::max);
let low = candles.iter().map(|c| c.low()).fold(uptrend_close, f64::min);
let volume = candles.iter().map(|c| c.volume()).sum::<f64>();
let bid = candles.iter().map(|c| c.bid()).sum::<f64>();
CandleBuilder::builder()
.open(open)
.high(high)
.low(low)
.close(close)
.volume(volume)
.bid(bid)
.open_time(first_candle.open_time())
.close_time(last_candle.close_time())
.build()
}
fn should_aggregate(&self, factor: usize, candles: &[&Candle]) -> bool {
candles.len() == factor
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone)]
pub struct Backtest {
#[cfg(test)]
index: usize,
wallet: Wallet,
data: Arc<[Candle]>,
#[cfg(feature = "metrics")]
events: Vec<Event>,
orders: VecDeque<Order>,
positions: VecDeque<Position>,
market_fees: Option<(f64, f64)>,
}
impl std::ops::Deref for Backtest {
type Target = Wallet;
fn deref(&self) -> &Self::Target {
&self.wallet
}
}
impl Backtest {
pub fn new(data: Arc<[Candle]>, initial_balance: f64, market_fees: Option<(f64, f64)>) -> Result<Self> {
if data.is_empty() {
return Err(Error::CandleDataEmpty);
}
if let Some((market_fee, limit_fee)) = market_fees
&& (market_fee <= 0.0 || limit_fee <= 0.0)
{
return Err(Error::NegZeroFees);
}
let market_fees = market_fees.map(|(mf, lf)| (mf / 100.0, lf / 100.0));
Ok(Self {
data,
#[cfg(test)]
index: 0,
market_fees,
#[cfg(feature = "metrics")]
events: Vec::new(),
orders: VecDeque::new(),
positions: VecDeque::new(),
wallet: Wallet::new(initial_balance)?,
})
}
pub fn market_fees(&self) -> Option<&(f64, f64)> {
self.market_fees.as_ref()
}
pub fn candles(&self) -> std::slice::Iter<'_, Candle> {
self.data.iter()
}
pub fn orders(&self) -> Iter<'_, Order> {
self.orders.iter()
}
pub fn positions(&self) -> Iter<'_, Position> {
self.positions.iter()
}
#[cfg(feature = "metrics")]
pub fn events(&self) -> std::slice::Iter<'_, Event> {
self.events.iter()
}
pub fn place_order(&mut self, _candle: &Candle, order: Order) -> Result<()> {
self.wallet.lock(order.cost()?)?;
self.orders.push_back(order);
#[cfg(feature = "metrics")]
{
let open_time = _candle.open_time();
self.events.push(Event::from((open_time, &self.wallet)));
self.events.push(Event::AddOrder(open_time, order));
}
Ok(())
}
pub fn delete_order(&mut self, _candle: &Candle, order: &Order, force_remove: bool) -> Result<()> {
if force_remove {
let order_idx = self
.orders
.iter()
.position(|o| o == order)
.ok_or(Error::OrderNotFound)?;
self.orders.remove(order_idx).ok_or(Error::RemoveOrder)?;
}
self.wallet.unlock(order.cost()?)?;
#[cfg(feature = "metrics")]
{
let open_time = _candle.open_time();
self.events.push(Event::DelOrder(open_time, *order));
self.events.push(Event::from((open_time, &self.wallet)));
}
Ok(())
}
fn open_position(&mut self, _candle: &Candle, position: Position) -> Result<()> {
self.wallet.sub(position.cost()?)?;
if let Some((market_fee, limit_fee)) = self.market_fees {
if position.is_market_type() {
self.wallet.sub_fees(position.cost()? * market_fee)?;
} else {
self.wallet.sub_fees(position.cost()? * limit_fee)?;
};
}
self.positions.push_back(position);
#[cfg(feature = "metrics")]
{
let open_time = _candle.open_time();
self.events.push(Event::from((open_time, &self.wallet)));
self.events.push(Event::AddPosition(open_time, position));
}
Ok(())
}
pub fn close_position(
&mut self,
_candle: &Candle,
position: &Position,
exit_price: f64,
force_remove: bool,
) -> Result<f64> {
if exit_price <= 0.0 || !exit_price.is_finite() {
return Err(Error::ExitPrice(exit_price));
}
if force_remove {
let pos_idx = self
.positions
.iter()
.position(|p| p == position)
.ok_or(Error::PositionNotFound)?;
self.positions.remove(pos_idx).ok_or(Error::RemovePosition)?;
}
let pnl = position.estimate_pnl(exit_price)?;
let total_amount = pnl + position.cost()?;
self.wallet.add(total_amount)?;
self.wallet.sub_pnl(total_amount);
if let Some((market_fee, limit_fee)) = self.market_fees {
if position.is_market_type() {
self.wallet.sub_fees(position.cost()? * market_fee)?;
} else {
self.wallet.sub_fees(position.cost()? * limit_fee)?;
};
}
#[cfg(feature = "metrics")]
{
let mut _position = *position;
_position.set_exit_price(exit_price)?;
let open_time = _candle.open_time();
self.events.push(Event::from((open_time, &self.wallet)));
self.events.push(Event::DelPosition(open_time, _position));
}
Ok(pnl)
}
pub fn close_all_positions(&mut self, candle: &Candle, exit_price: f64) -> Result<()> {
while let Some(position) = self.positions.pop_front() {
self.close_position(candle, &position, exit_price, false)?;
}
Ok(())
}
fn execute_orders(&mut self, candle: &Candle) -> Result<()> {
let mut orders = VecDeque::with_capacity(self.orders.len());
while let Some(order) = self.orders.pop_front() {
let price = order.entry_price()?;
if price >= candle.low() && price <= candle.high() {
self.open_position(candle, Position::from(order))?;
} else {
if order.is_market_type() {
self.delete_order(candle, &order, false)?;
} else {
orders.push_back(order);
}
}
}
self.orders.append(&mut orders);
Ok(())
}
fn execute_positions(&mut self, candle: &Candle) -> Result<()> {
let mut positions = VecDeque::with_capacity(self.positions.len());
while let Some(mut position) = self.positions.pop_front() {
let should_close = match position.exit_rule() {
Some(OrderType::TakeProfitAndStopLoss(take_profit, stop_loss)) => {
if *take_profit < 0.0 || *stop_loss < 0.0 {
return Err(Error::NegTakeProfitAndStopLoss);
}
match position.side() {
PositionSide::Long => {
if *take_profit > 0.0 && take_profit <= &candle.high() {
Some(*take_profit)
} else if *stop_loss > 0.0 && stop_loss >= &candle.low() {
Some(*stop_loss)
} else {
None
}
}
PositionSide::Short => {
if *take_profit > 0.0 && take_profit >= &candle.low() {
Some(*take_profit)
} else if *stop_loss > 0.0 && stop_loss <= &candle.high() {
Some(*stop_loss)
} else {
None
}
}
}
}
Some(OrderType::TrailingStop(price, percent)) => {
if *price <= 0.0 || *percent <= 0.0 {
return Err(Error::NegZeroTrailingStop);
}
match position.side() {
PositionSide::Long => {
let execute_price = price.subpercent(*percent);
if execute_price >= candle.low() {
Some(execute_price)
} else {
if &candle.high() > price {
position.set_trailingstop(candle.high());
}
None
}
}
PositionSide::Short => {
let execute_price = price.addpercent(*percent);
if execute_price <= candle.high() {
Some(execute_price)
} else {
if &candle.low() < price {
position.set_trailingstop(candle.low());
}
None
}
}
}
}
None => None,
_ => {
return Err(Error::MismatchedOrderType);
}
};
match should_close {
Some(exit_price) => {
self.close_position(candle, &position, exit_price, false)?;
}
None => positions.push_back(position),
}
}
let mut total_unrealized_pnl = 0.0;
for position in &positions {
let current_price = candle.close();
let pnl = position.estimate_pnl(current_price)?;
total_unrealized_pnl += pnl;
}
self.positions.append(&mut positions);
self.wallet.set_unrealized_pnl(total_unrealized_pnl);
Ok(())
}
pub fn run<S>(&mut self, mut strategy: S) -> Result<()>
where
S: FnMut(&mut Self, &Candle) -> Result<()>,
{
let candles = Arc::clone(&self.data);
for candle in candles.iter() {
strategy(self, candle)?;
self.execute_orders(candle)?;
self.execute_positions(candle)?;
}
Ok(())
}
pub fn run_with_aggregator<A, S>(&mut self, aggregator: &A, mut strategy: S) -> Result<()>
where
A: Aggregation,
S: FnMut(&mut Self, Vec<&Candle>) -> Result<()>,
{
use std::collections::BTreeMap;
let factors = aggregator.factors();
if factors.is_empty() {
return Err(Error::InvalidFactor);
}
let mut current_candles = BTreeMap::new();
let mut aggregated_candles_map = BTreeMap::new();
for &factor in factors {
current_candles.insert(factor, VecDeque::with_capacity(factor));
aggregated_candles_map.insert(factor, VecDeque::with_capacity(1));
}
let candles = Arc::clone(&self.data);
for candle in candles.iter() {
for (_, deque) in current_candles.iter_mut() {
deque.push_back(candle);
}
for (factor, agg) in aggregated_candles_map.iter_mut() {
let deque = current_candles.get_mut(factor).ok_or(Error::CandleDataEmpty)?;
let contiguous_candles = deque.make_contiguous();
if aggregator.should_aggregate(*factor, contiguous_candles) {
let candle = aggregator.aggregate(contiguous_candles)?;
agg.pop_front();
deque.pop_front();
agg.push_back(candle);
}
}
let agg_candles = aggregated_candles_map.values().flatten().collect();
strategy(self, agg_candles)?;
self.execute_orders(candle)?;
self.execute_positions(candle)?;
}
Ok(())
}
pub fn reset(&mut self) {
#[cfg(test)]
{
self.index = 0;
}
#[cfg(feature = "metrics")]
{
self.events = Vec::new();
}
self.wallet.reset();
self.orders = VecDeque::new();
self.positions = VecDeque::new();
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::PercentCalculus;
use crate::engine::*;
use chrono::DateTime;
fn get_data() -> Arc<[Candle]> {
let candle = CandleBuilder::builder()
.open(100.0)
.high(111.0)
.low(99.0)
.close(110.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
Arc::from_iter(vec![candle])
}
fn get_long_data() -> Arc<[Candle]> {
let candle1 = CandleBuilder::builder()
.open(90.0)
.high(110.0)
.low(80.0)
.close(100.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle2 = CandleBuilder::builder()
.open(100.0)
.high(119.0)
.low(90.0)
.close(110.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle3 = CandleBuilder::builder()
.open(110.0)
.high(129.0)
.low(100.0)
.close(120.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let iter = vec![candle1, candle2, candle3];
Arc::from_iter(iter)
}
fn get_short_data() -> Arc<[Candle]> {
let candle1 = CandleBuilder::builder()
.open(150.0)
.high(160.0)
.low(131.0)
.close(140.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle2 = CandleBuilder::builder()
.open(140.0)
.high(150.0)
.low(121.0)
.close(130.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle3 = CandleBuilder::builder()
.open(130.0)
.high(140.0)
.low(111.0)
.close(120.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let iter = vec![candle1, candle2, candle3];
Arc::from_iter(iter)
}
fn get_long_data_trailing_stop() -> Arc<[Candle]> {
let candle1 = CandleBuilder::builder()
.open(99.0)
.high(101.0)
.low(98.0)
.close(100.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle2 = CandleBuilder::builder()
.open(100.0)
.high(110.0)
.low(99.0)
.close(108.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle3 = CandleBuilder::builder()
.open(108.0)
.high(140.0)
.low(108.0)
.close(135.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle4 = CandleBuilder::builder()
.open(135.0)
.high(139.9)
.low(126.0)
.close(130.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let iter = vec![candle1, candle2, candle3, candle4];
Arc::from_iter(iter)
}
fn get_long_data_trailing_stop_loss() -> Arc<[Candle]> {
let candle1 = CandleBuilder::builder()
.open(99.0)
.high(100.0)
.low(98.0)
.close(100.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let candle2 = CandleBuilder::builder()
.open(100.0)
.high(100.0)
.low(90.0)
.close(100.0)
.volume(1.0)
.open_time(DateTime::from_timestamp_secs(1515151515).unwrap())
.close_time(DateTime::from_timestamp_secs(1515151516).unwrap())
.build()
.unwrap();
let iter = vec![candle1, candle2];
Arc::from_iter(iter)
}
#[test]
fn scenario_place_and_delete_order_with_market_fees() {
let data = get_data();
let balance = 1000.0;
let market_fee = 0.1; let mut bt = Backtest::new(data, balance, Some((market_fee, 0.01))).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let expected_fee = price * 1.0 * market_fee; let _expected_total_cost = price + expected_fee;
let order = Order::from((OrderType::Market(price), 1.0, OrderSide::Buy));
bt.place_order(&candle, order).unwrap();
assert!(!bt.orders.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 890.0);
bt.delete_order(&candle, &order, true).unwrap();
assert!(bt.orders.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 1000.0);
{
let data = get_long_data();
let balance = 1000.0;
let market_fee = 1.0; let mut bt = Backtest::new(data, balance, Some((market_fee, 1.0))).unwrap();
let candle = bt.next().unwrap();
let price = candle.close(); let take_profit = OrderType::TakeProfitAndStopLoss(price.addpercent(20.0), 0.0);
let order = Order::from((OrderType::Market(price), take_profit, 1.0, OrderSide::Buy));
let open_fee = price * 1.0 * (market_fee / 100.0);
let expected_total_cost = price + open_fee;
bt.place_order(&candle, order).unwrap();
bt.execute_orders(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 899.0);
assert_eq!(bt.total_balance(), 899.0);
assert_eq!(bt.free_balance().unwrap(), 1000.0 - expected_total_cost);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap(); assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1018.0); assert_eq!(bt.total_balance(), 1018.0);
assert_eq!(bt.free_balance().unwrap(), 1018.0);
}
}
#[test]
fn scenario_open_position_with_market_fees() {
let data = get_long_data();
let balance = 1000.0;
let market_fee = 1.0; let mut bt = Backtest::new(data, balance, Some((market_fee, 1.0))).unwrap();
let candle = bt.next().unwrap();
let price = candle.close(); let take_profit = OrderType::TakeProfitAndStopLoss(price.addpercent(20.0), 0.0);
let order = Order::from((OrderType::Market(price), take_profit, 1.0, OrderSide::Buy));
let open_fee = price * 1.0 * (market_fee / 100.0);
let expected_total_cost = price + open_fee;
bt.place_order(&candle, order).unwrap();
bt.execute_orders(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 899.0);
assert_eq!(bt.total_balance(), 899.0);
assert_eq!(bt.free_balance().unwrap(), 1000.0 - expected_total_cost);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap(); assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1018.0); assert_eq!(bt.total_balance(), 1018.0);
assert_eq!(bt.free_balance().unwrap(), 1018.0);
}
#[test]
fn scenario_place_and_delete_auto_a_market_order() {
let data = get_data();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let order = Order::from((OrderType::Market(price * 3.0), 1.0, OrderSide::Buy));
bt.place_order(&candle, order).unwrap();
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 670.0);
bt.execute_orders(&candle).unwrap();
assert!(bt.orders.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 1000.0);
}
#[test]
fn scenario_place_and_delete_order() {
let data = get_data();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let order = Order::from((OrderType::Market(price), 1.0, OrderSide::Buy));
bt.place_order(&candle, order).unwrap();
assert!(!bt.orders.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 890.0);
bt.delete_order(&candle, &order, true).unwrap();
assert!(bt.orders.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 1000.0);
}
#[test]
fn scenario_open_long_position_and_take_profit() {
let data = get_long_data();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let take_profit = OrderType::TakeProfitAndStopLoss(price.addpercent(20.0), 0.0);
let order = Order::from((OrderType::Market(price), take_profit, 1.0, OrderSide::Buy));
bt.place_order(&candle, order).unwrap();
assert!(!bt.orders.is_empty());
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
bt.execute_orders(&candle).unwrap();
assert!(bt.orders.is_empty());
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 900.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 910.0); assert_eq!(bt.free_balance().unwrap(), 900.0);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1020.0);
assert_eq!(bt.total_balance(), 1020.0);
assert_eq!(bt.free_balance().unwrap(), 1020.0);
}
#[test]
fn scenario_open_long_position_and_stop_loss() {
let data = get_short_data();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let stop_loss = OrderType::TakeProfitAndStopLoss(0.0, price - 20.0);
let order = Order::from((OrderType::Market(price), stop_loss, 1.0, OrderSide::Buy));
bt.place_order(&candle, order).unwrap();
assert!(!bt.orders.is_empty());
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 860.0);
bt.execute_orders(&candle).unwrap();
assert!(bt.orders.is_empty());
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 860.0);
assert_eq!(bt.total_balance(), 860.0);
assert_eq!(bt.free_balance().unwrap(), 860.0);
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 860.0);
assert_eq!(bt.total_balance(), 850.0); assert_eq!(bt.free_balance().unwrap(), 860.0);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 980.0);
assert_eq!(bt.total_balance(), 980.0);
assert_eq!(bt.free_balance().unwrap(), 980.0);
}
#[test]
fn scenario_open_short_position_and_take_profit() {
let data = get_short_data();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let take_profit = OrderType::TakeProfitAndStopLoss(price - 20.0, 0.0);
let order = Order::from((OrderType::Market(price), take_profit, 1.0, OrderSide::Sell));
bt.place_order(&candle, order).unwrap();
assert!(!bt.orders.is_empty());
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 860.0);
bt.execute_orders(&candle).unwrap();
assert!(bt.orders.is_empty());
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 860.0);
assert_eq!(bt.total_balance(), 860.0);
assert_eq!(bt.free_balance().unwrap(), 860.0);
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 860.0);
assert_eq!(bt.total_balance(), 870.0); assert_eq!(bt.free_balance().unwrap(), 860.0);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1020.0);
assert_eq!(bt.total_balance(), 1020.0);
assert_eq!(bt.free_balance().unwrap(), 1020.0);
}
#[test]
fn scenario_open_short_position_and_stop_loss() {
let data = get_long_data();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let stop_loss = OrderType::TakeProfitAndStopLoss(0.0, price.addpercent(20.0));
let order = Order::from((OrderType::Market(price), stop_loss, 1.0, OrderSide::Sell));
bt.place_order(&candle, order).unwrap();
assert!(!bt.orders.is_empty());
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1000.0);
assert_eq!(bt.total_balance(), 1000.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
bt.execute_orders(&candle).unwrap();
assert!(bt.orders.is_empty());
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 900.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 890.0); assert_eq!(bt.free_balance().unwrap(), 900.0);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 980.0);
assert_eq!(bt.total_balance(), 980.0);
assert_eq!(bt.free_balance().unwrap(), 980.0);
}
#[test]
fn scenario_open_long_position_with_trailing_stop_profit() {
let data = get_long_data_trailing_stop();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let trailing_stop = OrderType::TrailingStop(price, 10.0);
let order = Order::from((OrderType::Market(price), trailing_stop, 1.0, OrderSide::Buy));
bt.place_order(&candle, order).unwrap();
bt.execute_orders(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 900.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 908.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 935.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 1026.0);
assert_eq!(bt.total_balance(), 1026.0);
assert_eq!(bt.free_balance().unwrap(), 1026.0);
}
#[test]
fn scenario_open_long_position_with_trailing_stop_loss() {
let data = get_long_data_trailing_stop_loss();
let balance = 1000.0;
let mut bt = Backtest::new(data, balance, None).unwrap();
let candle = bt.next().unwrap();
let price = candle.close();
let trailing_stop = OrderType::TrailingStop(price, 10.0);
let order = Order::from((OrderType::Market(price), trailing_stop, 1.0, OrderSide::Buy));
bt.place_order(&candle, order).unwrap();
bt.execute_orders(&candle).unwrap();
assert!(!bt.positions.is_empty());
assert_eq!(bt.balance(), 900.0);
assert_eq!(bt.total_balance(), 900.0);
assert_eq!(bt.free_balance().unwrap(), 900.0);
bt.execute_positions(&candle).unwrap();
assert!(!bt.positions.is_empty());
let candle = bt.next().unwrap();
bt.execute_positions(&candle).unwrap();
assert!(bt.positions.is_empty());
assert_eq!(bt.balance(), 990.0);
assert_eq!(bt.total_balance(), 990.0);
assert_eq!(bt.free_balance().unwrap(), 990.0);
}
struct TestAggregator;
impl Aggregation for TestAggregator {
fn factors(&self) -> &[usize] {
&[1, 2]
}
}
#[test]
fn scenario_with_aggregator() {
let data = get_short_data();
let mut bt = Backtest::new(data, 1.0, None).unwrap();
let mut ic = 0;
let aggregator = TestAggregator;
bt.run_with_aggregator(&aggregator, |_, candles| {
let candle_one = candles.first();
let candle_two = candles.get(1);
assert!(candle_one.is_some());
if ic > 0 {
assert!(candle_two.is_some());
assert_ne!(candle_one, candle_two);
}
ic += 1;
Ok(())
})
.unwrap();
}
}