1use crate::Backtest;
4use pine_core::{
5 AlertCondition, AlertConditionOutput, Indicator, Input, InputOutput, LogEntry, LogOutput,
6 MetadataOutput, PineOutput, Plot, PlotOutput,
7};
8use std::collections::BTreeMap;
9
10pub struct Run<O: PineOutput> {
13 pub outputs: Vec<O>,
15 pub backtest: Option<Backtest>,
17}
18
19#[derive(Debug, Clone, Default)]
24pub struct RunResult {
25 pub bars: usize,
26 pub plots: BTreeMap<String, Vec<Option<f64>>>,
28 pub logs: Vec<LogEntry>,
29 pub alerts: Vec<AlertCondition>,
30 pub indicator: Option<Indicator>,
31 pub inputs: Vec<Input>,
32}
33
34impl RunResult {
35 pub fn collect<O>(outputs: &[O]) -> Self
37 where
38 O: PlotOutput + LogOutput + AlertConditionOutput + MetadataOutput + InputOutput,
39 {
40 let mut result = Self::default();
41
42 for output in outputs {
43 result.push_bar(output.plots());
44 result.logs.extend(output.get_logs().iter().cloned());
45 }
46
47 if let Some(last) = outputs.last() {
49 result.alerts = last.alertconditions().to_vec();
50 result.inputs = last.inputs().to_vec();
51 result.indicator = last.indicator().cloned();
52 }
53
54 result
55 }
56
57 fn push_bar(&mut self, plots: &[Plot]) {
60 for plot in plots {
61 let column = self.plots.entry(plot.title.clone()).or_default();
62 column.resize(self.bars, None);
63 column.push((!plot.series.is_nan()).then_some(plot.series));
64 }
65
66 self.bars += 1;
67
68 for column in self.plots.values_mut() {
69 column.resize(self.bars, None);
70 }
71 }
72
73 pub fn plot(&self, title: &str) -> Option<&[Option<f64>]> {
75 self.plots.get(title).map(Vec::as_slice)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn plot(title: &str, series: f64) -> Plot {
84 Plot {
85 series,
86 title: title.to_string(),
87 ..Default::default()
88 }
89 }
90
91 #[test]
92 fn columns_line_up_with_bars() {
93 let mut run = RunResult::default();
94 run.push_bar(&[plot("a", 1.0)]);
95 run.push_bar(&[plot("a", 2.0)]);
96
97 assert_eq!(run.bars, 2);
98 assert_eq!(run.plot("a"), Some([Some(1.0), Some(2.0)].as_slice()));
99 }
100
101 #[test]
102 fn na_becomes_a_gap() {
103 let mut run = RunResult::default();
104 run.push_bar(&[plot("a", f64::NAN)]);
105 run.push_bar(&[plot("a", 2.0)]);
106
107 assert_eq!(run.plot("a"), Some([None, Some(2.0)].as_slice()));
108 }
109
110 #[test]
111 fn a_plot_appearing_late_is_padded_at_the_front() {
112 let mut run = RunResult::default();
113 run.push_bar(&[plot("a", 1.0)]);
114 run.push_bar(&[plot("a", 2.0), plot("b", 9.0)]);
115
116 assert_eq!(run.plot("a"), Some([Some(1.0), Some(2.0)].as_slice()));
117 assert_eq!(run.plot("b"), Some([None, Some(9.0)].as_slice()));
118 }
119
120 #[test]
121 fn a_plot_that_stops_is_padded_at_the_end() {
122 let mut run = RunResult::default();
123 run.push_bar(&[plot("a", 1.0)]);
124 run.push_bar(&[]);
125
126 assert_eq!(run.plot("a"), Some([Some(1.0), None].as_slice()));
127 assert_eq!(run.bars, 2);
128 }
129
130 #[test]
131 fn an_unplotted_title_is_absent() {
132 let run = RunResult::default();
133 assert!(run.plot("nope").is_none());
134 }
135
136 #[test]
137 fn with_broker_swaps_the_broker_factory() {
138 use crate::broker::{Broker, BrokerConfig, BrokerFactory, DefaultBrokerFactory};
139 use crate::core::DefaultPineOutput;
140 use crate::ScriptBuilder;
141 use std::sync::atomic::{AtomicUsize, Ordering};
142 use std::sync::Arc;
143
144 struct CountingFactory(Arc<AtomicUsize>);
147 impl BrokerFactory for CountingFactory {
148 fn build(&self, config: &BrokerConfig) -> Box<dyn Broker> {
149 self.0.fetch_add(1, Ordering::SeqCst);
150 DefaultBrokerFactory.build(config)
151 }
152 }
153
154 let source = r#"
155//@version=5
156strategy("t", initial_capital = 10000)
157if bar_index == 1
158 strategy.entry("Long", strategy.long)
159"#;
160 let calls = Arc::new(AtomicUsize::new(0));
161 let run = ScriptBuilder::<DefaultPineOutput>::with_code(source)
162 .with_data(crate::data::synthetic(5))
163 .with_broker(Box::new(CountingFactory(Arc::clone(&calls))))
164 .compile()
165 .expect("compile")
166 .run()
167 .expect("run");
168
169 assert!(run.backtest.is_some());
171 assert_eq!(calls.load(Ordering::SeqCst), 1);
172 }
173}