1pub use pine_ast as ast;
3pub use pine_builtins as builtins;
4use pine_builtins::DefaultPineOutput;
5pub use pine_core as core;
6pub use pine_data as data;
7pub use pine_interpreter as interpreter;
8pub use pine_lexer as lexer;
9pub use pine_parser as parser;
10
11mod backtest;
12mod run;
13
14pub use backtest::Backtest;
15pub use pine_core::{DataProvider, FileResolver, LibraryLoader};
16pub use run::{Run, RunResult};
17
18use pine_ast::Program;
19use pine_core::{
20 AlertConditionOutput, BoxOutput, FillOutput, GlobalOutput, IndicatorOutput, InputOutput,
21 LabelOutput, LineOutput, LogOutput, PineOutput, PlotOutput, TableOutput,
22};
23use pine_core::{Bar, Data, PineVersion, Timeframe, VersionError};
24use pine_diagnostics::Diagnostic;
25use pine_interpreter::{Interpreter, RuntimeError, Value};
26use pine_lexer::{Lexer, LexerError};
27use pine_parser::{Parser, ParserError};
28use std::collections::HashMap;
29use std::rc::Rc;
30
31#[derive(Debug)]
33pub enum Error {
34 Lexer(LexerError),
35 Parser(ParserError),
36 Runtime(RuntimeError),
37 Sema(Vec<Diagnostic>),
40 Version(VersionError),
43 Data(pine_core::ProviderError),
46}
47
48impl std::fmt::Display for Error {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Error::Lexer(e) => write!(f, "Lexer error: {}", e),
52 Error::Parser(e) => write!(f, "Parser error: {}", e),
53 Error::Runtime(e) => write!(f, "Runtime error: {}", e),
54 Error::Version(e) => write!(f, "Version error: {}", e),
55 Error::Data(e) => write!(f, "Data error: {}", e),
56 Error::Sema(diags) => {
58 for (i, d) in diags.iter().enumerate() {
59 if i > 0 {
60 writeln!(f)?;
61 }
62 write!(f, "{}", d)?;
63 }
64 Ok(())
65 }
66 }
67 }
68}
69
70impl std::error::Error for Error {}
71
72impl From<RuntimeError> for Error {
73 fn from(e: RuntimeError) -> Self {
74 Error::Runtime(e)
75 }
76}
77
78impl From<LexerError> for Error {
79 fn from(e: LexerError) -> Self {
80 Error::Lexer(e)
81 }
82}
83
84impl From<ParserError> for Error {
85 fn from(e: ParserError) -> Self {
86 Error::Parser(e)
87 }
88}
89
90impl From<VersionError> for Error {
91 fn from(e: VersionError) -> Self {
92 Error::Version(e)
93 }
94}
95
96pub struct ScriptBuilder<O: PineOutput> {
97 source: String,
98 custom_variables: HashMap<String, Value<O>>,
99 library_loader: Option<Box<dyn LibraryLoader>>,
100 request_provider: Option<Box<dyn DataProvider>>,
101 ticker: Option<String>,
102 timeframe: Timeframe,
103 data: Option<Data>,
104 bar_count: Option<usize>,
105}
106
107impl<O: PineOutput> ScriptBuilder<O> {
108 pub fn with_code(source: &str) -> ScriptBuilder<O> {
109 Self {
110 source: source.to_string(),
111 custom_variables: HashMap::new(),
112 library_loader: None,
113 request_provider: None,
114 ticker: None,
115 timeframe: Timeframe::default(),
116 data: None,
117 bar_count: None,
118 }
119 }
120
121 pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
124 self.custom_variables = variables;
125 self
126 }
127
128 pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
130 self.library_loader = Some(loader);
131 self
132 }
133
134 pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
137 self.request_provider = Some(provider);
138 self
139 }
140
141 pub fn with_ticker(mut self, ticker: String) -> Self {
142 self.ticker = Some(ticker);
143 self
144 }
145
146 pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
149 self.timeframe = timeframe;
150 self
151 }
152
153 pub fn with_bar_count(mut self, bar_count: usize) -> Self {
156 self.bar_count = Some(bar_count);
157 self
158 }
159
160 pub fn with_data(mut self, data: Data) -> Self {
168 self.data = Some(data);
169 self
170 }
171
172 pub fn compile(self) -> Result<Script<O>, Error>
174 where
175 O: LogOutput
176 + PlotOutput
177 + LabelOutput
178 + BoxOutput
179 + InputOutput
180 + LineOutput
181 + TableOutput
182 + IndicatorOutput
183 + GlobalOutput
184 + AlertConditionOutput
185 + FillOutput,
186 {
187 let data = match self.data {
188 Some(data) => data,
189 None => {
190 let provider = self
191 .request_provider
192 .as_ref()
193 .ok_or_else(|| Error::Data("no data or request provider set".into()))?;
194
195 let ticker = self.ticker.clone().unwrap_or_default();
196 provider
197 .request(&ticker, self.timeframe.clone())
198 .map_err(Error::Data)?
199 }
200 };
201
202 let syminfo = data.syminfo;
203 let timeframe = self.timeframe;
204
205 let mut bars = data.bars;
207 if let Some(n) = self.bar_count {
208 let len = bars.len();
209 bars = bars.split_off(len.saturating_sub(n.max(1)));
210 }
211
212 let chart_period = bars
215 .windows(2)
216 .next()
217 .map(|pair| pair[1].time - pair[0].time);
218
219 let source = self.source.as_str();
220 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
221
222 let mut lexer = Lexer::with_version(source, version);
223 let tokens = lexer.tokenize()?;
224
225 let mut parser = Parser::new(tokens);
226 let statements = parser.parse()?;
227 let program = Program::new(statements);
228
229 let namespaces =
230 pine_builtins::register_namespace_objects(version, Some(syminfo), Some(timeframe));
231
232 let mut builtins = namespaces.clone();
236 for (name, value) in pine_builtins::register_per_bar(&Bar::default()) {
237 builtins.insert(name, value);
238 }
239 for name in [
240 "open",
241 "high",
242 "low",
243 "close",
244 "volume",
245 "hl2",
246 "hlc3",
247 "hlcc4",
248 "ohlc4",
249 "bar_index",
250 ] {
251 builtins.insert(name.to_string(), Value::Na);
252 }
253 for (name, value) in &self.custom_variables {
254 builtins.insert(name.clone(), value.clone());
255 }
256
257 let errors: Vec<_> =
259 pine_sema::analyze(&program, &builtins, self.library_loader.as_deref())
260 .into_iter()
261 .filter(|diagnostic| diagnostic.severity == pine_diagnostics::Severity::Error)
262 .collect();
263 if !errors.is_empty() {
264 return Err(Error::Sema(errors));
265 }
266
267 let mut interpreter = Interpreter::new();
269 if let Some(library_loader) = self.library_loader {
270 interpreter.set_library_loader(library_loader);
271 }
272 interpreter.request_provider = self.request_provider.map(Rc::from);
274 interpreter.chart_period = chart_period;
275
276 for (name, value) in namespaces {
278 interpreter.set_const_variable(&name, value);
279 }
280
281 for (name, value) in self.custom_variables {
282 interpreter.set_const_variable(&name, value);
283 }
284
285 Ok(Script {
286 program,
287 interpreter,
288 bars,
289 equity_curve: Vec::new(),
290 last_close: 0.0,
291 equity_peak: f64::NEG_INFINITY,
292 equity_trough: f64::INFINITY,
293 max_drawdown: 0.0,
294 max_runup: 0.0,
295 })
296 }
297}
298
299pub struct Script<O: PineOutput> {
306 program: Program,
307 interpreter: Interpreter<O>,
308 bars: Vec<Bar>,
310 equity_curve: Vec<f64>,
312 last_close: f64,
314 equity_peak: f64,
316 equity_trough: f64,
317 max_drawdown: f64,
318 max_runup: f64,
319}
320
321impl<O: PineOutput> Script<O> {
322 fn execute(&mut self, bar: &Bar) -> Result<O, Error> {
325 use interpreter::{Series, Value};
327
328 for (id, value) in [
330 ("open", bar.open),
331 ("high", bar.high),
332 ("low", bar.low),
333 ("close", bar.close),
334 ("volume", bar.volume),
335 ("hl2", (bar.high + bar.low) / 2.0),
336 ("hlc3", (bar.high + bar.low + bar.close) / 3.0),
337 ("hlcc4", (bar.high + bar.low + bar.close * 2.0) / 4.0),
338 ("ohlc4", (bar.open + bar.high + bar.low + bar.close) / 4.0),
339 ] {
340 self.interpreter.advance_series(
341 id,
342 Value::Series(Series {
343 id: id.to_string(),
344 current: Box::new(Value::Number(value)),
345 }),
346 );
347 }
348
349 self.interpreter
350 .set_variable("bar_index", Value::Number(bar.index as f64));
351
352 for (name, value) in pine_builtins::register_per_bar(bar) {
354 self.interpreter.set_variable(&name, value);
355 }
356
357 self.advance_broker(bar);
361
362 let output = self.interpreter.execute(&self.program)?;
363
364 if let Some(broker) = self.interpreter.broker.as_ref() {
366 self.equity_curve.push(broker.equity(bar.close));
367 self.last_close = bar.close;
368 }
369
370 Ok(output)
371 }
372
373 fn advance_broker(&mut self, bar: &Bar) {
378 use interpreter::Value;
379
380 let close = bar.close;
381
382 let Some(broker) = self.interpreter.broker.as_mut() else {
384 return;
385 };
386 broker.advance(bar);
387
388 let position = broker.position();
389 let equity = broker.equity(close);
390 let initial = broker.initial_capital();
391 let equity_hi = broker.equity(bar.high);
394 let equity_lo = broker.equity(bar.low);
395 let intrabar_low = equity_hi.min(equity_lo);
396 let intrabar_high = equity_hi.max(equity_lo);
397 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
398 let open_trades = broker.open_trades().len() as i64;
399 let closed_trades = broker.closed_trades().len() as i64;
400
401 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
402 let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
403 for trade in broker.closed_trades() {
404 let profit = trade.profit(close); if profit > 0.0 {
406 gross_profit += profit;
407 wins += 1;
408 } else if profit < 0.0 {
409 gross_loss -= profit; losses += 1;
411 } else {
412 evens += 1;
413 }
414 }
415
416 if self.equity_peak == f64::NEG_INFINITY {
420 self.equity_peak = initial;
421 self.equity_trough = initial;
422 }
423 self.equity_peak = self.equity_peak.max(equity);
424 self.equity_trough = self.equity_trough.min(equity);
425 self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
426 self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
427
428 let net_profit = equity - initial - open_profit;
431 let avg_price = if position.size == 0.0 {
433 Value::Na
434 } else {
435 Value::Number(position.avg_price)
436 };
437
438 let refreshed = [
439 ("position_size", Value::Number(position.size)),
440 ("position_avg_price", avg_price),
441 ("equity", Value::Number(equity)),
442 ("netprofit", Value::Number(net_profit)),
443 ("openprofit", Value::Number(open_profit)),
444 ("grossprofit", Value::Number(gross_profit)),
445 ("grossloss", Value::Number(gross_loss)),
446 ("max_drawdown", Value::Number(self.max_drawdown)),
447 ("max_runup", Value::Number(self.max_runup)),
448 ("opentrades", Value::Int(open_trades)),
449 ("closedtrades", Value::Int(closed_trades)),
450 ("wintrades", Value::Int(wins)),
451 ("losstrades", Value::Int(losses)),
452 ("eventrades", Value::Int(evens)),
453 ];
454 for (name, value) in refreshed {
455 self.interpreter.set_object_field("strategy", name, value);
456 }
457 }
458
459 pub fn run(mut self) -> Result<Run<O>, Error> {
462 let bars = std::mem::take(&mut self.bars);
463 let outputs = bars
464 .iter()
465 .map(|bar| self.execute(bar))
466 .collect::<Result<Vec<O>, Error>>()?;
467 let backtest = self.take_backtest();
468 Ok(Run { outputs, backtest })
469 }
470
471 fn take_backtest(&mut self) -> Option<Backtest> {
472 let broker = self.interpreter.broker.as_ref()?;
473 let close = self.last_close;
474
475 let mut trades: Vec<_> = broker.closed_trades().to_vec();
477 trades.extend(broker.open_trades().into_iter().cloned());
478
479 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
480 let initial_capital = broker.initial_capital();
481 let position_size = broker.position().size;
482
483 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
484 let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
485 for trade in broker.closed_trades() {
486 let profit = trade.profit(close);
487 if profit > 0.0 {
488 gross_profit += profit;
489 win_trades += 1;
490 } else if profit < 0.0 {
491 gross_loss -= profit;
492 loss_trades += 1;
493 } else {
494 even_trades += 1;
495 }
496 }
497
498 let equity = std::mem::take(&mut self.equity_curve);
499 let final_equity = equity.last().copied().unwrap_or(initial_capital);
500
501 Some(Backtest {
502 initial_capital,
503 net_profit: final_equity - initial_capital - open_profit,
504 open_profit,
505 gross_profit,
506 gross_loss,
507 max_drawdown: self.max_drawdown,
508 max_runup: self.max_runup,
509 win_trades,
510 loss_trades,
511 even_trades,
512 position_size,
513 mark_price: close,
514 equity,
515 trades,
516 })
517 }
518}
519
520pub fn execute(source: &str, data: Data) -> Result<(), Error> {
521 ScriptBuilder::<DefaultPineOutput>::with_code(source)
522 .with_data(data)
523 .compile()?
524 .run()
525 .map(|_| ())
526}