pub use pine_ast as ast;
pub use pine_broker as broker;
pub use pine_builtins as builtins;
use pine_builtins::DefaultPineOutput;
pub use pine_core as core;
pub use pine_data as data;
pub use pine_diagnostics as diagnostics;
pub use pine_format as format;
pub use pine_interpreter as interpreter;
pub use pine_lexer as lexer;
pub use pine_lint as lint;
pub use pine_parser as parser;
pub use pine_sema as sema;
mod backtest;
mod run;
pub use backtest::Backtest;
pub use pine_core::{DataProvider, DirLoader, FileResolver, LibraryLoader};
pub use run::{Run, RunResult};
use pine_ast::Program;
use pine_core::{
AlertConditionOutput, BoxOutput, FillOutput, GlobalOutput, InputOutput, LabelOutput,
LineOutput, LogOutput, MetadataOutput, PineOutput, PlotOutput, TableOutput,
};
use pine_core::{Bar, Data, PineVersion, Timeframe, VersionError};
use pine_diagnostics::Diagnostic;
use pine_interpreter::{Interpreter, RuntimeError, Value};
use pine_lexer::{Lexer, LexerError};
use pine_parser::{Parser, ParserError};
use std::collections::HashMap;
use std::rc::Rc;
#[derive(Debug)]
pub enum Error {
Lexer(LexerError),
Parser(ParserError),
Runtime(RuntimeError),
Sema(Vec<Diagnostic>),
Version(VersionError),
Data(pine_core::ProviderError),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Lexer(e) => write!(f, "Lexer error: {}", e),
Error::Parser(e) => write!(f, "Parser error: {}", e),
Error::Runtime(e) => write!(f, "Runtime error: {}", e),
Error::Version(e) => write!(f, "Version error: {}", e),
Error::Data(e) => write!(f, "Data error: {}", e),
Error::Sema(diags) => {
for (i, d) in diags.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(f, "{}", d)?;
}
Ok(())
}
}
}
}
impl std::error::Error for Error {}
impl From<RuntimeError> for Error {
fn from(e: RuntimeError) -> Self {
Error::Runtime(e)
}
}
impl From<LexerError> for Error {
fn from(e: LexerError) -> Self {
Error::Lexer(e)
}
}
impl From<ParserError> for Error {
fn from(e: ParserError) -> Self {
Error::Parser(e)
}
}
impl From<VersionError> for Error {
fn from(e: VersionError) -> Self {
Error::Version(e)
}
}
pub fn check(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Vec<Diagnostic>, Error> {
let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
let tokens = Lexer::with_version(source, version).tokenize()?;
let program = Program::new(Parser::new(tokens).parse()?);
let mut env: HashMap<String, Value<DefaultPineOutput>> =
pine_builtins::register_namespace_objects(version, None, None);
for (name, value) in pine_builtins::per_bar_variables(&Bar::default()) {
env.insert(name, value);
}
Ok(pine_sema::analyze(&program, &env, loader))
}
pub struct ScriptBuilder<O: PineOutput> {
source: String,
custom_variables: HashMap<String, Value<O>>,
library_loader: Option<Box<dyn LibraryLoader>>,
request_provider: Option<Box<dyn DataProvider>>,
ticker: Option<String>,
timeframe: Timeframe,
data: Option<Data>,
bar_count: Option<usize>,
broker_factory: Option<Box<dyn pine_broker::BrokerFactory>>,
}
impl<O: PineOutput> ScriptBuilder<O> {
pub fn with_code(source: &str) -> ScriptBuilder<O> {
Self {
source: source.to_string(),
custom_variables: HashMap::new(),
library_loader: None,
request_provider: None,
ticker: None,
timeframe: Timeframe::default(),
data: None,
bar_count: None,
broker_factory: None,
}
}
pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
self.custom_variables = variables;
self
}
pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
self.library_loader = Some(loader);
self
}
pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
self.request_provider = Some(provider);
self
}
pub fn with_broker(mut self, factory: Box<dyn pine_broker::BrokerFactory>) -> Self {
self.broker_factory = Some(factory);
self
}
pub fn with_ticker(mut self, ticker: String) -> Self {
self.ticker = Some(ticker);
self
}
pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
self.timeframe = timeframe;
self
}
pub fn with_bar_count(mut self, bar_count: usize) -> Self {
self.bar_count = Some(bar_count);
self
}
pub fn with_data(mut self, data: Data) -> Self {
self.data = Some(data);
self
}
pub fn compile(self) -> Result<Script<O>, Error>
where
O: LogOutput
+ PlotOutput
+ LabelOutput
+ BoxOutput
+ InputOutput
+ LineOutput
+ TableOutput
+ MetadataOutput
+ GlobalOutput
+ AlertConditionOutput
+ FillOutput,
{
let data = match self.data {
Some(data) => data,
None => {
let provider = self
.request_provider
.as_ref()
.ok_or_else(|| Error::Data("no data or request provider set".into()))?;
let ticker = self.ticker.clone().unwrap_or_default();
provider
.request(&ticker, self.timeframe.clone())
.map_err(Error::Data)?
}
};
let syminfo = data.syminfo;
let timeframe = self.timeframe;
let mut bars = data.bars;
if let Some(n) = self.bar_count {
let len = bars.len();
bars = bars.split_off(len.saturating_sub(n.max(1)));
}
let chart_period = bars
.windows(2)
.next()
.map(|pair| pair[1].time - pair[0].time);
let source = self.source.as_str();
let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
let mut lexer = Lexer::with_version(source, version);
let tokens = lexer.tokenize()?;
let mut parser = Parser::new(tokens);
let statements = parser.parse()?;
let program = Program::new(statements);
let mut consts =
pine_builtins::register_namespace_objects(version, Some(syminfo), Some(timeframe));
for (name, value) in self.custom_variables {
consts.insert(name, value);
}
let mut builtins = consts.clone();
for (name, value) in pine_builtins::per_bar_variables(&Bar::default()) {
builtins.insert(name, value);
}
let errors: Vec<_> =
pine_sema::analyze(&program, &builtins, self.library_loader.as_deref())
.into_iter()
.filter(|diagnostic| diagnostic.severity == pine_diagnostics::Severity::Error)
.collect();
if !errors.is_empty() {
return Err(Error::Sema(errors));
}
let mut interpreter = Interpreter::new();
interpreter.library_loader = self.library_loader;
interpreter.request_provider = self.request_provider.map(Rc::from);
interpreter.chart_period = chart_period;
if let Some(broker_factory) = self.broker_factory {
interpreter.broker_factory = Some(broker_factory);
}
interpreter.set_const_variables(consts);
Ok(Script {
program,
interpreter,
bars,
equity_curve: Vec::new(),
last_close: 0.0,
equity_peak: f64::NEG_INFINITY,
equity_trough: f64::INFINITY,
max_drawdown: 0.0,
max_runup: 0.0,
})
}
}
pub struct Script<O: PineOutput> {
program: Program,
interpreter: Interpreter<O>,
bars: Vec<Bar>,
equity_curve: Vec<f64>,
last_close: f64,
equity_peak: f64,
equity_trough: f64,
max_drawdown: f64,
max_runup: f64,
}
impl<O: PineOutput> Script<O> {
pub fn execute(&mut self, bar: &Bar) -> Result<O, Error> {
use interpreter::Value;
for (name, value) in pine_builtins::per_bar_variables(bar) {
if matches!(value, Value::Series(_)) {
self.interpreter.advance_series(&name, value);
} else {
self.interpreter.set_variable(&name, value);
}
}
self.advance_broker(bar);
let output = self.interpreter.execute(&self.program)?;
if let Some(broker) = self.interpreter.broker.as_ref() {
self.equity_curve.push(broker.equity(bar.close));
self.last_close = bar.close;
}
Ok(output)
}
fn advance_broker(&mut self, bar: &Bar) {
use interpreter::Value;
let close = bar.close;
let Some(broker) = self.interpreter.broker.as_mut() else {
return;
};
broker.advance(bar);
let position = broker.position();
let equity = broker.equity(close);
let initial = broker.initial_capital();
let equity_hi = broker.equity(bar.high);
let equity_lo = broker.equity(bar.low);
let intrabar_low = equity_hi.min(equity_lo);
let intrabar_high = equity_hi.max(equity_lo);
let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
let open_trades = broker.open_trades().len() as i64;
let closed_trades = broker.closed_trades().len() as i64;
let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
for trade in broker.closed_trades() {
let profit = trade.profit(close); if profit > 0.0 {
gross_profit += profit;
wins += 1;
} else if profit < 0.0 {
gross_loss -= profit; losses += 1;
} else {
evens += 1;
}
}
if self.equity_peak == f64::NEG_INFINITY {
self.equity_peak = initial;
self.equity_trough = initial;
}
self.equity_peak = self.equity_peak.max(equity);
self.equity_trough = self.equity_trough.min(equity);
self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
let net_profit = equity - initial - open_profit;
let avg_price = if position.size == 0.0 {
Value::Na
} else {
Value::Number(position.avg_price)
};
let refreshed = [
("position_size", Value::Number(position.size)),
("position_avg_price", avg_price),
("equity", Value::Number(equity)),
("netprofit", Value::Number(net_profit)),
("openprofit", Value::Number(open_profit)),
("grossprofit", Value::Number(gross_profit)),
("grossloss", Value::Number(gross_loss)),
("max_drawdown", Value::Number(self.max_drawdown)),
("max_runup", Value::Number(self.max_runup)),
("opentrades", Value::Int(open_trades)),
("closedtrades", Value::Int(closed_trades)),
("wintrades", Value::Int(wins)),
("losstrades", Value::Int(losses)),
("eventrades", Value::Int(evens)),
];
for (name, value) in refreshed {
self.interpreter.set_object_field("strategy", name, value);
}
}
pub fn run(mut self) -> Result<Run<O>, Error> {
let bars = std::mem::take(&mut self.bars);
let outputs = bars
.iter()
.map(|bar| self.execute(bar))
.collect::<Result<Vec<O>, Error>>()?;
let backtest = self.take_backtest();
Ok(Run { outputs, backtest })
}
fn take_backtest(&mut self) -> Option<Backtest> {
let broker = self.interpreter.broker.as_ref()?;
let close = self.last_close;
let mut trades: Vec<_> = broker.closed_trades().to_vec();
trades.extend(broker.open_trades().into_iter().cloned());
let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
let initial_capital = broker.initial_capital();
let position_size = broker.position().size;
let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
for trade in broker.closed_trades() {
let profit = trade.profit(close);
if profit > 0.0 {
gross_profit += profit;
win_trades += 1;
} else if profit < 0.0 {
gross_loss -= profit;
loss_trades += 1;
} else {
even_trades += 1;
}
}
let equity = std::mem::take(&mut self.equity_curve);
let final_equity = equity.last().copied().unwrap_or(initial_capital);
Some(Backtest {
initial_capital,
net_profit: final_equity - initial_capital - open_profit,
open_profit,
gross_profit,
gross_loss,
max_drawdown: self.max_drawdown,
max_runup: self.max_runup,
win_trades,
loss_trades,
even_trades,
position_size,
mark_price: close,
equity,
trades,
})
}
}
pub fn execute(source: &str, data: Data) -> Result<(), Error> {
ScriptBuilder::<DefaultPineOutput>::with_code(source)
.with_data(data)
.compile()?
.run()
.map(|_| ())
}