1use super::*;
4
5#[derive(Clone, Debug, PartialEq)]
8#[non_exhaustive]
9pub struct StrategyFill {
10 pub sequence: u64,
12 pub time: DateTime<Utc>,
14 pub order_id: String,
16 pub side: OrderSide,
18 pub price: f64,
20 pub quantity: f64,
22 pub position: f64,
24}
25
26#[derive(Clone, Debug, PartialEq)]
27pub(super) struct LiveState {
28 bars: Vec<Bar>,
29 lower: Vec<Bar>,
30 settings: BacktestSettings,
31 inputs: Vec<(String, String)>,
32 report: StrategyReport,
33 processed: usize,
34 forming: Option<(DateTime<Utc>, f64)>,
35 sequence: u64,
36 orders: Vec<Working>,
37 lots: Vec<Lot>,
38 trades: Vec<StrategyTrade>,
39 realized: f64,
40 rejected: usize,
41 tick: u64,
42}
43
44fn same_bar(a: &Bar, b: &Bar) -> bool {
46 a.time == b.time
47 && [a.open, a.high, a.low, a.close, a.volume].map(f64::to_bits)
48 == [b.open, b.high, b.low, b.close, b.volume].map(f64::to_bits)
49}
50
51fn same_bars(a: &[Bar], b: &[Bar]) -> bool {
52 a.len() == b.len() && a.iter().zip(b).all(|(a, b)| same_bar(a, b))
53}
54
55fn same_lower(a: &[Bar], b: &[Bar], before: Option<DateTime<Utc>>) -> bool {
56 let historical = |bar: &&Bar| before.is_none_or(|at| bar.time < at);
57 let mut a = a.iter().filter(historical);
58 let mut b = b.iter().filter(historical);
59 loop {
60 match (a.next(), b.next()) {
61 (Some(a), Some(b)) if same_bar(a, b) => {}
62 (None, None) => return true,
63 _ => return false,
64 }
65 }
66}
67
68impl Backtest {
69 pub fn report(&self) -> Option<&StrategyReport> {
73 self.live.as_ref().map(|live| &live.report)
74 }
75
76 pub fn update(
97 &mut self,
98 strategy: &mut dyn Strategy,
99 bars: &[Bar],
100 lower: &[Bar],
101 ) -> Vec<StrategyFill> {
102 assert!(
103 bars.windows(2).all(|pair| pair[0].time < pair[1].time),
104 "bar times must be strictly increasing"
105 );
106 let inputs = strategy.inputs();
107 let previous = self.live.take();
108 let replay_before = previous
109 .as_ref()
110 .and_then(|live| live.bars.last().map(|bar| bar.time));
111 let replayed_forming = previous.as_ref().is_some_and(|live| live.forming.is_some());
112 let replayed = |time| {
113 replay_before
114 .is_some_and(|before| time < before || (replayed_forming && time == before))
115 };
116 let sequence = previous.as_ref().map_or(0, |live| live.sequence);
117 let rebuild = previous.as_ref().is_none_or(|live| {
118 let prefix = live.bars.len().saturating_sub(1);
119 bars.len() < live.bars.len()
120 || !same_bars(&live.bars[..prefix], &bars[..prefix.min(bars.len())])
121 || live
122 .bars
123 .last()
124 .zip(bars.get(prefix))
125 .is_some_and(|(a, b)| a.time != b.time)
126 || live.settings != self.settings
127 || live.inputs != inputs
128 || live.report.name != self.name
129 || live.report.symbol != self.symbol
130 || live.report.timeframe != self.timeframe
131 || !same_lower(&live.lower, lower, replay_before)
132 });
133 let mut live = if rebuild {
134 Box::new(LiveState {
135 bars: Vec::new(),
136 lower: Vec::new(),
137 settings: self.settings.clone(),
138 inputs,
139 report: self.new_report(strategy, lower.len()),
140 processed: 0,
141 forming: None,
142 sequence,
143 orders: Vec::new(),
144 lots: Vec::new(),
145 trades: Vec::new(),
146 realized: 0.0,
147 rejected: 0,
148 tick: 0,
149 })
150 } else {
151 previous.expect("an existing live run")
152 };
153 if let Some((_, count)) = live
154 .report
155 .properties
156 .iter_mut()
157 .find(|(name, _)| name == "Lower-interval bars")
158 {
159 *count = lower.len().to_string();
160 }
161 let closed = bars.len().saturating_sub(1);
162 let tick_changed =
163 self.settings.recalculate_on_every_update && !same_bars(&live.bars, bars);
164 let changed = rebuild || live.processed < closed || tick_changed;
165 if !changed {
166 live.bars = bars.to_vec();
167 live.lower = lower.to_vec();
168 self.live = Some(live);
169 return Vec::new();
170 }
171 let calculation_bars = if self.settings.recalculate_on_every_update {
172 bars
173 } else {
174 &bars[..closed]
175 };
176 if rebuild {
177 strategy.prepare(calculation_bars);
178 } else {
179 strategy.prepare_update(calculation_bars);
180 }
181 let mut fills = Vec::new();
182 let mut run = Run {
183 orders: std::mem::take(&mut live.orders),
184 lots: std::mem::take(&mut live.lots),
185 trades: std::mem::take(&mut live.trades),
186 realized: live.realized,
187 rejected: live.rejected,
188 settings: &self.settings,
189 strategy,
190 history: &[],
191 trading: false,
192 tick: live.tick,
193 fills: Some(&mut fills),
194 forming: false,
195 };
196 let mut finer: Vec<_> = lower.iter().filter(|bar| valid_bar(bar)).collect();
197 finer.sort_by_key(|bar| bar.time);
198 finer.dedup_by_key(|bar| bar.time);
199 if self.timeframe.is_trade_based()
201 && self.settings.start_time.is_some()
202 && live.processed > 0
203 && live.processed < closed
204 && let Some(mark) = live.report.equity.last_mut()
205 {
206 mark.0 = bars[live.processed].time;
207 }
208 if live.report.equity.is_empty() {
209 run.start_equity(self.timeframe, &bars[..closed], &mut live.report);
210 }
211 for index in live.processed..closed {
212 let bar = &bars[index];
213 let observed = live.forming.is_some_and(|(time, _)| time == bar.time);
214 if observed {
215 if live.bars.get(index).is_none_or(|old| !same_bar(old, bar)) {
216 run.forming_bar(bars, index, live.forming.map(|(_, price)| price), false);
217 }
218 live.forming = None;
219 }
220 run.closed_bar(&bars[..closed], index, &finer, &mut live.report, !observed);
221 if rebuild
222 && replayed(bar.time)
223 && let Some(fills) = &mut run.fills
224 {
225 fills.clear();
226 }
227 }
228 live.processed = closed;
229 if self.settings.recalculate_on_every_update
230 && let Some(bar) = bars.last()
231 {
232 let previous_price = live
233 .forming
234 .filter(|(time, _)| *time == bar.time)
235 .map(|(_, price)| price);
236 if run.forming_bar(bars, closed, previous_price, true) {
237 live.forming = Some((bar.time, bar.close));
238 }
239 if rebuild
240 && replayed(bar.time)
241 && let Some(fills) = &mut run.fills
242 {
243 fills.clear();
244 }
245 }
246 run.report_trades(&mut live.report);
247 live.orders = run.orders;
248 live.lots = run.lots;
249 live.trades = run.trades;
250 live.realized = run.realized;
251 live.rejected = run.rejected;
252 live.tick = run.tick;
253 for fill in &mut fills {
254 live.sequence += 1;
255 fill.sequence = live.sequence;
256 }
257 live.bars = bars.to_vec();
258 live.lower = lower.to_vec();
259 self.live = Some(live);
260 fills
261 }
262}
263
264impl<'a> Run<'a> {
265 fn forming_bar(
266 &mut self,
267 bars: &'a [Bar],
268 index: usize,
269 previous: Option<f64>,
270 decide: bool,
271 ) -> bool {
272 let bar = &bars[index];
273 self.trading = self
274 .settings
275 .start_time
276 .is_none_or(|start| bar.time >= start);
277 if !valid_bar(bar) {
278 return false;
279 }
280 self.history = &bars[..index];
281 self.forming = true;
282 if self.trading {
283 if previous.is_none() {
284 self.tick += 1;
285 }
286 self.fill_along(previous.unwrap_or(bar.open), bar.close, bar.time);
287 }
288 if decide {
289 self.history = &bars[..=index];
290 self.decide(bar.close, false);
291 }
292 true
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::strategies::StrategyFactory;
300
301 fn bar(minute: i64, close: f64) -> Bar {
302 Bar::new(
303 DateTime::UNIX_EPOCH + TimeDelta::minutes(minute),
304 close,
305 close + 2.0,
306 close - 2.0,
307 close,
308 1.0,
309 )
310 }
311
312 #[derive(Default)]
313 struct Script {
314 decisions: Vec<usize>,
315 resets: usize,
316 }
317
318 impl Strategy for Script {
319 fn prepare(&mut self, _: &[Bar]) {
320 self.decisions.clear();
321 self.resets += 1;
322 }
323 fn prepare_update(&mut self, _: &[Bar]) {}
324 fn on_bar(&mut self, context: &mut StrategyContext<'_>) {
325 if context.is_after_fill() {
326 return;
327 }
328 let index = context.bars().len();
329 self.decisions.push(index);
330 match index {
331 1 => context.entry("Long", OrderSide::Buy, 2.0, OrderType::Market),
332 3 => context.exit("Part", Some(1.0), None, None),
333 5 => context.entry("Short", OrderSide::Sell, 3.0, OrderType::Market),
334 7 => context.exit("Flat", None, None, None),
335 _ => {}
336 }
337 }
338 }
339
340 #[test]
341 fn closed_updates_match_full_runs_without_repeating_decisions() {
342 let bars: Vec<_> = [100., 101., 104., 102., 98., 95., 97., 99., 96.]
343 .into_iter()
344 .enumerate()
345 .map(|(i, price)| bar(i as i64, price))
346 .collect();
347 for (fill_on_close, start_time, magnifier, timeframe) in [
348 (false, None, false, Timeframe::Min1),
349 (true, None, false, Timeframe::Min1),
350 (false, Some(bars[2].time), false, Timeframe::Min1),
351 (true, Some(bars[2].time), true, Timeframe::Min1),
352 (
353 false,
354 Some(bars[0].time),
355 false,
356 Timeframe::Ticks(std::num::NonZeroU64::new(1).unwrap()),
357 ),
358 (
359 true,
360 Some(bars[0].time),
361 false,
362 Timeframe::Ticks(std::num::NonZeroU64::new(1).unwrap()),
363 ),
364 (
365 true,
366 None,
367 true,
368 Timeframe::Ticks(std::num::NonZeroU64::new(1).unwrap()),
369 ),
370 (
371 false,
372 Some(bars[2].time + TimeDelta::seconds(30)),
373 false,
374 Timeframe::Ticks(std::num::NonZeroU64::new(1).unwrap()),
375 ),
376 ] {
377 let mut engine = Backtest::new("Live", "SYM", timeframe, 10_000.0);
378 engine.settings.fill_on_close = fill_on_close;
379 engine.settings.start_time = start_time;
380 engine.settings.commission = Commission::PerOrder(1.0);
381 engine.settings.slippage = Slippage::Ticks {
382 ticks: 1,
383 tick_size: 0.25,
384 };
385 engine.settings.recalculate_after_fills = true;
386 let mut script = Script::default();
387 let mut sequence = 0;
388 for count in 0..=bars.len() {
389 let lower = if magnifier {
390 bars[..count.saturating_sub(1)].to_vec()
391 } else {
392 vec![]
393 };
394 let fills = engine.update(&mut script, &bars[..count], &lower);
395 for fill in fills {
396 sequence += 1;
397 assert_eq!(fill.sequence, sequence);
398 }
399 let full = engine.run(
400 &mut Script::default(),
401 &bars[..count.saturating_sub(1)],
402 &lower,
403 );
404 let asynchronous = {
405 let mut strategy = Script::default();
406 let mut future = std::pin::pin!(engine.run_async(
407 &mut strategy,
408 &bars[..count.saturating_sub(1)],
409 &lower,
410 ));
411 let mut context = std::task::Context::from_waker(std::task::Waker::noop());
412 loop {
413 if let std::task::Poll::Ready(report) =
414 std::future::Future::poll(future.as_mut(), &mut context)
415 {
416 break report;
417 }
418 }
419 };
420 assert_eq!(asynchronous, full, "async prefix {count}");
421 assert_eq!(
422 engine.report(),
423 Some(&full),
424 "prefix {count}, close {fill_on_close}, start {start_time:?}"
425 );
426 assert_eq!(script.decisions, (1..count).collect::<Vec<_>>());
427 assert!(
428 engine
429 .update(&mut script, &bars[..count], &lower)
430 .is_empty()
431 );
432 assert_eq!(script.decisions, (1..count).collect::<Vec<_>>());
433 }
434 assert_eq!(script.resets, 1);
435 let mut corrected = bars.clone();
437 corrected[1] = bar(1, 120.0);
438 assert!(engine.update(&mut script, &corrected, &[]).is_empty());
439 assert_eq!(script.resets, 2);
440 assert_eq!(
441 engine.report(),
442 Some(&engine.run(&mut Script::default(), &corrected[..8], &[]))
443 );
444 }
445 let factory = StrategyFactory::new();
447 let source: Vec<_> = (0..80)
448 .map(|i| bar(i, 100.0 + f64::from((i % 13) as i32) * 2.0))
449 .collect();
450 for name in factory.list() {
451 let mut strategy = factory.create(name).unwrap();
452 let mut engine = Backtest::new(name, "SYM", Timeframe::Min1, 10_000.0);
453 for count in 1..=source.len() {
454 engine.update(strategy.as_mut(), &source[..count], &[]);
455 }
456 let full = engine.run(
457 factory.create(name).unwrap().as_mut(),
458 &source[..source.len() - 1],
459 &[],
460 );
461 assert_eq!(engine.report(), Some(&full), "{name}");
462 }
463 }
464
465 #[test]
466 fn forming_ticks_keep_fills_and_do_not_replay_extremes_at_close() {
467 struct TickStrategy;
468 impl Strategy for TickStrategy {
469 fn on_bar(&mut self, context: &mut StrategyContext<'_>) {
470 if context.is_after_fill() {
471 return;
472 }
473 if context.price() >= 105.0 && context.position() == 0.0 {
474 context.entry("Long", OrderSide::Buy, 2.0, OrderType::Market);
475 context.exit("Target", None, Some(110.0), None);
476 }
477 }
478 }
479 let mut engine = Backtest::new("Ticks", "SYM", Timeframe::Min1, 10_000.0);
480 engine.settings.recalculate_on_every_update = true;
481 let mut bars = vec![bar(0, 100.0)];
482 let mut strategy = TickStrategy;
483 assert!(engine.update(&mut strategy, &bars, &[]).is_empty());
484 bars[0] = Bar::new(bars[0].time, 100., 120., 90., 106., 2.);
485 assert!(engine.update(&mut strategy, &bars, &[]).is_empty());
486 assert!(engine.update(&mut strategy, &bars, &[]).is_empty());
487 bars[0].close = 108.;
488 let fills = engine.update(&mut strategy, &bars, &[]);
489 assert_eq!(fills.len(), 1);
490 assert_eq!(
492 (fills[0].price, fills[0].quantity, fills[0].position),
493 (108., 2., 2.)
494 );
495 assert_eq!(
496 (&*fills[0].order_id, fills[0].side, fills[0].time),
497 ("Long", OrderSide::Buy, bars[0].time)
498 );
499 bars.push(bar(1, 108.));
500 assert!(engine.update(&mut strategy, &bars, &[]).is_empty());
501 let report = engine.report().unwrap();
503 assert_eq!(report.equity.last().unwrap().1, 10_000.);
504 assert!(report.trades[0].exit.is_none());
505 bars[1].high = 112.;
506 bars[1].close = 112.;
507 let fills = engine.update(&mut strategy, &bars, &[]);
508 assert_eq!(fills.len(), 1);
509 assert_eq!(
510 (
511 fills[0].sequence,
512 fills[0].price,
513 fills[0].quantity,
514 fills[0].position
515 ),
516 (2, 110., 2., 0.)
517 );
518 assert_eq!(fills[0].side, OrderSide::Sell);
519 assert_eq!(
520 engine.report().unwrap().trades[0]
521 .exit
522 .as_ref()
523 .unwrap()
524 .profit,
525 4.
526 );
527 bars[0].high = 121.;
529 assert!(engine.update(&mut strategy, &bars, &[]).is_empty());
530 }
531
532 #[test]
533 #[cfg(feature = "alert-engine")]
534 fn strategy_alerts_deliver_each_fill_once_even_at_the_same_timestamp() {
535 use crate::model::{Alert, AlertCondition};
536 use crate::services::{AlertEngine, AlertEvent};
537 use std::sync::{Arc, Mutex};
538 struct Reverse;
539 impl Strategy for Reverse {
540 fn on_bar(&mut self, context: &mut StrategyContext<'_>) {
541 match context.bars().len() {
542 1 => {
543 context.entry("A", OrderSide::Buy, 2., OrderType::Market);
544 context.entry("B", OrderSide::Buy, 2., OrderType::Market);
545 }
546 2 => context.entry("Reverse", OrderSide::Sell, 3., OrderType::Market),
547 3 => context.exit("Flat", None, None, None),
548 _ => {}
549 }
550 }
551 }
552 let delivered = Arc::new(Mutex::new(Vec::new()));
553 let sink = delivered.clone();
554 let mut alert = Alert::new("SYM", AlertCondition::StrategyFills("run-1".into()));
555 alert.timeframe = Some(Timeframe::Min1);
556 let alerts = AlertEngine::new()
557 .with_alerts(vec![alert])
558 .on_trigger(move |_, event| {
559 sink.lock().unwrap().push(event.clone());
560 });
561 let mut engine = Backtest::new("Run", "SYM", Timeframe::Min1, 10_000.);
562 engine.settings.pyramiding = 2;
563 let bars: Vec<_> = [187.5, 187.5, 180., 175., 175.]
564 .into_iter()
565 .enumerate()
566 .map(|(i, p)| bar(i as i64, p))
567 .collect();
568 let mut all = vec![];
569 for count in 1..=bars.len() {
570 let fills = engine.update(&mut Reverse, &bars[..count], &[]);
571 alerts.strategy_fills(
572 "other-run",
573 "SYM",
574 Timeframe::Min1,
575 &fills,
576 bars[count - 1].time,
577 );
578 alerts.strategy_fills(
579 "run-1",
580 "OTHER",
581 Timeframe::Min1,
582 &fills,
583 bars[count - 1].time,
584 );
585 alerts.strategy_fills(
586 "run-1",
587 "SYM",
588 Timeframe::Hour1,
589 &fills,
590 bars[count - 1].time,
591 );
592 assert_eq!(
593 delivered.lock().unwrap().len(),
594 all.len(),
595 "other subjects do not notify"
596 );
597 alerts.strategy_fills(
598 "run-1",
599 "SYM",
600 Timeframe::Min1,
601 &fills,
602 bars[count - 1].time,
603 );
604 alerts.strategy_fills(
605 "run-1",
606 "SYM",
607 Timeframe::Min1,
608 &fills,
609 bars[count - 1].time,
610 );
611 all.extend(fills);
612 }
613 assert_eq!(
614 all.iter()
615 .map(|fill| (fill.side, fill.price, fill.quantity, fill.position))
616 .collect::<Vec<_>>(),
617 [
618 (OrderSide::Buy, 187.5, 2., 2.),
619 (OrderSide::Buy, 187.5, 2., 4.),
620 (OrderSide::Sell, 180., 7., -3.),
621 (OrderSide::Buy, 175., 3., 0.),
622 ]
623 );
624 let events = delivered.lock().unwrap();
625 assert_eq!(events.len(), 4);
626 for (event, fill) in events.iter().zip(&all) {
627 let AlertEvent::TriggeredForSymbol {
628 time,
629 price,
630 message,
631 ..
632 } = event
633 else {
634 panic!("fill trigger");
635 };
636 assert_eq!((*time, *price), (fill.time, Some(fill.price)));
637 assert!(!message.is_empty());
638 }
639 let AlertEvent::TriggeredForSymbol { message, .. } = &events[1] else {
640 panic!("fill trigger");
641 };
642 assert_eq!(message, "Buy 2 at 187.50, position 4");
643 drop(events);
644 use crate::data::inbox::Inbox;
646 use crate::services::Alerts;
647 let inbox = Arc::new(Inbox::default());
648 for status in [
649 crate::model::AlertStatus::Disabled,
650 crate::model::AlertStatus::Active,
651 ] {
652 let mut edited = alerts.alerts()[0].clone();
653 edited.name = "Renamed".into();
654 edited.status = status;
655 alerts.save(edited, inbox.reply(0));
656 alerts.strategy_fills("run-1", "SYM", Timeframe::Min1, &all, bars[4].time);
657 assert_eq!(delivered.lock().unwrap().len(), 4);
658 }
659 let mut next = all[0].clone();
660 next.sequence = 5;
661 alerts.strategy_fills("run-1", "SYM", Timeframe::Min1, &[next], bars[4].time);
662 assert_eq!(delivered.lock().unwrap().len(), 5);
663 use crate::model::{
665 AlertFrequency, AlertStatus, NotificationSchedule, NotificationWindow, TimezoneId,
666 };
667 type Edit = fn(&mut Alert);
668 for (edit, count) in [
669 (
670 (|alert: &mut Alert| alert.frequency = AlertFrequency::Once) as Edit,
671 1,
672 ),
673 (|alert| alert.status = AlertStatus::Disabled, 0),
674 (|alert| alert.expires = Some(DateTime::UNIX_EPOCH), 0),
675 (
676 |alert| {
677 alert.schedule = Some(Box::new(NotificationSchedule {
678 timezone: TimezoneId::utc(),
679 days: vec![chrono::Weekday::Thu],
680 windows: vec![NotificationWindow {
681 start: chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
682 end: chrono::NaiveTime::from_hms_opt(13, 0, 0).unwrap(),
683 }],
684 }))
685 },
686 0,
687 ),
688 ] {
689 let mut alert = Alert::new("SYM", AlertCondition::StrategyFills("run-1".into()));
690 edit(&mut alert);
691 let delivered = Arc::new(Mutex::new(Vec::new()));
692 let sink = delivered.clone();
693 let engine = AlertEngine::new()
694 .with_alerts(vec![alert])
695 .on_trigger(move |_, event| {
696 sink.lock().unwrap().push(event.clone());
697 });
698 assert!(
699 engine
700 .subjects(engine.alerts()[0].id.as_ref().unwrap())
701 .is_empty()
702 );
703 engine.strategy_fills("run-1", "SYM", Timeframe::Min1, &all, bars[4].time);
704 assert_eq!(delivered.lock().unwrap().len(), count);
705 engine.strategy_fills(
707 "run-1",
708 "SYM",
709 Timeframe::Min1,
710 &all,
711 DateTime::UNIX_EPOCH + TimeDelta::hours(12),
712 );
713 assert_eq!(delivered.lock().unwrap().len(), count);
714 }
715 for (message, expected) in [
716 ("", "Buy 2 at 0.00025, position 2"),
717 ("{{symbol}} {{value}}", "SYM 0.00025"),
718 ] {
719 let mut alert = Alert::new("SYM", AlertCondition::StrategyFills("run-1".into()));
720 alert.message = message.into();
721 let delivered = Arc::new(Mutex::new(Vec::new()));
722 let sink = delivered.clone();
723 let engine = AlertEngine::new()
724 .with_alerts(vec![alert])
725 .on_trigger(move |_, event| {
726 sink.lock().unwrap().push(event.clone());
727 });
728 let mut fill = all[0].clone();
729 fill.price = 0.00025;
730 engine.strategy_fills("run-1", "SYM", Timeframe::Min1, &[fill], bars[4].time);
731 let events = delivered.lock().unwrap();
732 let [AlertEvent::TriggeredForSymbol { message, .. }] = events.as_slice() else {
733 panic!("one fill");
734 };
735 assert_eq!(message, expected);
736 }
737 }
738}