use crate::Backtest;
use pine_core::{
AlertCondition, AlertConditionOutput, Indicator, Input, InputOutput, LogEntry, LogOutput,
MetadataOutput, PineOutput, Plot, PlotOutput,
};
use std::collections::BTreeMap;
pub struct Run<O: PineOutput> {
pub outputs: Vec<O>,
pub backtest: Option<Backtest>,
}
#[derive(Debug, Clone, Default)]
pub struct RunResult {
pub bars: usize,
pub plots: BTreeMap<String, Vec<Option<f64>>>,
pub logs: Vec<LogEntry>,
pub alerts: Vec<AlertCondition>,
pub indicator: Option<Indicator>,
pub inputs: Vec<Input>,
}
impl RunResult {
pub fn collect<O>(outputs: &[O]) -> Self
where
O: PlotOutput + LogOutput + AlertConditionOutput + MetadataOutput + InputOutput,
{
let mut result = Self::default();
for output in outputs {
result.push_bar(output.plots());
result.logs.extend(output.get_logs().iter().cloned());
}
if let Some(last) = outputs.last() {
result.alerts = last.alertconditions().to_vec();
result.inputs = last.inputs().to_vec();
result.indicator = last.indicator().cloned();
}
result
}
fn push_bar(&mut self, plots: &[Plot]) {
for plot in plots {
let column = self.plots.entry(plot.title.clone()).or_default();
column.resize(self.bars, None);
column.push((!plot.series.is_nan()).then_some(plot.series));
}
self.bars += 1;
for column in self.plots.values_mut() {
column.resize(self.bars, None);
}
}
pub fn plot(&self, title: &str) -> Option<&[Option<f64>]> {
self.plots.get(title).map(Vec::as_slice)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plot(title: &str, series: f64) -> Plot {
Plot {
series,
title: title.to_string(),
..Default::default()
}
}
#[test]
fn columns_line_up_with_bars() {
let mut run = RunResult::default();
run.push_bar(&[plot("a", 1.0)]);
run.push_bar(&[plot("a", 2.0)]);
assert_eq!(run.bars, 2);
assert_eq!(run.plot("a"), Some([Some(1.0), Some(2.0)].as_slice()));
}
#[test]
fn na_becomes_a_gap() {
let mut run = RunResult::default();
run.push_bar(&[plot("a", f64::NAN)]);
run.push_bar(&[plot("a", 2.0)]);
assert_eq!(run.plot("a"), Some([None, Some(2.0)].as_slice()));
}
#[test]
fn a_plot_appearing_late_is_padded_at_the_front() {
let mut run = RunResult::default();
run.push_bar(&[plot("a", 1.0)]);
run.push_bar(&[plot("a", 2.0), plot("b", 9.0)]);
assert_eq!(run.plot("a"), Some([Some(1.0), Some(2.0)].as_slice()));
assert_eq!(run.plot("b"), Some([None, Some(9.0)].as_slice()));
}
#[test]
fn a_plot_that_stops_is_padded_at_the_end() {
let mut run = RunResult::default();
run.push_bar(&[plot("a", 1.0)]);
run.push_bar(&[]);
assert_eq!(run.plot("a"), Some([Some(1.0), None].as_slice()));
assert_eq!(run.bars, 2);
}
#[test]
fn an_unplotted_title_is_absent() {
let run = RunResult::default();
assert!(run.plot("nope").is_none());
}
#[test]
fn with_broker_swaps_the_broker_factory() {
use crate::broker::{Broker, BrokerConfig, BrokerFactory, DefaultBrokerFactory};
use crate::core::DefaultPineOutput;
use crate::ScriptBuilder;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct CountingFactory(Arc<AtomicUsize>);
impl BrokerFactory for CountingFactory {
fn build(&self, config: &BrokerConfig) -> Box<dyn Broker> {
self.0.fetch_add(1, Ordering::SeqCst);
DefaultBrokerFactory.build(config)
}
}
let source = r#"
//@version=5
strategy("t", initial_capital = 10000)
if bar_index == 1
strategy.entry("Long", strategy.long)
"#;
let calls = Arc::new(AtomicUsize::new(0));
let run = ScriptBuilder::<DefaultPineOutput>::with_code(source)
.with_data(crate::data::synthetic(5))
.with_broker(Box::new(CountingFactory(Arc::clone(&calls))))
.compile()
.expect("compile")
.run()
.expect("run");
assert!(run.backtest.is_some());
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn backtest_reports_the_halt_bar() {
use crate::core::DefaultPineOutput;
use crate::ScriptBuilder;
let halting = r#"
//@version=5
strategy("t", initial_capital = 10000)
strategy.risk.max_drawdown(50, strategy.cash)
if bar_index == 1
strategy.entry("S", strategy.short, qty = 100)
"#;
let run = ScriptBuilder::<DefaultPineOutput>::with_code(halting)
.with_data(crate::data::synthetic(10))
.compile()
.expect("compile")
.run()
.expect("run");
assert!(run.backtest.expect("strategy").halted.is_some());
let quiet = r#"
//@version=5
strategy("t", initial_capital = 10000)
if bar_index == 1
strategy.entry("L", strategy.long, qty = 1)
"#;
let run = ScriptBuilder::<DefaultPineOutput>::with_code(quiet)
.with_data(crate::data::synthetic(10))
.compile()
.expect("compile")
.run()
.expect("run");
assert_eq!(run.backtest.expect("strategy").halted, None);
}
}