1pub use pine_ast as ast;
3pub use pine_broker as broker;
4pub use pine_builtins as builtins;
5use pine_builtins::DefaultPineOutput;
6pub use pine_core as core;
7pub use pine_data as data;
8pub use pine_diagnostics as diagnostics;
9pub use pine_format as format;
10pub use pine_interpreter as interpreter;
11pub use pine_lexer as lexer;
12pub use pine_lint as lint;
13pub use pine_parser as parser;
14pub use pine_sema as sema;
15
16mod backtest;
17mod run;
18
19pub use backtest::Backtest;
20pub use pine_core::{DataProvider, DirLoader, FileResolver, LibraryLoader};
21pub use run::{Run, RunResult};
22
23use pine_ast::Program;
24use pine_core::{
25 AlertConditionOutput, BoxOutput, FillOutput, GlobalOutput, InputOutput, LabelOutput,
26 LineOutput, LogOutput, MetadataOutput, PineOutput, PlotOutput, TableOutput,
27};
28use pine_core::{Bar, Data, PineVersion, Timeframe, VersionError};
29use pine_diagnostics::Diagnostic;
30use pine_interpreter::{Interpreter, RuntimeError, Value};
31use pine_lexer::{Lexer, LexerError};
32use pine_parser::{Parser, ParserError};
33use std::collections::HashMap;
34use std::rc::Rc;
35
36#[derive(Debug)]
38pub enum Error {
39 Lexer(LexerError),
40 Parser(ParserError),
41 Runtime(RuntimeError),
42 Sema(Vec<Diagnostic>),
45 Version(VersionError),
48 Data(pine_core::ProviderError),
51}
52
53impl std::fmt::Display for Error {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Error::Lexer(e) => write!(f, "Lexer error: {}", e),
57 Error::Parser(e) => write!(f, "Parser error: {}", e),
58 Error::Runtime(e) => write!(f, "Runtime error: {}", e),
59 Error::Version(e) => write!(f, "Version error: {}", e),
60 Error::Data(e) => write!(f, "Data error: {}", e),
61 Error::Sema(diags) => {
63 for (i, d) in diags.iter().enumerate() {
64 if i > 0 {
65 writeln!(f)?;
66 }
67 write!(f, "{}", d)?;
68 }
69 Ok(())
70 }
71 }
72 }
73}
74
75impl std::error::Error for Error {}
76
77impl From<RuntimeError> for Error {
78 fn from(e: RuntimeError) -> Self {
79 Error::Runtime(e)
80 }
81}
82
83impl From<LexerError> for Error {
84 fn from(e: LexerError) -> Self {
85 Error::Lexer(e)
86 }
87}
88
89impl From<ParserError> for Error {
90 fn from(e: ParserError) -> Self {
91 Error::Parser(e)
92 }
93}
94
95impl From<VersionError> for Error {
96 fn from(e: VersionError) -> Self {
97 Error::Version(e)
98 }
99}
100
101pub fn check(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Vec<Diagnostic>, Error> {
104 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
105 let tokens = Lexer::with_version(source, version).tokenize()?;
106 let program = Program::new(Parser::new(tokens).parse()?);
107
108 let mut env: HashMap<String, Value<DefaultPineOutput>> =
109 pine_builtins::register_namespace_objects(version, None, None);
110 for (name, value) in pine_builtins::per_bar_variables(&Bar::default()) {
111 env.insert(name, value);
112 }
113
114 Ok(pine_sema::analyze(&program, &env, loader))
115}
116
117pub struct ScriptBuilder<O: PineOutput> {
118 source: String,
119 custom_variables: HashMap<String, Value<O>>,
120 library_loader: Option<Box<dyn LibraryLoader>>,
121 request_provider: Option<Box<dyn DataProvider>>,
122 ticker: Option<String>,
123 timeframe: Timeframe,
124 data: Option<Data>,
125 bar_count: Option<usize>,
126 broker_factory: Option<Box<dyn pine_broker::BrokerFactory>>,
127}
128
129impl<O: PineOutput> ScriptBuilder<O> {
130 pub fn with_code(source: &str) -> ScriptBuilder<O> {
131 Self {
132 source: source.to_string(),
133 custom_variables: HashMap::new(),
134 library_loader: None,
135 request_provider: None,
136 ticker: None,
137 timeframe: Timeframe::default(),
138 data: None,
139 bar_count: None,
140 broker_factory: None,
141 }
142 }
143
144 pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
147 self.custom_variables = variables;
148 self
149 }
150
151 pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
153 self.library_loader = Some(loader);
154 self
155 }
156
157 pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
160 self.request_provider = Some(provider);
161 self
162 }
163
164 pub fn with_broker(mut self, factory: Box<dyn pine_broker::BrokerFactory>) -> Self {
167 self.broker_factory = Some(factory);
168 self
169 }
170
171 pub fn with_ticker(mut self, ticker: String) -> Self {
172 self.ticker = Some(ticker);
173 self
174 }
175
176 pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
179 self.timeframe = timeframe;
180 self
181 }
182
183 pub fn with_bar_count(mut self, bar_count: usize) -> Self {
186 self.bar_count = Some(bar_count);
187 self
188 }
189
190 pub fn with_data(mut self, data: Data) -> Self {
198 self.data = Some(data);
199 self
200 }
201
202 pub fn compile(self) -> Result<Script<O>, Error>
204 where
205 O: LogOutput
206 + PlotOutput
207 + LabelOutput
208 + BoxOutput
209 + InputOutput
210 + LineOutput
211 + TableOutput
212 + MetadataOutput
213 + GlobalOutput
214 + AlertConditionOutput
215 + FillOutput,
216 {
217 let data = match self.data {
218 Some(data) => data,
219 None => {
220 let provider = self
221 .request_provider
222 .as_ref()
223 .ok_or_else(|| Error::Data("no data or request provider set".into()))?;
224
225 let ticker = self.ticker.clone().unwrap_or_default();
226 provider
227 .request(&ticker, self.timeframe.clone())
228 .map_err(Error::Data)?
229 }
230 };
231
232 let syminfo = data.syminfo;
233 let timeframe = self.timeframe;
234
235 let mut bars = data.bars;
237 if let Some(n) = self.bar_count {
238 let len = bars.len();
239 bars = bars.split_off(len.saturating_sub(n.max(1)));
240 }
241
242 let chart_period = bars
245 .windows(2)
246 .next()
247 .map(|pair| pair[1].time - pair[0].time);
248
249 let source = self.source.as_str();
250 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
251
252 let mut lexer = Lexer::with_version(source, version);
253 let tokens = lexer.tokenize()?;
254
255 let mut parser = Parser::new(tokens);
256 let statements = parser.parse()?;
257 let program = Program::new(statements);
258
259 let mut consts =
262 pine_builtins::register_namespace_objects(version, Some(syminfo), Some(timeframe));
263 for (name, value) in self.custom_variables {
264 consts.insert(name, value);
265 }
266
267 let mut builtins = consts.clone();
268 for (name, value) in pine_builtins::per_bar_variables(&Bar::default()) {
269 builtins.insert(name, value);
270 }
271
272 let errors: Vec<_> =
274 pine_sema::analyze(&program, &builtins, self.library_loader.as_deref())
275 .into_iter()
276 .filter(|diagnostic| diagnostic.severity == pine_diagnostics::Severity::Error)
277 .collect();
278 if !errors.is_empty() {
279 return Err(Error::Sema(errors));
280 }
281
282 let mut interpreter = Interpreter::new();
284 interpreter.library_loader = self.library_loader;
285 interpreter.request_provider = self.request_provider.map(Rc::from);
286 interpreter.chart_period = chart_period;
287 if let Some(broker_factory) = self.broker_factory {
288 interpreter.broker_factory = Some(broker_factory);
289 }
290 interpreter.set_const_variables(consts);
291
292 Ok(Script {
293 program,
294 interpreter,
295 bars,
296 equity_curve: Vec::new(),
297 last_close: 0.0,
298 equity_peak: f64::NEG_INFINITY,
299 equity_trough: f64::INFINITY,
300 max_drawdown: 0.0,
301 max_runup: 0.0,
302 })
303 }
304}
305
306pub struct Script<O: PineOutput> {
313 program: Program,
314 interpreter: Interpreter<O>,
315 bars: Vec<Bar>,
317 equity_curve: Vec<f64>,
319 last_close: f64,
321 equity_peak: f64,
323 equity_trough: f64,
324 max_drawdown: f64,
325 max_runup: f64,
326}
327
328impl<O: PineOutput> Script<O> {
329 pub fn execute(&mut self, bar: &Bar) -> Result<O, Error> {
332 use interpreter::Value;
333
334 for (name, value) in pine_builtins::per_bar_variables(bar) {
335 if matches!(value, Value::Series(_)) {
336 self.interpreter.advance_series(&name, value);
337 } else {
338 self.interpreter.set_variable(&name, value);
339 }
340 }
341
342 self.advance_broker(bar);
346
347 let output = self.interpreter.execute(&self.program)?;
348
349 if let Some(broker) = self.interpreter.broker.as_ref() {
351 self.equity_curve.push(broker.equity(bar.close));
352 self.last_close = bar.close;
353 }
354
355 Ok(output)
356 }
357
358 fn advance_broker(&mut self, bar: &Bar) {
363 use interpreter::Value;
364
365 let close = bar.close;
366
367 let Some(broker) = self.interpreter.broker.as_mut() else {
369 return;
370 };
371 broker.advance(bar);
372
373 let position = broker.position();
374 let equity = broker.equity(close);
375 let initial = broker.initial_capital();
376 let equity_hi = broker.equity(bar.high);
379 let equity_lo = broker.equity(bar.low);
380 let intrabar_low = equity_hi.min(equity_lo);
381 let intrabar_high = equity_hi.max(equity_lo);
382 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
383 let open_trades = broker.open_trades().len() as i64;
384 let closed_trades = broker.closed_trades().len() as i64;
385
386 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
387 let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
388 for trade in broker.closed_trades() {
389 let profit = trade.profit(close); if profit > 0.0 {
391 gross_profit += profit;
392 wins += 1;
393 } else if profit < 0.0 {
394 gross_loss -= profit; losses += 1;
396 } else {
397 evens += 1;
398 }
399 }
400
401 if self.equity_peak == f64::NEG_INFINITY {
405 self.equity_peak = initial;
406 self.equity_trough = initial;
407 }
408 self.equity_peak = self.equity_peak.max(equity);
409 self.equity_trough = self.equity_trough.min(equity);
410 self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
411 self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
412
413 let net_profit = equity - initial - open_profit;
416 let avg_price = if position.size == 0.0 {
418 Value::Na
419 } else {
420 Value::Number(position.avg_price)
421 };
422
423 let refreshed = [
424 ("position_size", Value::Number(position.size)),
425 ("position_avg_price", avg_price),
426 ("equity", Value::Number(equity)),
427 ("netprofit", Value::Number(net_profit)),
428 ("openprofit", Value::Number(open_profit)),
429 ("grossprofit", Value::Number(gross_profit)),
430 ("grossloss", Value::Number(gross_loss)),
431 ("max_drawdown", Value::Number(self.max_drawdown)),
432 ("max_runup", Value::Number(self.max_runup)),
433 ("opentrades", Value::Int(open_trades)),
434 ("closedtrades", Value::Int(closed_trades)),
435 ("wintrades", Value::Int(wins)),
436 ("losstrades", Value::Int(losses)),
437 ("eventrades", Value::Int(evens)),
438 ];
439 for (name, value) in refreshed {
440 self.interpreter.set_object_field("strategy", name, value);
441 }
442 }
443
444 pub fn run(mut self) -> Result<Run<O>, Error> {
447 let bars = std::mem::take(&mut self.bars);
448 let outputs = bars
449 .iter()
450 .map(|bar| self.execute(bar))
451 .collect::<Result<Vec<O>, Error>>()?;
452 let backtest = self.take_backtest();
453 Ok(Run { outputs, backtest })
454 }
455
456 fn take_backtest(&mut self) -> Option<Backtest> {
457 let broker = self.interpreter.broker.as_ref()?;
458 let close = self.last_close;
459
460 let mut trades: Vec<_> = broker.closed_trades().to_vec();
462 trades.extend(broker.open_trades().into_iter().cloned());
463
464 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
465 let initial_capital = broker.initial_capital();
466 let position_size = broker.position().size;
467
468 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
469 let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
470 for trade in broker.closed_trades() {
471 let profit = trade.profit(close);
472 if profit > 0.0 {
473 gross_profit += profit;
474 win_trades += 1;
475 } else if profit < 0.0 {
476 gross_loss -= profit;
477 loss_trades += 1;
478 } else {
479 even_trades += 1;
480 }
481 }
482
483 let equity = std::mem::take(&mut self.equity_curve);
484 let final_equity = equity.last().copied().unwrap_or(initial_capital);
485
486 Some(Backtest {
487 initial_capital,
488 net_profit: final_equity - initial_capital - open_profit,
489 open_profit,
490 gross_profit,
491 gross_loss,
492 max_drawdown: self.max_drawdown,
493 max_runup: self.max_runup,
494 win_trades,
495 loss_trades,
496 even_trades,
497 position_size,
498 mark_price: close,
499 equity,
500 trades,
501 })
502 }
503}
504
505pub fn execute(source: &str, data: Data) -> Result<(), Error> {
506 ScriptBuilder::<DefaultPineOutput>::with_code(source)
507 .with_data(data)
508 .compile()?
509 .run()
510 .map(|_| ())
511}