1use chrono::{DateTime, TimeDelta, Utc};
4
5#[path = "live.rs"]
6mod live;
7pub use live::StrategyFill;
8
9use crate::model::rounding_tolerance;
10use crate::model::{
11 Bar, OrderSide, OrderType, StrategyExit, StrategyReport, StrategyTrade, Timeframe,
12};
13
14#[derive(Clone, Copy, Debug, PartialEq)]
16#[non_exhaustive]
17pub enum OrderSize {
18 Fixed(f64),
20 EquityFraction(f64),
22 Cash(f64),
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
28#[non_exhaustive]
29pub enum Commission {
30 Fraction(f64),
32 PerUnit(f64),
34 PerOrder(f64),
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
40#[non_exhaustive]
41pub enum Slippage {
42 Ticks {
44 ticks: u32,
46 tick_size: f64,
48 },
49 Fraction(f64),
51}
52
53#[derive(Clone, Debug, PartialEq)]
59#[non_exhaustive]
60pub struct BacktestSettings {
61 pub initial_capital: f64,
63 pub currency: Option<String>,
65 pub risk_free_rate: f64,
67 pub order_size: OrderSize,
69 pub pyramiding: usize,
71 pub commission: Commission,
73 pub slippage: Slippage,
75 pub leverage: f64,
77 pub fill_on_close: bool,
79 pub recalculate_after_fills: bool,
81 pub recalculate_on_every_update: bool,
84 pub start_time: Option<DateTime<Utc>>,
86}
87
88impl Default for BacktestSettings {
89 fn default() -> Self {
90 Self {
91 initial_capital: 10_000.0,
92 currency: None,
93 risk_free_rate: 0.0,
94 order_size: OrderSize::Fixed(1.0),
95 pyramiding: 1,
96 commission: Commission::Fraction(0.0),
97 slippage: Slippage::Fraction(0.0),
98 leverage: 1.0,
99 fill_on_close: false,
100 recalculate_after_fills: false,
101 recalculate_on_every_update: false,
102 start_time: None,
103 }
104 }
105}
106
107fn nonnegative(value: f64) -> f64 {
108 if value.is_finite() {
109 value.max(0.0)
110 } else {
111 0.0
112 }
113}
114
115impl BacktestSettings {
116 fn fee(&self, quantity: f64, price: f64) -> f64 {
117 match self.commission {
118 Commission::Fraction(rate) => quantity * price.abs() * nonnegative(rate),
119 Commission::PerUnit(amount) => quantity * nonnegative(amount),
120 Commission::PerOrder(amount) => nonnegative(amount),
121 }
122 }
123
124 fn slipped(&self, price: f64, side: OrderSide) -> f64 {
125 let amount = match self.slippage {
126 Slippage::Ticks { ticks, tick_size } => f64::from(ticks) * nonnegative(tick_size),
127 Slippage::Fraction(rate) => price.abs() * nonnegative(rate),
128 };
129 price + sign(side) * amount
130 }
131
132 fn leverage(&self) -> f64 {
133 if self.leverage.is_finite() {
134 self.leverage.max(1.0)
135 } else {
136 1.0
137 }
138 }
139}
140
141pub trait Strategy {
178 fn inputs(&self) -> Vec<(String, String)> {
180 self.study_inputs()
181 .into_iter()
182 .map(|input| {
183 use crate::studies::{InputKind, InputValue};
184 let value = match input.value {
185 InputValue::Length(value) => value.to_string(),
186 InputValue::Integer(value) => value.to_string(),
187 InputValue::Number(value) => value.to_string(),
188 InputValue::Toggle(value) => value.to_string(),
189 InputValue::Choice(index) => match input.kind {
190 InputKind::Choice(choices) => {
191 choices.get(index).cloned().unwrap_or_default()
192 }
193 _ => index.to_string(),
194 },
195 InputValue::Text(value) | InputValue::Symbol(value) => value,
196 value => format!("{value:?}"),
197 };
198 (input.label, value)
199 })
200 .collect()
201 }
202
203 fn study_inputs(&self) -> Vec<crate::studies::StudyInput> {
205 Vec::new()
206 }
207
208 fn set_input(
211 &mut self,
212 id: &str,
213 value: crate::studies::InputValue,
214 ) -> Result<(), &'static str> {
215 let _ = (id, value);
216 Err("strategy has no editable inputs")
217 }
218
219 fn prepare(&mut self, bars: &[Bar]) {
224 let _ = bars;
225 }
226
227 fn prepare_update(&mut self, bars: &[Bar]) {
231 self.prepare(bars);
232 }
233
234 fn on_bar(&mut self, context: &mut StrategyContext<'_>);
240}
241
242pub struct StrategyContext<'a> {
249 bars: &'a [Bar],
250 position: f64,
251 orders: &'a mut Vec<Working>,
252 price: f64,
253 equity: f64,
254 after_fill: bool,
255 forming: bool,
256 trading: bool,
257 tick: u64,
258}
259
260impl StrategyContext<'_> {
261 pub fn bars(&self) -> &[Bar] {
264 self.bars
265 }
266
267 pub fn position(&self) -> f64 {
269 self.position
270 }
271
272 pub fn price(&self) -> f64 {
274 self.price
275 }
276
277 pub fn equity(&self) -> f64 {
279 self.equity
280 }
281
282 pub fn is_after_fill(&self) -> bool {
284 self.after_fill
285 }
286
287 pub fn is_forming_bar(&self) -> bool {
290 self.forming
291 }
292
293 pub fn entry(
297 &mut self,
298 id: impl Into<String>,
299 side: OrderSide,
300 quantity: impl Into<Option<f64>>,
301 order_type: OrderType,
302 ) {
303 let finite = match order_type {
304 OrderType::Market => true,
305 OrderType::Limit(price) | OrderType::Stop(price) => price.is_finite(),
306 OrderType::StopLimit { stop, limit } => stop.is_finite() && limit.is_finite(),
307 };
308 if finite {
309 self.place(id.into(), quantity.into(), Kind::Entry(side, order_type));
310 }
311 }
312
313 pub fn exit(
319 &mut self,
320 id: impl Into<String>,
321 quantity: Option<f64>,
322 take_profit: Option<f64>,
323 stop_loss: Option<f64>,
324 ) {
325 if take_profit.into_iter().chain(stop_loss).all(f64::is_finite) {
326 let side = exit_side(self.position);
327 self.place(
328 id.into(),
329 quantity,
330 Kind::Exit(take_profit, stop_loss, side),
331 );
332 }
333 }
334
335 pub fn cancel(&mut self, id: &str) {
337 self.orders.retain(|order| order.id != id);
338 }
339
340 pub fn exit_relative(
346 &mut self,
347 id: impl Into<String>,
348 side: OrderSide,
349 take_profit: f64,
350 stop_loss: f64,
351 ) {
352 if [take_profit, stop_loss]
353 .iter()
354 .all(|value| value.is_finite() && *value > 0.0)
355 {
356 self.place(
357 id.into(),
358 None,
359 Kind::RelativeExit(take_profit, stop_loss, side),
360 );
361 }
362 }
363
364 fn place(&mut self, id: String, quantity: Option<f64>, kind: Kind) {
365 if !self.trading {
366 return;
367 }
368 if quantity.is_some_and(|quantity| !quantity.is_finite() || quantity <= 0.0) {
369 return;
370 }
371 let order = Working {
372 id,
373 quantity,
374 kind,
375 deferred: (self.after_fill || self.forming).then_some(self.tick),
376 };
377 match self
378 .orders
379 .iter_mut()
380 .find(|working| working.id == order.id)
381 {
382 Some(working) => *working = order,
383 None => self.orders.push(order),
384 }
385 }
386}
387
388#[derive(Clone, Debug, PartialEq)]
390struct Working {
391 id: String,
392 quantity: Option<f64>,
394 kind: Kind,
395 deferred: Option<u64>,
397}
398
399#[derive(Clone, Copy, Debug, PartialEq)]
400enum Kind {
401 Entry(OrderSide, OrderType),
402 Exit(Option<f64>, Option<f64>, Option<OrderSide>),
404 RelativeExit(f64, f64, OrderSide),
406}
407
408#[derive(Clone, Debug, PartialEq)]
410struct Lot {
411 entry: String,
412 side: OrderSide,
413 quantity: f64,
414 entry_quantity: f64,
415 time: DateTime<Utc>,
416 price: f64,
417 fees: f64,
418}
419
420#[derive(Clone, Debug, PartialEq)]
446#[non_exhaustive]
447pub struct Backtest {
448 pub name: String,
450 pub symbol: String,
452 pub timeframe: Timeframe,
454 pub settings: BacktestSettings,
456 live: Option<Box<live::LiveState>>,
457}
458
459impl Backtest {
460 pub fn new(
462 name: impl Into<String>,
463 symbol: impl Into<String>,
464 timeframe: Timeframe,
465 initial_capital: f64,
466 ) -> Self {
467 Self {
468 name: name.into(),
469 symbol: symbol.into(),
470 timeframe,
471 live: None,
472 settings: BacktestSettings {
473 initial_capital,
474 ..Default::default()
475 },
476 }
477 }
478
479 pub fn run(&self, strategy: &mut dyn Strategy, bars: &[Bar], lower: &[Bar]) -> StrategyReport {
494 finish_now(self.run_inner(strategy, bars, lower, false))
495 }
496
497 pub async fn run_async(
506 &self,
507 strategy: &mut dyn Strategy,
508 bars: &[Bar],
509 lower: &[Bar],
510 ) -> StrategyReport {
511 self.run_inner(strategy, bars, lower, true).await
512 }
513
514 async fn run_inner(
515 &self,
516 strategy: &mut dyn Strategy,
517 bars: &[Bar],
518 lower: &[Bar],
519 cooperative: bool,
520 ) -> StrategyReport {
521 let settings = &self.settings;
522 let mut report = self.new_report(strategy, lower.len());
523 let mut lower: Vec<_> = lower.iter().filter(|bar| valid_bar(bar)).collect();
524 lower.sort_by_key(|bar| bar.time);
525 lower.dedup_by_key(|bar| bar.time);
526 strategy.prepare(bars);
527 let mut run = Run {
528 orders: Vec::new(),
529 lots: Vec::new(),
530 trades: Vec::new(),
531 realized: 0.0,
532 rejected: 0,
533 settings,
534 strategy,
535 history: &[],
536 trading: false,
537 tick: 0,
538 fills: None,
539 forming: false,
540 };
541 run.start_equity(self.timeframe, bars, &mut report);
542 let mut processed = cooperative.then_some(0);
543 for index in 0..bars.len() {
544 run.closed_bar_inner(bars, index, &lower, &mut report, true, &mut processed)
545 .await;
546 }
547 run.report_trades(&mut report);
548 report
549 }
550
551 fn new_report(&self, strategy: &dyn Strategy, lower_count: usize) -> StrategyReport {
552 let settings = &self.settings;
553 assert!(
554 settings.initial_capital.is_finite(),
555 "starting capital must be finite"
556 );
557 let mut report = StrategyReport::new(
558 &self.name,
559 &self.symbol,
560 self.timeframe,
561 settings.initial_capital,
562 );
563 report.currency = settings.currency.clone();
564 report.risk_free_rate = settings.risk_free_rate;
565 let size = match settings.order_size {
566 OrderSize::Fixed(value) => format!("Fixed units: {value}"),
567 OrderSize::EquityFraction(value) => format!("Equity fraction: {value}"),
568 OrderSize::Cash(value) => format!("Cash: {value}"),
569 };
570 let commission = match settings.commission {
571 Commission::Fraction(value) => format!("Fraction: {}", nonnegative(value)),
572 Commission::PerUnit(value) => format!("Per unit: {}", nonnegative(value)),
573 Commission::PerOrder(value) => format!("Per order: {}", nonnegative(value)),
574 };
575 let slippage = match settings.slippage {
576 Slippage::Fraction(value) => format!("Fraction: {}", nonnegative(value)),
577 Slippage::Ticks { ticks, tick_size } => {
578 format!("{ticks} ticks of {}", nonnegative(tick_size))
579 }
580 };
581 report.properties = vec![
582 ("Order size".into(), size),
583 ("Pyramiding".into(), settings.pyramiding.to_string()),
584 ("Commission".into(), commission),
585 ("Slippage".into(), slippage),
586 ("Leverage".into(), settings.leverage().to_string()),
587 ("Fill on close".into(), settings.fill_on_close.to_string()),
588 (
589 "Recalculate after fills".into(),
590 settings.recalculate_after_fills.to_string(),
591 ),
592 (
593 "Recalculate on every update".into(),
594 settings.recalculate_on_every_update.to_string(),
595 ),
596 ];
597 report.properties.push((
598 "Start time".into(),
599 settings
600 .start_time
601 .map_or_else(|| "All supplied history".into(), |time| time.to_rfc3339()),
602 ));
603 report
604 .properties
605 .push(("Lower-interval bars".into(), lower_count.to_string()));
606 report.properties.extend(
607 strategy
608 .inputs()
609 .into_iter()
610 .map(|(name, value)| (format!("Input: {name}"), value)),
611 );
612 report
613 }
614}
615
616fn finish_now<T>(future: impl std::future::Future<Output = T>) -> T {
617 let mut future = std::pin::pin!(future);
618 let mut context = std::task::Context::from_waker(std::task::Waker::noop());
619 match std::future::Future::poll(future.as_mut(), &mut context) {
620 std::task::Poll::Ready(output) => output,
621 std::task::Poll::Pending => unreachable!("synchronous execution never yields"),
622 }
623}
624
625async fn execution_step(processed: &mut Option<usize>) {
626 if let Some(processed) = processed {
627 if processed.is_multiple_of(256) {
628 yield_now().await;
629 }
630 *processed += 1;
631 }
632}
633
634async fn yield_now() {
635 let mut yielded = false;
636 std::future::poll_fn(|context| {
637 if yielded {
638 std::task::Poll::Ready(())
639 } else {
640 yielded = true;
641 context.waker().wake_by_ref();
642 std::task::Poll::Pending
643 }
644 })
645 .await;
646}
647
648fn bar_end(timeframe: Timeframe, bar: &Bar, next: Option<&Bar>) -> Option<DateTime<Utc>> {
650 match timeframe {
651 Timeframe::Month1 => bar.time.checked_add_months(chrono::Months::new(1)),
652 Timeframe::Months(count) => bar
653 .time
654 .checked_add_months(chrono::Months::new(count.get())),
655 _ if timeframe.is_trade_based() => next.map(|bar| bar.time),
656 _ => bar.time.checked_add_signed(timeframe.duration()),
657 }
658}
659
660pub(super) fn valid_bar(bar: &Bar) -> bool {
661 [bar.open, bar.high, bar.low, bar.close]
662 .iter()
663 .all(|price| price.is_finite())
664 && bar.low <= bar.open.min(bar.close)
665 && bar.high >= bar.open.max(bar.close)
666}
667
668fn exit_side(position: f64) -> Option<OrderSide> {
670 if position > 0.0 {
671 Some(OrderSide::Sell)
672 } else if position < 0.0 {
673 Some(OrderSide::Buy)
674 } else {
675 None
676 }
677}
678
679fn sign(side: OrderSide) -> f64 {
680 match side {
681 OrderSide::Buy => 1.0,
682 OrderSide::Sell => -1.0,
683 }
684}
685
686struct Run<'a> {
687 orders: Vec<Working>,
688 lots: Vec<Lot>,
690 trades: Vec<StrategyTrade>,
691 realized: f64,
693 rejected: usize,
694 settings: &'a BacktestSettings,
695 strategy: &'a mut dyn Strategy,
696 history: &'a [Bar],
697 trading: bool,
698 tick: u64,
699 fills: Option<&'a mut Vec<StrategyFill>>,
700 forming: bool,
701}
702
703impl<'a> Run<'a> {
704 fn start_equity(&self, timeframe: Timeframe, bars: &[Bar], report: &mut StrategyReport) {
705 if let Some(first) = bars.iter().find(|bar| {
706 self.settings
707 .start_time
708 .is_none_or(|start| bar.time >= start)
709 }) {
710 let interval = timeframe.duration().max(TimeDelta::milliseconds(1));
711 let before = first.time.checked_sub_signed(interval);
712 report.equity.push((
713 self.settings
714 .start_time
715 .unwrap_or(before.unwrap_or(DateTime::<Utc>::MIN_UTC)),
716 self.settings.initial_capital,
717 ));
718 }
719 }
720
721 fn closed_bar(
722 &mut self,
723 bars: &'a [Bar],
724 index: usize,
725 lower: &[&Bar],
726 report: &mut StrategyReport,
727 walk: bool,
728 ) {
729 finish_now(self.closed_bar_inner(bars, index, lower, report, walk, &mut None));
730 }
731
732 async fn closed_bar_inner(
733 &mut self,
734 bars: &'a [Bar],
735 index: usize,
736 lower: &[&Bar],
737 report: &mut StrategyReport,
738 walk: bool,
739 processed: &mut Option<usize>,
740 ) {
741 execution_step(processed).await;
742 self.forming = false;
743 let timeframe = report.timeframe;
744 let bar = &bars[index];
745 self.trading = self
746 .settings
747 .start_time
748 .is_none_or(|start| bar.time >= start);
749 if !self.trading {
750 self.history = &bars[..=index];
751 self.decide(bar.close, false);
752 return;
753 }
754 let end = bar_end(timeframe, bar, bars.get(index + 1));
755 self.history = &bars[..index];
756 let valid = valid_bar(bar);
757 let from = lower.partition_point(|lower| lower.time < bar.time);
758 let to = lower.partition_point(|lower| end.is_none_or(|end| lower.time < end));
759 let last_price_time = if from < to {
760 lower[to - 1].time
761 } else {
762 bar.time
763 };
764 if valid && walk {
765 if from < to {
766 for lower in &lower[from..to] {
767 execution_step(processed).await;
768 self.fill_bar(lower);
769 }
770 } else {
771 self.fill_bar(bar);
772 }
773 }
774 self.history = &bars[..=index];
775 self.decide(bar.close, false);
776 if valid && self.settings.fill_on_close {
777 let close_time = if timeframe.is_trade_based() {
779 last_price_time
780 } else if from < to || self.settings.start_time.is_some() {
781 end.unwrap_or(last_price_time)
782 } else {
783 bar.time
784 };
785 self.fill_markets(bar.close, close_time);
786 }
787 report.equity.push((
788 if self.settings.start_time.is_some() {
789 end.unwrap_or_else(|| {
791 last_price_time
792 .checked_add_signed(TimeDelta::nanoseconds(1))
793 .unwrap_or(last_price_time)
794 })
795 } else {
796 bar.time
797 },
798 if valid {
799 self.equity(bar.close)
800 } else {
801 f64::NAN
802 },
803 ));
804 }
805
806 fn report_trades(&self, report: &mut StrategyReport) {
807 report
808 .properties
809 .retain(|(name, _)| name != "Rejected orders");
810 report
811 .properties
812 .push(("Rejected orders".into(), self.rejected.to_string()));
813 report.trades = self.trades.clone();
814 report.trades.extend(self.lots.iter().map(|lot| {
815 let mut trade = StrategyTrade::new(lot.side, lot.quantity, lot.time, lot.price);
816 trade.label = Some(lot.entry.clone());
817 trade.fees = lot.fees;
818 trade
819 }));
820 }
821
822 fn position(&self) -> f64 {
823 self.lots.iter().fold(0.0, |position, lot| {
824 position + sign(lot.side) * lot.quantity
825 })
826 }
827
828 fn equity(&self, price: f64) -> f64 {
829 self.settings.initial_capital
830 + self.realized
831 + self
832 .lots
833 .iter()
834 .map(|lot| sign(lot.side) * (price - lot.price) * lot.quantity)
835 .sum::<f64>()
836 }
837
838 fn decide(&mut self, price: f64, after_fill: bool) {
839 let position = self.position();
840 let equity = self.equity(price);
841 self.strategy.on_bar(&mut StrategyContext {
842 bars: self.history,
843 position,
844 orders: &mut self.orders,
845 price,
846 equity,
847 after_fill,
848 forming: self.forming,
849 trading: self.trading,
850 tick: self.tick,
851 });
852 }
853
854 fn fill_bar(&mut self, bar: &Bar) {
855 self.tick += 1;
856 let up = bar.high - bar.open;
857 let down = bar.open - bar.low;
858 let near = if up - down
859 <= rounding_tolerance(bar.high.abs().max(bar.low.abs()), 4.0 * f64::EPSILON)
860 {
861 bar.high
862 } else {
863 bar.low
864 };
865 let far = if near == bar.high { bar.low } else { bar.high };
866 for (start, end) in [(bar.open, near), (near, far), (far, bar.close)] {
867 self.fill_along(start, end, bar.time);
868 }
869 }
870
871 fn fill_price(&self, order: &Working, start: f64, end: f64) -> Option<(f64, bool)> {
873 let reached = match order.kind {
874 Kind::RelativeExit(profit, loss, side) => {
875 let quantity = self.position();
876 if quantity * sign(side) <= 0.0 {
877 return None;
878 }
879 let average = self
880 .lots
881 .iter()
882 .map(|lot| lot.price * (lot.quantity / quantity.abs()))
883 .sum::<f64>();
884 let exit = Working {
885 id: String::new(),
886 quantity: None,
887 kind: Kind::Exit(
888 Some(average + sign(side) * profit),
889 Some(average - sign(side) * loss),
890 exit_side(quantity),
891 ),
892 deferred: order.deferred,
893 };
894 return self.fill_price(&exit, start, end);
895 }
896 Kind::Entry(side, order_type) => order_type.fill_price(side, start, end).map(|price| {
897 (
898 price,
899 matches!(order_type, OrderType::Market | OrderType::Stop(_)),
900 )
901 }),
902 Kind::Exit(take_profit, stop_loss, side) => {
903 let side = side?;
904 if take_profit.is_none() && stop_loss.is_none() {
905 Some((start, true))
906 } else {
907 [
909 (stop_loss.map(OrderType::Stop), true),
910 (take_profit.map(OrderType::Limit), false),
911 ]
912 .into_iter()
913 .filter_map(|(order_type, slips)| {
914 order_type?
915 .fill_price(side, start, end)
916 .map(|price| (price, slips))
917 })
918 .min_by(|(a, _), (b, _)| (a - start).abs().total_cmp(&(b - start).abs()))
919 }
920 }
921 }?;
922 if order.deferred == Some(self.tick) && reached.0 == start {
923 if start == end {
926 return None;
927 }
928 let next = Working {
929 id: String::new(),
930 quantity: order.quantity,
931 kind: order.kind,
932 deferred: None,
933 };
934 self.fill_price(&next, end, end)
935 } else {
936 Some(reached)
937 }
938 }
939
940 fn margin_call(&self, start: f64, end: f64) -> Option<f64> {
942 if self.lots.is_empty() {
943 return None;
944 }
945 let available = |price: f64| {
946 self.equity(price) - self.position().abs() * price.abs() / self.settings.leverage()
947 };
948 let mut from = start;
949 for to in [
951 if start.min(end) < 0.0 && start.max(end) > 0.0 {
952 0.0
953 } else {
954 end
955 },
956 end,
957 ] {
958 let a = available(from);
959 let b = available(to);
960 if a < 0.0 {
961 return Some(from);
962 }
963 if b < 0.0 {
964 return Some(from + (to - from) * (a / (a - b)));
965 }
966 from = to;
967 }
968 None
969 }
970
971 fn fill_along(&mut self, mut start: f64, end: f64, time: DateTime<Utc>) {
973 let mut at_end = false;
974 loop {
975 let mut next: Option<(usize, f64, bool)> = None;
976 for (index, order) in self.orders.iter().enumerate() {
977 if let Some((price, slips)) = self.fill_price(order, start, end)
978 && next.is_none_or(|(_, best, _)| (price - start).abs() < (best - start).abs())
979 {
980 next = Some((index, price, slips));
981 }
982 }
983 if let Some(price) = self.margin_call(start, end)
985 && next.is_none_or(|(_, best, _)| {
986 (price - start).abs() < (best - start).abs()
987 || (price == start
988 && self.equity(start)
989 < self.position().abs() * start.abs() / self.settings.leverage())
990 })
991 {
992 if price != start {
993 self.tick += 1;
994 at_end = price == end;
995 }
996 start = price;
997 self.fill(
998 Working {
999 id: "Margin call".into(),
1000 quantity: None,
1001 kind: Kind::Exit(None, None, exit_side(self.position())),
1002 deferred: None,
1003 },
1004 price,
1005 true,
1006 time,
1007 );
1008 if !self.lots.is_empty() {
1009 return;
1010 }
1011 continue;
1012 }
1013 let Some((index, price, slips)) = next else {
1014 if !at_end {
1015 self.tick += 1;
1017 start = end;
1018 at_end = true;
1019 continue;
1020 }
1021 return;
1022 };
1023 if price != start {
1024 self.tick += 1;
1025 at_end = price == end;
1026 }
1027 start = price;
1028 if let Kind::Entry(side, OrderType::StopLimit { limit, .. }) = self.orders[index].kind {
1029 self.orders[index].kind = Kind::Entry(side, OrderType::Limit(limit));
1030 self.orders[index].deferred = None;
1031 continue;
1032 }
1033 let order = self.orders.remove(index);
1034 self.fill(order, price, slips, time);
1035 }
1036 }
1037
1038 fn fill_markets(&mut self, price: f64, time: DateTime<Utc>) {
1040 let mut index = 0;
1041 while index < self.orders.len() {
1042 let order = &self.orders[index];
1043 let market = matches!(
1044 order.kind,
1045 Kind::Entry(_, OrderType::Market) | Kind::Exit(None, None, _)
1046 );
1047 if market && self.fill_price(order, price, price).is_some() {
1048 let order = self.orders.remove(index);
1049 self.fill(order, price, true, time);
1050 index = 0;
1051 } else {
1052 index += 1;
1053 }
1054 }
1055 if self.margin_call(price, price).is_some() {
1056 self.fill(
1057 Working {
1058 id: "Margin call".into(),
1059 quantity: None,
1060 kind: Kind::Exit(None, None, exit_side(self.position())),
1061 deferred: None,
1062 },
1063 price,
1064 true,
1065 time,
1066 );
1067 }
1068 }
1069
1070 fn fill(&mut self, order: Working, mark: f64, slips: bool, time: DateTime<Utc>) {
1071 let side = match order.kind {
1072 Kind::Entry(side, _) => side,
1073 Kind::RelativeExit(_, _, side) => {
1074 if side == OrderSide::Buy {
1075 OrderSide::Sell
1076 } else {
1077 OrderSide::Buy
1078 }
1079 }
1080 Kind::Exit(.., Some(side)) => side,
1081 Kind::Exit(.., None) => return,
1082 };
1083 let price = if slips {
1084 self.settings.slipped(mark, side)
1085 } else {
1086 mark
1087 };
1088 let held = self.position().abs();
1089 let order_id = self.fills.as_ref().map(|_| order.id.clone());
1090 let filled_quantity;
1091 match order.kind {
1092 Kind::Entry(..) => {
1093 let reversing = self.lots.first().is_some_and(|lot| lot.side != side);
1094 if self.settings.pyramiding == 0
1095 || (!reversing && self.lots.len() >= self.settings.pyramiding)
1096 {
1097 return;
1098 }
1099 let quantity = order
1100 .quantity
1101 .unwrap_or_else(|| match self.settings.order_size {
1102 OrderSize::Fixed(value) => value,
1103 OrderSize::EquityFraction(value) => self.equity(mark) * value / price.abs(),
1104 OrderSize::Cash(value) => value / price.abs(),
1105 });
1106 let filled = quantity + if reversing { held } else { 0.0 };
1107 let fee = self.settings.fee(filled, price);
1108 let after = quantity + if reversing { 0.0 } else { held };
1109 let equity = self.equity(mark) + sign(side) * (mark - price) * filled;
1110 let needed = after * mark.abs() / self.settings.leverage() + fee;
1111 if !quantity.is_finite()
1112 || quantity <= 0.0
1113 || !price.is_finite()
1114 || !needed.is_finite()
1115 || !equity.is_finite()
1116 || needed > equity
1117 {
1118 self.rejected += 1;
1119 return;
1120 }
1121 filled_quantity = filled;
1122 self.realized -= fee;
1123 if reversing {
1124 self.close(f64::INFINITY, &order.id, price, fee, filled, time);
1125 }
1126 self.lots.push(Lot {
1127 entry: order.id,
1128 side,
1129 quantity,
1130 entry_quantity: quantity,
1131 time,
1132 price,
1133 fees: fee * (quantity / filled),
1134 });
1135 }
1136 Kind::Exit(..) | Kind::RelativeExit(..) => {
1137 let quantity = order.quantity.unwrap_or(held).min(held);
1138 if quantity <= 0.0 {
1139 return;
1140 }
1141 let fee = self.settings.fee(quantity, price);
1142 if !price.is_finite() || !fee.is_finite() {
1143 return;
1144 }
1145 filled_quantity = quantity;
1146 self.realized -= fee;
1147 self.close(
1148 order.quantity.unwrap_or(f64::INFINITY),
1149 &order.id,
1150 price,
1151 fee,
1152 quantity,
1153 time,
1154 );
1155 if let Kind::RelativeExit(_, _, side) = order.kind
1156 && self.orders.iter().any(|working| {
1157 matches!(working.kind, Kind::Entry(entry_side, _) if entry_side == side)
1158 })
1159 {
1160 self.orders.push(order);
1161 }
1162 }
1163 }
1164 let position = self.position();
1165 if let Some(fills) = &mut self.fills {
1166 fills.push(StrategyFill {
1167 sequence: 0,
1168 time,
1169 order_id: order_id.unwrap_or_default(),
1170 side,
1171 price,
1172 quantity: filled_quantity,
1173 position,
1174 });
1175 }
1176 let pending = [OrderSide::Buy, OrderSide::Sell].map(|side| {
1177 self.orders
1178 .iter()
1179 .any(|order| matches!(order.kind, Kind::Entry(entry_side, _) if entry_side == side))
1180 });
1181 self.orders.retain_mut(|order| match &mut order.kind {
1182 Kind::Exit(.., side) => {
1183 *side = side.or(exit_side(position));
1184 side.is_none_or(|side| side.reduces(position))
1185 }
1186 Kind::Entry(..) => true,
1187 Kind::RelativeExit(_, _, side) => {
1188 position * sign(*side) > 0.0 || pending[usize::from(*side == OrderSide::Sell)]
1189 }
1190 });
1191 if self.settings.recalculate_after_fills {
1192 self.decide(price, true);
1193 }
1194 }
1195
1196 fn close(
1198 &mut self,
1199 mut quantity: f64,
1200 exit: &str,
1201 price: f64,
1202 fee: f64,
1203 filled_quantity: f64,
1204 time: DateTime<Utc>,
1205 ) {
1206 let exit_quantity = quantity;
1207 while quantity > 0.0
1208 && let Some(lot) = self.lots.first_mut()
1209 {
1210 let closed = quantity.min(lot.quantity);
1211 let entry_fee = if lot.quantity - closed <= rounding_tolerance(lot.entry_quantity, 1e-9)
1212 {
1213 lot.fees
1214 } else {
1215 lot.fees * (closed / lot.quantity)
1216 };
1217 let fees = entry_fee + fee * (closed / filled_quantity);
1218 let gross = sign(lot.side) * (price - lot.price) * closed;
1219 let mut trade = StrategyTrade::new(lot.side, closed, lot.time, lot.price);
1220 trade.exit = Some(StrategyExit::new(time, price, gross - fees + 0.0));
1221 trade.fees = fees;
1222 trade.label = Some(format!("{} / {exit}", lot.entry));
1223 self.trades.push(trade);
1224 self.realized += gross;
1225 quantity -= closed;
1226 lot.quantity -= closed;
1227 lot.fees -= entry_fee;
1228 if quantity.is_finite() && quantity <= rounding_tolerance(exit_quantity, 1e-9) {
1229 quantity = 0.0;
1230 }
1231 if lot.quantity <= rounding_tolerance(lot.entry_quantity, 1e-9) {
1232 self.lots.remove(0);
1233 }
1234 }
1235 }
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::*;
1241
1242 struct Script(fn(usize, &mut StrategyContext<'_>));
1244
1245 impl Strategy for Script {
1246 fn on_bar(&mut self, context: &mut StrategyContext<'_>) {
1247 if context.position() == 0.0 {
1248 assert!(context.position().is_sign_positive());
1249 }
1250 (self.0)(context.bars().len() - 1, context);
1251 }
1252 }
1253
1254 fn hour(hour: i64) -> DateTime<Utc> {
1255 DateTime::UNIX_EPOCH + TimeDelta::hours(hour)
1256 }
1257
1258 fn script(bar: usize, context: &mut StrategyContext<'_>) {
1259 use OrderSide::{Buy, Sell};
1260 match bar {
1261 0 => {
1262 context.entry("Long", Buy, 10.0, OrderType::Market);
1263 context.exit("Bracket", None, Some(105.0), Some(98.0));
1264 }
1265 2 => {
1266 let stop_limit = OrderType::StopLimit {
1267 stop: 102.0,
1268 limit: 102.5,
1269 };
1270 context.entry("Short", Sell, 5.0, stop_limit);
1271 context.entry("Long2", Buy, 4.0, OrderType::Limit(97.0));
1272 }
1273 3 => context.entry("Long2", Buy, 4.0, OrderType::Limit(100.0)),
1274 4 => {
1275 context.entry("Add", Buy, 2.0, OrderType::Market);
1276 context.exit("Part", Some(5.0), None, None);
1277 }
1278 5 => {
1279 context.exit("Flat", Some(100.0), None, None);
1280 context.entry("Last", Buy, 1.0, OrderType::Market);
1281 }
1282 6 => {
1283 context.exit("Guard", None, Some(110.0), Some(90.0));
1284 context.entry("Flip", Sell, 1.0, OrderType::Market);
1285 }
1286 7 => context.exit("Cover", None, Some(96.0), Some(103.0)),
1287 8 => {
1288 context.entry("Long3", Buy, 1.0, OrderType::Market);
1289 context.exit("Target", None, Some(104.0), Some(90.0));
1290 }
1291 9 => context.exit("Out", None, None, None),
1292 10 => context.entry("Long4", Buy, 1.0, OrderType::Market),
1293 11 => {
1294 context.entry("Reverse", Sell, 1.0, OrderType::Stop(103.0));
1295 context.exit("Stop", None, Some(108.0), Some(103.0));
1296 }
1297 _ => {}
1298 }
1299 }
1300
1301 fn stop_first(bar: usize, context: &mut StrategyContext<'_>) {
1303 if bar == 11 {
1304 context.exit("Stop", None, Some(108.0), Some(103.0));
1305 }
1306 script(bar, context);
1307 }
1308
1309 #[test]
1311 fn run_fills_along_the_bar_path_and_pairs_trades_first_in_first_out() {
1312 let bars: Vec<_> = [
1313 (100.0, 101.0, 99.0, 100.0),
1314 (101.0, 103.0, 100.0, 102.0),
1316 (102.0, 106.0, 97.0, 105.0),
1318 (104.0, 105.0, 101.0, 103.0),
1321 (98.0, 100.0, 95.0, 99.0),
1324 (99.0, 101.0, 98.0, 100.0),
1326 (100.0, 101.0, 99.0, 100.5),
1328 (100.0, 101.0, 99.0, 100.0),
1331 (100.0, 105.0, 96.0, 97.0),
1333 (97.0, 98.0, 96.0, 97.5),
1335 (98.0, 99.0, 97.0, 98.0),
1337 (105.0, 106.0, 104.5, 105.5),
1339 (105.0, 108.5, 102.0, 102.5),
1342 ]
1343 .into_iter()
1344 .enumerate()
1345 .map(|(i, (open, high, low, close))| Bar::new(hour(i as i64), open, high, low, close, 0.0))
1346 .collect();
1347 let mut backtest = Backtest::new("Script", "TEST", Timeframe::Hour1, 10_000.0);
1348 backtest.settings.pyramiding = usize::MAX;
1349 let report = backtest.run(&mut Script(script), &bars, &[]);
1350
1351 let trade = |side,
1352 quantity,
1353 entry: i64,
1354 entry_price,
1355 exit: Option<(i64, f64, f64)>,
1356 label: &str| {
1357 let mut trade = StrategyTrade::new(side, quantity, hour(entry), entry_price);
1358 trade.exit =
1359 exit.map(|(time, price, profit)| StrategyExit::new(hour(time), price, profit));
1360 trade.label = Some(label.to_owned());
1361 trade
1362 };
1363 use OrderSide::{Buy, Sell};
1364 assert_eq!(
1365 report.trades,
1366 [
1367 trade(
1368 Buy,
1369 10.0,
1370 1,
1371 101.0,
1372 Some((2, 105.0, 40.0)),
1373 "Long / Bracket"
1374 ),
1375 trade(Sell, 5.0, 3, 102.5, Some((4, 98.0, 22.5)), "Short / Long2"),
1376 trade(Buy, 4.0, 4, 98.0, Some((5, 99.0, 4.0)), "Long2 / Part"),
1377 trade(Buy, 1.0, 5, 99.0, Some((5, 99.0, 0.0)), "Add / Part"),
1378 trade(Buy, 1.0, 5, 99.0, Some((6, 100.0, 1.0)), "Add / Flat"),
1379 trade(Buy, 1.0, 6, 100.0, Some((7, 100.0, 0.0)), "Last / Flip"),
1380 trade(Sell, 1.0, 7, 100.0, Some((8, 96.0, 4.0)), "Flip / Cover"),
1381 trade(Buy, 1.0, 9, 97.0, Some((10, 98.0, 1.0)), "Long3 / Out"),
1382 trade(
1383 Buy,
1384 1.0,
1385 11,
1386 105.0,
1387 Some((12, 103.0, -2.0)),
1388 "Long4 / Reverse"
1389 ),
1390 trade(Sell, 1.0, 12, 103.0, None, "Reverse"),
1391 ]
1392 );
1393 let equity = [
1394 10_000.0, 10_000.0, 10_010.0, 10_040.0, 10_037.5, 10_066.5, 10_067.5, 10_068.0,
1395 10_067.5, 10_071.5, 10_072.0, 10_072.5, 10_073.0, 10_071.0,
1396 ];
1397 let times = std::iter::once(hour(-1)).chain((0..13).map(hour));
1398 assert_eq!(report.equity, times.zip(equity).collect::<Vec<_>>());
1399 assert_eq!(report.equity_curve(), report.equity);
1400 let metrics = report.performance();
1402 assert_eq!(
1403 (
1404 metrics.closed_trades,
1405 metrics.open_trades,
1406 metrics.skipped_trades
1407 ),
1408 (9, 1, 0)
1409 );
1410 assert_eq!((metrics.winning_trades, metrics.losing_trades), (6, 1));
1411 assert_eq!(
1412 (metrics.net_profit, metrics.gross_profit, metrics.gross_loss),
1413 (70.5, 72.5, 2.0)
1414 );
1415 assert_eq!(metrics.win_rate, Some(6.0 / 9.0));
1416 assert_eq!(metrics.profit_factor, Some(36.25));
1417
1418 let first = backtest.run(&mut Script(script), &bars[..4], &[]);
1420 assert_eq!(first.equity, report.equity[..5]);
1421 assert_eq!(first.trades[0], report.trades[0]);
1422 assert_eq!(first.trades[1], trade(Sell, 5.0, 3, 102.5, None, "Short"));
1423
1424 let stop_first = backtest.run(&mut Script(stop_first), &bars, &[]);
1426 assert_eq!(stop_first.equity, report.equity);
1427 let mut trades = report.trades.clone();
1428 trades[8].label = Some("Long4 / Stop".to_owned());
1429 assert_eq!(stop_first.trades, trades);
1430
1431 let mut on_close = backtest.clone();
1433 on_close.settings.fill_on_close = true;
1434 let on_close = on_close.run(&mut Script(script), &bars, &[]);
1435 assert_eq!(
1436 on_close.trades[0],
1437 trade(
1438 Buy,
1439 10.0,
1440 0,
1441 100.0,
1442 Some((2, 105.0, 50.0)),
1443 "Long / Bracket"
1444 )
1445 );
1446
1447 fn decimals(bar: usize, context: &mut StrategyContext<'_>) {
1448 match bar {
1449 0 => {
1450 context.entry("A", Buy, 0.1, OrderType::Market);
1451 context.entry("B", Buy, 0.2, OrderType::Market);
1452 }
1453 1 => context.exit("X", Some(0.3), None, None),
1454 _ => {}
1455 }
1456 }
1457 fn thirds(bar: usize, context: &mut StrategyContext<'_>) {
1458 match bar {
1459 0 => context.entry("A", Buy, 0.3, OrderType::Market),
1460 1 => context.entry("B", Buy, 0.5, OrderType::Market),
1461 2 => context.exit("X1", Some(0.1), None, None),
1462 3 => context.exit("X2", Some(0.1), None, None),
1463 4 => context.exit("X3", Some(0.1), None, None),
1464 _ => {}
1465 }
1466 }
1467 fn tenths(bar: usize, context: &mut StrategyContext<'_>) {
1468 match bar {
1469 0 => context.entry("A", Buy, 1.0, OrderType::Market),
1470 1..=10 => context.exit("X", Some(0.1), None, None),
1471 11 => assert_eq!(context.position(), 0.0),
1472 _ => {}
1473 }
1474 }
1475 fn pyramided_scale_out(bar: usize, context: &mut StrategyContext<'_>) {
1476 match bar {
1477 0 => {
1478 context.entry("A", Buy, 1.4, OrderType::Market);
1479 context.entry("B", Buy, 3.2, OrderType::Market);
1480 }
1481 1..=14 => context.exit("X", Some(0.1), None, None),
1482 15..=78 => context.exit("X", Some(0.05), None, None),
1483 79 => assert_eq!(context.position(), 0.0),
1484 _ => {}
1485 }
1486 }
1487 fn decimal_tie(bar: usize, context: &mut StrategyContext<'_>) {
1488 if bar == 0 {
1489 context.entry("L", Buy, 1.0, OrderType::Market);
1490 context.exit("B", None, Some(1.2), Some(1.1));
1491 }
1492 }
1493 fn invalid_range(bar: usize, context: &mut StrategyContext<'_>) {
1494 if bar == 0 {
1495 context.entry("L", Buy, 1.0, OrderType::Limit(104.0));
1496 }
1497 }
1498 fn short_breakeven(bar: usize, context: &mut StrategyContext<'_>) {
1499 match bar {
1500 0 => context.entry("S", Sell, 1.0, OrderType::Market),
1501 1 => context.exit("X", None, None, None),
1502 _ => {}
1503 }
1504 }
1505 fn low_first_stop(bar: usize, context: &mut StrategyContext<'_>) {
1506 match bar {
1507 0 => context.entry("L", Buy, 1.0, OrderType::Market),
1508 1 => context.exit("B", None, Some(104.0), Some(98.0)),
1509 _ => {}
1510 }
1511 }
1512 fn short_stop(bar: usize, context: &mut StrategyContext<'_>) {
1513 match bar {
1514 0 => context.entry("S", Sell, 1.0, OrderType::Market),
1515 1 => context.exit("B", None, Some(97.0), Some(101.0)),
1516 _ => {}
1517 }
1518 }
1519 fn stop_limit_gap(bar: usize, context: &mut StrategyContext<'_>) {
1520 if bar == 0 {
1521 context.entry(
1522 "G",
1523 Buy,
1524 1.0,
1525 OrderType::StopLimit {
1526 stop: 102.0,
1527 limit: 103.0,
1528 },
1529 );
1530 }
1531 }
1532 fn replaced_first(bar: usize, context: &mut StrategyContext<'_>) {
1533 if bar == 0 {
1534 context.entry("A", Buy, 1.0, OrderType::Market);
1535 context.entry("B", Buy, 1.0, OrderType::Market);
1536 context.exit("X", Some(1.0), None, None);
1537 context.entry("A", Buy, 2.0, OrderType::Market);
1538 }
1539 }
1540 for (script, prices, expected, equity, closed, open, wins, profit) in [
1547 (
1548 decimals as fn(usize, &mut StrategyContext<'_>),
1549 vec![(100.0, 100.0, 100.0, 100.0); 4],
1550 vec![
1551 trade(Buy, 0.1, 1, 100.0, Some((2, 100.0, 0.0)), "A / X"),
1552 trade(Buy, 0.2, 1, 100.0, Some((2, 100.0, 0.0)), "B / X"),
1553 ],
1554 vec![10_000.0; 5],
1555 2,
1556 0,
1557 0,
1558 0.0,
1559 ),
1560 (
1561 thirds,
1562 [100.0, 100.0, 104.0, 102.0, 102.0, 102.0]
1563 .into_iter()
1564 .map(|p| (p, p, p, p))
1565 .collect(),
1566 vec![
1567 trade(Buy, 0.1, 1, 100.0, Some((3, 102.0, 0.2)), "A / X1"),
1568 trade(Buy, 0.1, 1, 100.0, Some((4, 102.0, 0.2)), "A / X2"),
1569 trade(Buy, 0.1, 1, 100.0, Some((5, 102.0, 0.2)), "A / X3"),
1570 trade(Buy, 0.5, 2, 104.0, None, "B"),
1571 ],
1572 vec![
1573 10_000.0, 10_000.0, 10_000.0, 10_001.2, 9999.6, 9999.6, 9999.6,
1574 ],
1575 3,
1576 1,
1577 3,
1578 0.6,
1579 ),
1580 (
1581 tenths,
1583 vec![(100.0, 100.0, 100.0, 100.0); 12],
1584 (2..=11)
1585 .map(|exit| trade(Buy, 0.1, 1, 100.0, Some((exit, 100.0, 0.0)), "A / X"))
1586 .collect(),
1587 vec![10_000.0; 13],
1588 10,
1589 0,
1590 0,
1591 0.0,
1592 ),
1593 (
1594 pyramided_scale_out,
1596 vec![(100.0, 100.0, 100.0, 100.0); 80],
1597 (2..=79)
1598 .map(|exit| {
1599 let (quantity, label) = if exit <= 15 {
1600 (0.1, "A / X")
1601 } else {
1602 (0.05, "B / X")
1603 };
1604 trade(Buy, quantity, 1, 100.0, Some((exit, 100.0, 0.0)), label)
1605 })
1606 .collect(),
1607 vec![10_000.0; 81],
1608 78,
1609 0,
1610 0,
1611 0.0,
1612 ),
1613 (
1614 decimal_tie,
1615 vec![
1616 (1.15, 1.16, 1.14, 1.15),
1617 (1.15, 1.16, 1.14, 1.15),
1618 (1.15, 1.2, 1.1, 1.15),
1619 ],
1620 vec![trade(Buy, 1.0, 1, 1.15, Some((2, 1.2, 0.05)), "L / B")],
1621 vec![10_000.0, 10_000.0, 10_000.0, 10_000.05],
1622 1,
1623 0,
1624 1,
1625 0.05,
1626 ),
1627 (
1628 invalid_range,
1629 vec![
1630 (100.0, 101.0, 99.0, 100.0),
1631 (105.0, 101.0, 99.0, 100.0),
1632 (100.0, 101.0, 99.0, 102.0),
1633 (103.0, 104.0, 102.0, 103.0),
1634 ],
1635 vec![trade(Buy, 1.0, 3, 103.0, None, "L")],
1636 vec![10_000.0, 10_000.0, f64::NAN, f64::NAN, 10_000.0],
1637 0,
1638 1,
1639 0,
1640 0.0,
1641 ),
1642 (
1644 short_breakeven,
1645 vec![(100.0, 100.0, 100.0, 100.0); 3],
1646 vec![trade(Sell, 1.0, 1, 100.0, Some((2, 100.0, 0.0)), "S / X")],
1647 vec![10_000.0; 4],
1648 1,
1649 0,
1650 0,
1651 0.0,
1652 ),
1653 (
1655 low_first_stop,
1656 vec![(90.0, 90.0, 90.0, 90.0); 2]
1657 .into_iter()
1658 .chain([(100.0, 105.0, 97.0, 100.0)])
1659 .collect(),
1660 vec![trade(Buy, 1.0, 1, 90.0, Some((2, 98.0, 8.0)), "L / B")],
1661 vec![10_000.0, 10_000.0, 10_000.0, 10_008.0],
1662 1,
1663 0,
1664 1,
1665 8.0,
1666 ),
1667 (
1669 short_stop,
1670 vec![(110.0, 110.0, 110.0, 110.0); 2]
1671 .into_iter()
1672 .chain([(100.0, 102.0, 96.0, 100.0)])
1673 .collect(),
1674 vec![trade(Sell, 1.0, 1, 110.0, Some((2, 101.0, 9.0)), "S / B")],
1675 vec![10_000.0, 10_000.0, 10_000.0, 10_009.0],
1676 1,
1677 0,
1678 1,
1679 9.0,
1680 ),
1681 (decimals, vec![], vec![], vec![], 0, 0, 0, 0.0),
1683 (
1684 decimals,
1685 vec![(100.0, 100.0, 100.0, 100.0)],
1686 vec![],
1687 vec![10_000.0; 2],
1688 0,
1689 0,
1690 0,
1691 0.0,
1692 ),
1693 (
1695 invalid_range,
1696 vec![
1697 (100.0, 101.0, 99.0, 100.0),
1698 (100.0, f64::NAN, 99.0, 100.0),
1699 (103.0, 104.0, 102.0, 103.0),
1700 ],
1701 vec![trade(Buy, 1.0, 2, 103.0, None, "L")],
1702 vec![10_000.0, 10_000.0, f64::NAN, 10_000.0],
1703 0,
1704 1,
1705 0,
1706 0.0,
1707 ),
1708 (
1711 stop_limit_gap,
1712 vec![
1713 (100.0, 100.0, 100.0, 100.0),
1714 (105.0, 106.0, 104.0, 105.0),
1715 (104.0, 105.0, 102.0, 104.0),
1716 ],
1717 vec![trade(Buy, 1.0, 2, 103.0, None, "G")],
1718 vec![10_000.0, 10_000.0, 10_000.0, 10_001.0],
1719 0,
1720 1,
1721 0,
1722 0.0,
1723 ),
1724 (
1727 replaced_first,
1728 vec![(100.0, 100.0, 100.0, 100.0); 2],
1729 vec![
1730 trade(Buy, 1.0, 1, 100.0, Some((1, 100.0, 0.0)), "A / X"),
1731 trade(Buy, 1.0, 1, 100.0, None, "A"),
1732 trade(Buy, 1.0, 1, 100.0, None, "B"),
1733 ],
1734 vec![10_000.0; 3],
1735 1,
1736 2,
1737 0,
1738 0.0,
1739 ),
1740 ] {
1741 let source: Vec<_> = prices
1742 .into_iter()
1743 .enumerate()
1744 .map(|(i, (open, high, low, close))| {
1745 Bar::new(hour(i as i64), open, high, low, close, 0.0)
1746 })
1747 .collect();
1748 let report = backtest.run(&mut Script(script), &source, &[]);
1749 assert_eq!(report.trades.len(), expected.len());
1750 for (mut actual, expected) in report.trades.iter().cloned().zip(expected) {
1751 assert!((actual.quantity - expected.quantity).abs() < 1e-15);
1752 actual.quantity = expected.quantity;
1753 if let (Some(actual), Some(expected)) = (&mut actual.exit, &expected.exit) {
1754 assert!((actual.profit - expected.profit).abs() < 1e-15);
1755 if actual.profit == 0.0 {
1756 assert!(actual.profit.is_sign_positive());
1757 }
1758 actual.profit = expected.profit;
1759 }
1760 assert_eq!(actual, expected);
1761 }
1762 assert_eq!(report.equity.len(), equity.len());
1763 let skipped = equity.iter().filter(|value| value.is_nan()).count();
1764 for ((time, actual), (expected_time, expected)) in report
1765 .equity
1766 .iter()
1767 .zip((-1..source.len() as i64).map(hour).zip(equity))
1768 {
1769 assert_eq!(*time, expected_time);
1770 assert!(*actual == expected || (actual.is_nan() && expected.is_nan()));
1771 }
1772 assert_eq!(report.performance().skipped_equity_points, skipped);
1773 assert_eq!(report.comparison(&source).skipped_bars, skipped);
1774 let metrics = report.performance();
1775 assert_eq!(
1776 (
1777 metrics.closed_trades,
1778 metrics.open_trades,
1779 metrics.winning_trades,
1780 metrics.losing_trades
1781 ),
1782 (closed, open, wins, 0)
1783 );
1784 assert!((metrics.net_profit - profit).abs() < 1e-15);
1785 assert_eq!(
1786 metrics.win_rate,
1787 (closed > 0).then(|| wins as f64 / closed as f64)
1788 );
1789 assert_eq!(metrics.profit_factor, None);
1790 #[cfg(feature = "csv")]
1791 assert!(!report.trades_csv().contains(",-0"));
1792 }
1793
1794 fn sized(bar: usize, context: &mut StrategyContext<'_>) {
1795 match bar {
1796 0 => context.entry("A", Buy, None, OrderType::Market),
1797 1 => context.exit("X", None, None, None),
1798 _ => {}
1799 }
1800 }
1801 fn override_size(bar: usize, context: &mut StrategyContext<'_>) {
1802 if bar == 0 {
1803 context.entry("A", Buy, 4.0, OrderType::Market);
1804 } else if bar == 1 {
1805 context.exit("X", None, None, None);
1806 }
1807 }
1808 fn repeat(bar: usize, context: &mut StrategyContext<'_>) {
1809 match bar {
1810 0 => context.entry("A", Buy, None, OrderType::Market),
1811 1 => context.exit("X", None, None, None),
1812 2 => context.entry("B", Buy, None, OrderType::Market),
1813 _ => {}
1814 }
1815 }
1816 fn stack_in_time(bar: usize, context: &mut StrategyContext<'_>) {
1817 match bar {
1818 0 => context.entry("A", Buy, None, OrderType::Market),
1819 1 => context.entry("B", Buy, None, OrderType::Market),
1820 _ => {}
1821 }
1822 }
1823 fn stack(bar: usize, context: &mut StrategyContext<'_>) {
1824 if bar == 0 {
1825 context.entry("A", Buy, None, OrderType::Market);
1826 context.entry("B", Buy, None, OrderType::Market);
1827 } else if bar == 1 {
1828 context.entry("C", Buy, None, OrderType::Market);
1829 }
1830 }
1831 fn hold(bar: usize, context: &mut StrategyContext<'_>) {
1832 if bar == 0 {
1833 context.entry("A", Buy, None, OrderType::Market);
1834 }
1835 }
1836 fn hold_short(bar: usize, context: &mut StrategyContext<'_>) {
1837 if bar == 0 {
1838 context.entry("A", Sell, None, OrderType::Market);
1839 }
1840 }
1841 fn split(bar: usize, context: &mut StrategyContext<'_>) {
1842 if bar == 0 {
1843 context.entry("A", Buy, 1.0, OrderType::Market);
1844 context.entry("B", Buy, 3.0, OrderType::Market);
1845 } else if bar == 1 {
1846 context.exit("X", Some(2.0), None, None);
1847 }
1848 }
1849 fn reverse(bar: usize, context: &mut StrategyContext<'_>) {
1850 match bar {
1851 0 => context.entry("A", Buy, 2.0, OrderType::Market),
1852 1 => context.entry("B", Sell, 3.0, OrderType::Market),
1853 2 => context.exit("X", None, None, None),
1854 _ => {}
1855 }
1856 }
1857 fn stops(bar: usize, context: &mut StrategyContext<'_>) {
1858 if bar == 0 {
1859 context.entry("A", Buy, None, OrderType::Stop(105.0));
1860 context.exit("X", None, None, Some(95.0));
1861 }
1862 }
1863 fn limits(bar: usize, context: &mut StrategyContext<'_>) {
1864 if bar == 0 {
1865 context.entry("A", Buy, None, OrderType::Limit(100.0));
1866 context.exit("X", None, Some(110.0), None);
1867 }
1868 }
1869 fn recalculate(bar: usize, context: &mut StrategyContext<'_>) {
1870 if context.is_after_fill() {
1871 assert_eq!(
1872 context.bars().len(),
1873 1,
1874 "current bar must not leak future prices"
1875 );
1876 if context.position() > 0.0 {
1877 assert_eq!(context.price(), 100.0);
1878 assert_eq!(context.equity(), 1000.0);
1879 context.exit("X", None, Some(context.price() + 5.0), Some(98.0));
1880 } else {
1881 assert_eq!(context.price(), 105.0);
1882 }
1883 } else if bar == 0 {
1884 context.entry("A", Buy, None, OrderType::Market);
1885 }
1886 }
1887 fn cycle(bar: usize, context: &mut StrategyContext<'_>) {
1888 if context.is_after_fill() {
1889 if context.position() > 0.0 {
1890 context.exit("X", None, None, None);
1891 } else {
1892 context.entry("A", Buy, None, OrderType::Market);
1893 }
1894 } else if bar == 0 {
1895 context.entry("A", Buy, None, OrderType::Market);
1896 }
1897 }
1898 fn sized_short(bar: usize, context: &mut StrategyContext<'_>) {
1899 if bar == 0 {
1900 context.entry("A", Sell, None, OrderType::Market);
1901 } else if bar == 1 {
1902 context.exit("X", None, None, None);
1903 }
1904 }
1905 fn stop_limits(bar: usize, context: &mut StrategyContext<'_>) {
1906 if bar == 0 {
1907 context.entry(
1908 "A",
1909 Buy,
1910 None,
1911 OrderType::StopLimit {
1912 stop: 105.0,
1913 limit: 106.0,
1914 },
1915 );
1916 context.exit("X", None, Some(110.0), None);
1917 }
1918 }
1919 fn boundary_stop(bar: usize, context: &mut StrategyContext<'_>) {
1920 match bar {
1921 0 => context.entry("A", Buy, None, OrderType::Market),
1922 1 => context.exit("X", None, None, Some(100.0)),
1923 _ => {}
1924 }
1925 }
1926 let terms = BacktestSettings {
1927 initial_capital: 1000.0,
1928 currency: Some("USD".into()),
1929 risk_free_rate: 0.02,
1930 order_size: OrderSize::Fixed(2.0),
1931 ..Default::default()
1932 };
1933 let flat = |prices: &[f64]| prices.iter().map(|&p| (p, p, p, p)).collect::<Vec<_>>();
1934 let fee_trade = |side, quantity, entry, entry_price, exit, fees, label| {
1935 let mut result = trade(side, quantity, entry, entry_price, exit, label);
1936 result.fees = fees;
1937 result
1938 };
1939 for (name, script, settings, prices, expected, equity, rejected) in [
1944 (
1945 "fixed size",
1946 sized as fn(usize, &mut StrategyContext<'_>),
1947 terms.clone(),
1948 flat(&[100.0, 100.0, 110.0]),
1949 vec![trade(Buy, 2.0, 1, 100.0, Some((2, 110.0, 20.0)), "A / X")],
1950 vec![1000.0, 1000.0, 1000.0, 1020.0],
1951 0,
1952 ),
1953 (
1954 "cash size",
1955 sized,
1956 BacktestSettings {
1957 order_size: OrderSize::Cash(200.0),
1958 ..terms.clone()
1959 },
1960 flat(&[100.0, 125.0, 130.0]),
1961 vec![trade(Buy, 1.6, 1, 125.0, Some((2, 130.0, 8.0)), "A / X")],
1962 vec![1000.0, 1000.0, 1000.0, 1008.0],
1963 0,
1964 ),
1965 (
1966 "equity size",
1967 sized,
1968 BacktestSettings {
1969 order_size: OrderSize::EquityFraction(0.5),
1970 ..terms.clone()
1971 },
1972 flat(&[100.0, 100.0, 110.0]),
1973 vec![trade(Buy, 5.0, 1, 100.0, Some((2, 110.0, 50.0)), "A / X")],
1974 vec![1000.0, 1000.0, 1000.0, 1050.0],
1975 0,
1976 ),
1977 (
1978 "current equity size",
1979 repeat,
1980 BacktestSettings {
1981 order_size: OrderSize::EquityFraction(0.5),
1982 ..terms.clone()
1983 },
1984 flat(&[100.0, 100.0, 120.0, 100.0, 110.0]),
1985 vec![
1986 trade(Buy, 5.0, 1, 100.0, Some((2, 120.0, 100.0)), "A / X"),
1987 trade(Buy, 5.5, 3, 100.0, None, "B"),
1988 ],
1989 vec![1000.0, 1000.0, 1000.0, 1100.0, 1100.0, 1155.0],
1990 0,
1991 ),
1992 (
1994 "equity sizing marks existing lots",
1995 stack_in_time,
1996 BacktestSettings {
1997 order_size: OrderSize::EquityFraction(0.5),
1998 pyramiding: 2,
1999 leverage: 2.0,
2000 ..terms.clone()
2001 },
2002 flat(&[100.0, 100.0, 120.0]),
2003 vec![
2004 trade(Buy, 5.0, 1, 100.0, None, "A"),
2005 trade(Buy, 55.0 / 12.0, 2, 120.0, None, "B"),
2006 ],
2007 vec![1000.0, 1000.0, 1000.0, 1100.0],
2008 0,
2009 ),
2010 (
2011 "cash cannot size at zero",
2012 hold,
2013 BacktestSettings {
2014 order_size: OrderSize::Cash(200.0),
2015 ..terms.clone()
2016 },
2017 flat(&[100.0, 0.0, 0.0]),
2018 vec![],
2019 vec![1000.0; 4],
2020 1,
2021 ),
2022 (
2023 "quantity override",
2024 override_size,
2025 terms.clone(),
2026 flat(&[100.0, 100.0, 110.0]),
2027 vec![trade(Buy, 4.0, 1, 100.0, Some((2, 110.0, 40.0)), "A / X")],
2028 vec![1000.0, 1000.0, 1000.0, 1040.0],
2029 0,
2030 ),
2031 (
2032 "default pyramiding",
2033 stack,
2034 terms.clone(),
2035 flat(&[100.0, 100.0, 110.0]),
2036 vec![trade(Buy, 2.0, 1, 100.0, None, "A")],
2037 vec![1000.0, 1000.0, 1000.0, 1020.0],
2038 0,
2039 ),
2040 (
2041 "two entries",
2042 stack,
2043 BacktestSettings {
2044 pyramiding: 2,
2045 ..terms.clone()
2046 },
2047 flat(&[100.0, 100.0, 110.0]),
2048 vec![
2049 trade(Buy, 2.0, 1, 100.0, None, "A"),
2050 trade(Buy, 2.0, 1, 100.0, None, "B"),
2051 ],
2052 vec![1000.0, 1000.0, 1000.0, 1040.0],
2053 0,
2054 ),
2055 (
2056 "disabled entries",
2057 stack,
2058 BacktestSettings {
2059 pyramiding: 0,
2060 ..terms.clone()
2061 },
2062 flat(&[100.0, 100.0, 110.0]),
2063 vec![],
2064 vec![1000.0; 4],
2065 0,
2066 ),
2067 (
2068 "fraction commission",
2069 sized,
2070 BacktestSettings {
2071 commission: Commission::Fraction(0.01),
2072 ..terms.clone()
2073 },
2074 flat(&[100.0, 100.0, 110.0]),
2075 vec![fee_trade(
2076 Buy,
2077 2.0,
2078 1,
2079 100.0,
2080 Some((2, 110.0, 15.8)),
2081 4.2,
2082 "A / X",
2083 )],
2084 vec![1000.0, 1000.0, 998.0, 1015.8],
2085 0,
2086 ),
2087 (
2088 "unit commission",
2089 sized,
2090 BacktestSettings {
2091 commission: Commission::PerUnit(0.5),
2092 ..terms.clone()
2093 },
2094 flat(&[100.0, 100.0, 110.0]),
2095 vec![fee_trade(
2096 Buy,
2097 2.0,
2098 1,
2099 100.0,
2100 Some((2, 110.0, 18.0)),
2101 2.0,
2102 "A / X",
2103 )],
2104 vec![1000.0, 1000.0, 999.0, 1018.0],
2105 0,
2106 ),
2107 (
2108 "order commission",
2109 sized,
2110 BacktestSettings {
2111 commission: Commission::PerOrder(2.0),
2112 ..terms.clone()
2113 },
2114 flat(&[100.0, 100.0, 110.0]),
2115 vec![fee_trade(
2116 Buy,
2117 2.0,
2118 1,
2119 100.0,
2120 Some((2, 110.0, 16.0)),
2121 4.0,
2122 "A / X",
2123 )],
2124 vec![1000.0, 1000.0, 998.0, 1016.0],
2125 0,
2126 ),
2127 (
2128 "fees shared across FIFO lots",
2129 split,
2130 BacktestSettings {
2131 commission: Commission::PerOrder(4.0),
2132 pyramiding: 2,
2133 ..terms.clone()
2134 },
2135 flat(&[100.0, 100.0, 110.0]),
2136 vec![
2137 fee_trade(Buy, 1.0, 1, 100.0, Some((2, 110.0, 4.0)), 6.0, "A / X"),
2138 fee_trade(
2139 Buy,
2140 1.0,
2141 1,
2142 100.0,
2143 Some((2, 110.0, 20.0 / 3.0)),
2144 10.0 / 3.0,
2145 "B / X",
2146 ),
2147 fee_trade(Buy, 2.0, 1, 100.0, None, 8.0 / 3.0, "B"),
2148 ],
2149 vec![1000.0, 1000.0, 992.0, 1028.0],
2150 0,
2151 ),
2152 (
2153 "reversal is one fill",
2154 reverse,
2155 BacktestSettings {
2156 commission: Commission::PerOrder(4.0),
2157 ..terms.clone()
2158 },
2159 flat(&[100.0, 100.0, 110.0, 100.0]),
2160 vec![
2161 fee_trade(Buy, 2.0, 1, 100.0, Some((2, 110.0, 14.4)), 5.6, "A / B"),
2162 fee_trade(Sell, 3.0, 2, 110.0, Some((3, 100.0, 23.6)), 6.4, "B / X"),
2163 ],
2164 vec![1000.0, 1000.0, 996.0, 1012.0, 1038.0],
2165 0,
2166 ),
2167 (
2168 "tick slippage",
2169 sized,
2170 BacktestSettings {
2171 slippage: Slippage::Ticks {
2172 ticks: 2,
2173 tick_size: 0.5,
2174 },
2175 ..terms.clone()
2176 },
2177 flat(&[100.0, 100.0, 110.0]),
2178 vec![trade(Buy, 2.0, 1, 101.0, Some((2, 109.0, 16.0)), "A / X")],
2179 vec![1000.0, 1000.0, 998.0, 1016.0],
2180 0,
2181 ),
2182 (
2183 "fraction slippage",
2184 sized,
2185 BacktestSettings {
2186 slippage: Slippage::Fraction(0.01),
2187 ..terms.clone()
2188 },
2189 flat(&[100.0, 100.0, 110.0]),
2190 vec![trade(Buy, 2.0, 1, 101.0, Some((2, 108.9, 15.8)), "A / X")],
2191 vec![1000.0, 1000.0, 998.0, 1015.8],
2192 0,
2193 ),
2194 (
2195 "stop slippage",
2196 stops,
2197 BacktestSettings {
2198 slippage: Slippage::Ticks {
2199 ticks: 2,
2200 tick_size: 0.5,
2201 },
2202 ..terms.clone()
2203 },
2204 vec![
2205 (100.0, 100.0, 100.0, 100.0),
2206 (100.0, 110.0, 99.0, 108.0),
2207 (108.0, 109.0, 90.0, 100.0),
2208 ],
2209 vec![trade(Buy, 2.0, 1, 106.0, Some((2, 94.0, -24.0)), "A / X")],
2210 vec![1000.0, 1000.0, 1004.0, 976.0],
2211 0,
2212 ),
2213 (
2214 "limits never slip",
2215 limits,
2216 BacktestSettings {
2217 slippage: Slippage::Fraction(0.1),
2218 ..terms.clone()
2219 },
2220 vec![
2221 (100.0, 100.0, 100.0, 100.0),
2222 (101.0, 103.0, 99.0, 102.0),
2223 (108.0, 111.0, 107.0, 110.0),
2224 ],
2225 vec![trade(Buy, 2.0, 1, 100.0, Some((2, 110.0, 20.0)), "A / X")],
2226 vec![1000.0, 1000.0, 1004.0, 1020.0],
2227 0,
2228 ),
2229 (
2230 "insufficient margin",
2231 hold,
2232 BacktestSettings {
2233 order_size: OrderSize::Fixed(11.0),
2234 ..terms.clone()
2235 },
2236 flat(&[100.0, 100.0, 110.0]),
2237 vec![],
2238 vec![1000.0; 4],
2239 1,
2240 ),
2241 (
2242 "commission enters buying power",
2243 hold,
2244 BacktestSettings {
2245 order_size: OrderSize::Fixed(10.0),
2246 commission: Commission::PerOrder(1.0),
2247 ..terms.clone()
2248 },
2249 flat(&[100.0, 100.0, 110.0]),
2250 vec![],
2251 vec![1000.0; 4],
2252 1,
2253 ),
2254 (
2255 "leverage",
2256 hold,
2257 BacktestSettings {
2258 order_size: OrderSize::Fixed(15.0),
2259 leverage: 2.0,
2260 ..terms.clone()
2261 },
2262 flat(&[100.0, 100.0, 110.0]),
2263 vec![trade(Buy, 15.0, 1, 100.0, None, "A")],
2264 vec![1000.0, 1000.0, 1000.0, 1150.0],
2265 0,
2266 ),
2267 (
2268 "stop at the margin boundary averts a call",
2269 boundary_stop,
2270 BacktestSettings {
2271 order_size: OrderSize::Fixed(20.0),
2272 leverage: 2.0,
2273 ..terms.clone()
2274 },
2275 vec![
2276 (100.0, 100.0, 100.0, 100.0),
2277 (100.0, 100.0, 100.0, 100.0),
2278 (101.0, 102.0, 90.0, 100.0),
2279 ],
2280 vec![trade(Buy, 20.0, 1, 100.0, Some((2, 100.0, 0.0)), "A / X")],
2281 vec![1000.0; 4],
2282 0,
2283 ),
2284 (
2285 "an existing margin deficit wins a stop tie",
2286 boundary_stop,
2287 BacktestSettings {
2288 order_size: OrderSize::Fixed(20.0),
2289 leverage: 2.0,
2290 ..terms.clone()
2291 },
2292 flat(&[100.0, 100.0, 90.0]),
2293 vec![trade(
2294 Buy,
2295 20.0,
2296 1,
2297 100.0,
2298 Some((2, 90.0, -200.0)),
2299 "A / Margin call",
2300 )],
2301 vec![1000.0, 1000.0, 1000.0, 800.0],
2302 0,
2303 ),
2304 (
2305 "intrabar long margin call",
2306 hold,
2307 BacktestSettings {
2308 order_size: OrderSize::Fixed(15.0),
2309 leverage: 2.0,
2310 ..terms.clone()
2311 },
2312 vec![
2313 (100.0, 100.0, 100.0, 100.0),
2314 (100.0, 100.0, 100.0, 100.0),
2315 (100.0, 105.0, 50.0, 100.0),
2316 ],
2317 vec![trade(
2318 Buy,
2319 15.0,
2320 1,
2321 100.0,
2322 Some((2, 200.0 / 3.0, -500.0)),
2323 "A / Margin call",
2324 )],
2325 vec![1000.0, 1000.0, 1000.0, 500.0],
2326 0,
2327 ),
2328 (
2329 "intrabar short margin call",
2330 hold_short,
2331 BacktestSettings {
2332 order_size: OrderSize::Fixed(10.0),
2333 leverage: 2.0,
2334 ..terms.clone()
2335 },
2336 vec![
2337 (100.0, 100.0, 100.0, 100.0),
2338 (100.0, 100.0, 100.0, 100.0),
2339 (100.0, 150.0, 95.0, 100.0),
2340 ],
2341 vec![trade(
2342 Sell,
2343 10.0,
2344 1,
2345 100.0,
2346 Some((2, 400.0 / 3.0, -1000.0 / 3.0)),
2347 "A / Margin call",
2348 )],
2349 vec![1000.0, 1000.0, 1000.0, 2000.0 / 3.0],
2350 0,
2351 ),
2352 (
2353 "gap margin call",
2354 hold,
2355 BacktestSettings {
2356 order_size: OrderSize::Fixed(15.0),
2357 leverage: 2.0,
2358 ..terms.clone()
2359 },
2360 flat(&[100.0, 100.0, 50.0]),
2361 vec![trade(
2362 Buy,
2363 15.0,
2364 1,
2365 100.0,
2366 Some((2, 50.0, -750.0)),
2367 "A / Margin call",
2368 )],
2369 vec![1000.0, 1000.0, 1000.0, 250.0],
2370 0,
2371 ),
2372 (
2374 "margin crossings do not multiply tiny prices",
2375 hold,
2376 BacktestSettings {
2377 order_size: OrderSize::Fixed(5e200),
2378 leverage: 2.0,
2379 ..terms.clone()
2380 },
2381 vec![
2382 (1e-198, 1e-198, 1e-198, 1e-198),
2383 (1e-198, 1e-198, 1e-198, 1e-198),
2384 (1e-198, 1.05e-198, -1e-198, 1e-198),
2385 ],
2386 vec![trade(
2387 Buy,
2388 5e200,
2389 1,
2390 1e-198,
2391 Some((2, -200.0 / 3.0 * 1e-200, -2500.0 / 3.0)),
2392 "A / Margin call",
2393 )],
2394 vec![1000.0, 1000.0, 1000.0, 500.0 / 3.0],
2395 0,
2396 ),
2397 (
2398 "negative price margin call",
2399 hold,
2400 BacktestSettings {
2401 order_size: OrderSize::Fixed(5.0),
2402 leverage: 2.0,
2403 ..terms.clone()
2404 },
2405 vec![
2406 (100.0, 100.0, 100.0, 100.0),
2407 (100.0, 100.0, 100.0, 100.0),
2408 (100.0, 105.0, -100.0, 100.0),
2409 ],
2410 vec![trade(
2411 Buy,
2412 5.0,
2413 1,
2414 100.0,
2415 Some((2, -200.0 / 3.0, -2500.0 / 3.0)),
2416 "A / Margin call",
2417 )],
2418 vec![1000.0, 1000.0, 1000.0, 500.0 / 3.0],
2419 0,
2420 ),
2421 (
2422 "short slippage",
2423 sized_short,
2424 BacktestSettings {
2425 slippage: Slippage::Ticks {
2426 ticks: 2,
2427 tick_size: 0.5,
2428 },
2429 ..terms.clone()
2430 },
2431 flat(&[100.0, 100.0, 90.0]),
2432 vec![trade(Sell, 2.0, 1, 99.0, Some((2, 91.0, 16.0)), "A / X")],
2433 vec![1000.0, 1000.0, 998.0, 1016.0],
2434 0,
2435 ),
2436 (
2437 "stop-limit never slips",
2438 stop_limits,
2439 BacktestSettings {
2440 slippage: Slippage::Fraction(0.1),
2441 ..terms.clone()
2442 },
2443 vec![(100.0, 100.0, 100.0, 100.0), (100.0, 110.0, 99.0, 108.0)],
2444 vec![trade(Buy, 2.0, 1, 105.0, Some((1, 110.0, 10.0)), "A / X")],
2445 vec![1000.0, 1000.0, 1010.0],
2446 0,
2447 ),
2448 (
2449 "tiny quantity retains order fees",
2450 sized,
2451 BacktestSettings {
2452 order_size: OrderSize::Fixed(1e-300),
2453 commission: Commission::PerOrder(1.0),
2454 ..terms.clone()
2455 },
2456 flat(&[100.0, 100.0, 110.0]),
2457 vec![fee_trade(
2458 Buy,
2459 1e-300,
2460 1,
2461 100.0,
2462 Some((2, 110.0, -2.0)),
2463 2.0,
2464 "A / X",
2465 )],
2466 vec![1000.0, 1000.0, 999.0, 998.0],
2467 0,
2468 ),
2469 (
2470 "recalculation waits for the next point",
2471 cycle,
2472 BacktestSettings {
2473 recalculate_after_fills: true,
2474 ..terms.clone()
2475 },
2476 vec![(100.0, 100.0, 100.0, 100.0), (100.0, 101.0, 98.0, 100.0)],
2477 vec![
2478 trade(Buy, 2.0, 1, 100.0, Some((1, 101.0, 2.0)), "A / X"),
2479 trade(Buy, 2.0, 1, 98.0, Some((1, 100.0, 4.0)), "A / X"),
2480 ],
2481 vec![1000.0, 1000.0, 1006.0],
2482 0,
2483 ),
2484 (
2485 "flat bars retain successive fill points",
2486 cycle,
2487 BacktestSettings {
2488 recalculate_after_fills: true,
2489 ..terms.clone()
2490 },
2491 flat(&[100.0, 100.0]),
2492 vec![
2493 trade(Buy, 2.0, 1, 100.0, Some((1, 100.0, 0.0)), "A / X"),
2494 trade(Buy, 2.0, 1, 100.0, Some((1, 100.0, 0.0)), "A / X"),
2495 ],
2496 vec![1000.0; 3],
2497 0,
2498 ),
2499 (
2500 "recalculate after fills",
2501 recalculate,
2502 BacktestSettings {
2503 recalculate_after_fills: true,
2504 ..terms.clone()
2505 },
2506 vec![(100.0, 100.0, 100.0, 100.0), (100.0, 110.0, 99.0, 108.0)],
2507 vec![trade(Buy, 2.0, 1, 100.0, Some((1, 105.0, 10.0)), "A / X")],
2508 vec![1000.0, 1000.0, 1010.0],
2509 0,
2510 ),
2511 ] {
2512 let source: Vec<_> = prices
2513 .into_iter()
2514 .enumerate()
2515 .map(|(i, (o, h, l, c))| Bar::new(hour(i as i64), o, h, l, c, 0.0))
2516 .collect();
2517 let mut engine =
2518 Backtest::new("Terms", "TEST", Timeframe::Hour1, settings.initial_capital);
2519 engine.settings = settings;
2520 let report = engine.run(&mut Script(script), &source, &[]);
2521 let mut asynchronous_strategy = Script(script);
2522 let mut future =
2523 std::pin::pin!(engine.run_async(&mut asynchronous_strategy, &source, &[]));
2524 let mut context = std::task::Context::from_waker(std::task::Waker::noop());
2525 let mut batches = 0;
2526 let asynchronous = loop {
2527 match std::future::Future::poll(future.as_mut(), &mut context) {
2528 std::task::Poll::Ready(report) => break report,
2529 std::task::Poll::Pending => batches += 1,
2530 }
2531 };
2532 assert!(batches > 0, "asynchronous fills must yield");
2533 assert_eq!(
2534 serde_json::to_value(&asynchronous).unwrap(),
2535 serde_json::to_value(&report).unwrap()
2536 );
2537
2538 assert_eq!(report.currency, Some("USD".into()), "{name}");
2539 assert_eq!(report.risk_free_rate, 0.02, "{name}");
2540 assert!(
2541 report
2542 .properties
2543 .contains(&("Rejected orders".into(), rejected.to_string())),
2544 "{name}"
2545 );
2546 assert_eq!(report.trades.len(), expected.len(), "{name}");
2547 for (mut actual, expected) in report.trades.iter().cloned().zip(expected) {
2548 assert!(
2549 (actual.quantity - expected.quantity).abs() < 1e-10,
2550 "{name}"
2551 );
2552 assert!((actual.fees - expected.fees).abs() < 1e-10, "{name}");
2553 actual.quantity = expected.quantity;
2554 actual.fees = expected.fees;
2555 if let (Some(a), Some(e)) = (&mut actual.exit, &expected.exit) {
2556 assert!((a.price - e.price).abs() < 1e-10, "{name}");
2557 assert!((a.profit - e.profit).abs() < 1e-10, "{name}");
2558 a.price = e.price;
2559 a.profit = e.profit;
2560 }
2561 assert_eq!(actual, expected, "{name}");
2562 }
2563 assert_eq!(report.equity.len(), equity.len(), "{name}");
2564 for ((time, actual), (expected_time, expected)) in report
2565 .equity
2566 .iter()
2567 .zip((-1..source.len() as i64).map(hour).zip(equity))
2568 {
2569 assert_eq!(*time, expected_time, "{name}");
2570 assert!(
2571 (actual - expected).abs() < 1e-10,
2572 "{name}: {actual} != {expected}"
2573 );
2574 }
2575 }
2576
2577 struct History {
2578 prepared: usize,
2579 averages: Vec<f64>,
2580 decisions: Vec<usize>,
2581 entry_bar: usize,
2582 }
2583 impl Strategy for History {
2584 fn inputs(&self) -> Vec<(String, String)> {
2585 vec![("Length".into(), "2".into())]
2586 }
2587 fn prepare(&mut self, bars: &[Bar]) {
2588 self.prepared = bars.len();
2589 self.averages = bars
2590 .windows(2)
2591 .map(|pair| (pair[0].close + pair[1].close) / 2.0)
2592 .collect();
2593 }
2594 fn on_bar(&mut self, context: &mut StrategyContext<'_>) {
2595 let index = context.bars().len() - 1;
2596 self.decisions.push(index);
2597 if index <= self.entry_bar {
2598 if index == 1 {
2599 assert_eq!(self.averages[0], 104.0, "warm-up bar feeds the study");
2600 }
2601 context.entry("A", Buy, 2.0, OrderType::Market);
2602 context.exit("X", None, Some(110.0), Some(95.0));
2603 }
2604 }
2605 }
2606 let source = vec![
2607 Bar::new(hour(0), 100.0, 100.0, 100.0, 100.0, 0.0),
2608 Bar::new(hour(1), 100.0, 111.0, 94.0, 108.0, 0.0),
2609 Bar::new(hour(2), 108.0, 108.0, 108.0, 108.0, 0.0),
2610 ];
2611 let first = Bar::new(hour(1), 100.0, 111.0, 99.0, 110.0, 0.0);
2612 let second = Bar::new(
2613 hour(1) + TimeDelta::minutes(30),
2614 110.0,
2615 110.0,
2616 94.0,
2617 108.0,
2618 0.0,
2619 );
2620 let mut later_target = trade(Buy, 2.0, 1, 100.0, Some((1, 110.0, 20.0)), "A / X");
2621 later_target.exit.as_mut().unwrap().time += TimeDelta::minutes(30);
2622 for (name, lower, start, entry_bar, expected, times, decisions) in [
2626 (
2627 "chart path",
2628 vec![],
2629 None,
2630 0,
2631 vec![trade(Buy, 2.0, 1, 100.0, Some((1, 95.0, -10.0)), "A / X")],
2632 vec![hour(-1), hour(0), hour(1), hour(2)],
2633 vec![0, 1, 2],
2634 ),
2635 (
2636 "finer path",
2637 vec![first.clone(), second.clone()],
2638 None,
2639 0,
2640 vec![trade(Buy, 2.0, 1, 100.0, Some((1, 110.0, 20.0)), "A / X")],
2641 vec![hour(-1), hour(0), hour(1), hour(2)],
2642 vec![0, 1, 2],
2643 ),
2644 (
2645 "only fills use finer timestamps",
2646 vec![
2647 Bar::new(hour(1), 100.0, 101.0, 99.0, 100.0, 0.0),
2648 Bar::new(
2649 hour(1) + TimeDelta::minutes(30),
2650 100.0,
2651 111.0,
2652 99.0,
2653 108.0,
2654 0.0,
2655 ),
2656 ],
2657 None,
2658 0,
2659 vec![later_target],
2660 vec![hour(-1), hour(0), hour(1), hour(2)],
2661 vec![0, 1, 2],
2662 ),
2663 (
2664 "finer bars are ordered",
2665 vec![second.clone(), first.clone()],
2666 None,
2667 0,
2668 vec![trade(Buy, 2.0, 1, 100.0, Some((1, 110.0, 20.0)), "A / X")],
2669 vec![hour(-1), hour(0), hour(1), hour(2)],
2670 vec![0, 1, 2],
2671 ),
2672 (
2673 "uncovered bar falls back",
2674 vec![Bar::new(hour(2), 108.0, 108.0, 108.0, 108.0, 0.0)],
2675 None,
2676 0,
2677 vec![trade(Buy, 2.0, 1, 100.0, Some((1, 95.0, -10.0)), "A / X")],
2678 vec![hour(-1), hour(0), hour(1), hour(2)],
2679 vec![0, 1, 2],
2680 ),
2681 (
2682 "invalid finer bar falls back",
2683 vec![Bar::new(hour(1), 100.0, f64::NAN, 99.0, 100.0, 0.0)],
2684 None,
2685 0,
2686 vec![trade(Buy, 2.0, 1, 100.0, Some((1, 95.0, -10.0)), "A / X")],
2687 vec![hour(-1), hour(0), hour(1), hour(2)],
2688 vec![0, 1, 2],
2689 ),
2690 (
2691 "start warms studies",
2692 vec![],
2693 Some(hour(1)),
2694 1,
2695 vec![trade(Buy, 2.0, 2, 108.0, None, "A")],
2696 vec![hour(1), hour(2), hour(3)],
2697 vec![0, 1, 2],
2698 ),
2699 (
2700 "start between chart bars",
2701 vec![first.clone(), second.clone()],
2702 Some(hour(0) + TimeDelta::minutes(30)),
2703 1,
2704 vec![trade(Buy, 2.0, 2, 108.0, None, "A")],
2705 vec![hour(0) + TimeDelta::minutes(30), hour(2), hour(3)],
2706 vec![0, 1, 2],
2707 ),
2708 (
2709 "start after history",
2710 vec![first, second],
2711 Some(hour(3)),
2712 3,
2713 vec![],
2714 vec![],
2715 vec![0, 1, 2],
2716 ),
2717 ] {
2718 let mut strategy = History {
2719 prepared: 0,
2720 averages: vec![],
2721 decisions: vec![],
2722 entry_bar,
2723 };
2724 let mut engine = Backtest::new("History", "TEST", Timeframe::Hour1, 1000.0);
2725 engine.settings.start_time = start;
2726 let report = engine.run(&mut strategy, &source, &lower);
2727 assert_eq!(strategy.prepared, source.len(), "{name}");
2728 assert_eq!(strategy.decisions, decisions, "{name}");
2729 strategy.decisions.clear();
2730 let mut future = std::pin::pin!(engine.run_async(&mut strategy, &source, &lower));
2731 let mut context = std::task::Context::from_waker(std::task::Waker::noop());
2732 assert!(std::future::Future::poll(future.as_mut(), &mut context).is_pending());
2733 let std::task::Poll::Ready(asynchronous) =
2734 std::future::Future::poll(future.as_mut(), &mut context)
2735 else {
2736 panic!("{name}: these chart and finer bars fit in one execution batch");
2737 };
2738 assert_eq!(asynchronous, report, "{name}");
2739 assert_eq!(report.trades, expected, "{name}");
2740 assert_eq!(
2741 report
2742 .equity
2743 .iter()
2744 .map(|(time, _)| *time)
2745 .collect::<Vec<_>>(),
2746 times,
2747 "{name}"
2748 );
2749 let expected_last = expected
2750 .first()
2751 .and_then(|trade| trade.exit.as_ref())
2752 .map_or(1000.0, |exit| 1000.0 + exit.profit);
2753 if let Some((_, last)) = report.equity.last() {
2754 assert_eq!(*last, expected_last, "{name}");
2755 }
2756 #[cfg(feature = "csv")]
2757 {
2758 let csv = report.summary_csv(&report.performance(), &report.comparison(&source));
2759 for setting in ["Initial capital", "Currency", "Risk-free rate"] {
2760 assert_eq!(
2761 csv.lines()
2762 .filter(|row| row.starts_with(&format!("properties,{setting},")))
2763 .count(),
2764 1,
2765 "{name}: {setting} must appear once"
2766 );
2767 }
2768 }
2769 assert!(
2770 report
2771 .properties
2772 .contains(&("Input: Length".into(), "2".into()))
2773 );
2774 for setting in [
2775 "Order size",
2776 "Pyramiding",
2777 "Commission",
2778 "Slippage",
2779 "Leverage",
2780 "Fill on close",
2781 "Recalculate after fills",
2782 "Start time",
2783 "Lower-interval bars",
2784 "Rejected orders",
2785 ] {
2786 assert!(
2787 report.properties.iter().any(|(key, _)| key == setting),
2788 "{name}: missing {setting}"
2789 );
2790 }
2791 if let Some(start) = start {
2792 assert!(report.trades.iter().all(|trade| trade.entry_time >= start));
2793 assert!(report.equity.iter().all(|(time, _)| *time >= start));
2794 assert_eq!(report.equity_curve(), report.equity, "{name}");
2795 }
2796 }
2797
2798 fn stop_then_close(bar: usize, context: &mut StrategyContext<'_>) {
2799 match bar {
2800 0 => context.entry("A", Buy, 2.0, OrderType::Stop(105.0)),
2801 1 => context.exit("X", None, None, None),
2802 _ => {}
2803 }
2804 }
2805 for (lower, entry_time, exit_time) in [
2808 (vec![], hour(1), hour(1)),
2809 (
2810 vec![
2811 Bar::new(hour(1), 100.0, 101.0, 99.0, 100.0, 0.0),
2812 Bar::new(
2813 hour(1) + TimeDelta::minutes(30),
2814 100.0,
2815 111.0,
2816 99.0,
2817 108.0,
2818 0.0,
2819 ),
2820 ],
2821 hour(1) + TimeDelta::minutes(30),
2822 hour(2),
2823 ),
2824 ] {
2825 let mut engine = Backtest::new("On close", "TEST", Timeframe::Hour1, 1000.0);
2826 engine.settings.fill_on_close = true;
2827 let report = engine.run(&mut Script(stop_then_close), &source, &lower);
2828 assert_eq!(report.performance().closed_trades, 1);
2829 assert_eq!(report.trades[0].entry_time, entry_time);
2830 let exit = report.trades[0].exit.as_ref().unwrap();
2831 assert_eq!(exit.time, exit_time);
2832 assert_eq!(exit.profit, 6.0);
2833 }
2834
2835 for (count, lower, times) in [
2838 (
2839 1,
2840 vec![],
2841 vec![hour(0), hour(0) + TimeDelta::nanoseconds(1)],
2842 ),
2843 (
2844 2,
2845 vec![Bar::new(
2846 hour(1) + TimeDelta::minutes(30),
2847 100.0,
2848 100.0,
2849 100.0,
2850 100.0,
2851 0.0,
2852 )],
2853 vec![
2854 hour(0),
2855 hour(1),
2856 hour(1) + TimeDelta::minutes(30) + TimeDelta::nanoseconds(1),
2857 ],
2858 ),
2859 ] {
2860 let source: Vec<_> = (0..count)
2861 .map(|i| Bar::new(hour(i), 100.0, 100.0, 100.0, 100.0, 0.0))
2862 .collect();
2863 let mut engine = Backtest::new(
2864 "Execution",
2865 "TEST",
2866 Timeframe::Ticks(std::num::NonZeroU64::new(1).unwrap()),
2867 1000.0,
2868 );
2869 engine.settings = BacktestSettings {
2870 start_time: Some(hour(0)),
2871 fill_on_close: true,
2872 commission: Commission::PerOrder(1.0),
2873 ..terms.clone()
2874 };
2875 let report = engine.run(&mut Script(hold), &source, &lower);
2876 assert_eq!(
2877 report
2878 .equity
2879 .iter()
2880 .map(|(time, _)| *time)
2881 .collect::<Vec<_>>(),
2882 times
2883 );
2884 assert_eq!(report.equity_curve(), report.equity);
2885 assert_eq!(report.performance().max_drawdown, Some(1.0));
2886 assert_eq!(report.equity.last().unwrap().1, 999.0);
2887 assert_eq!(report.trades[0].fees, 1.0);
2888 }
2889 }
2890}