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;
16pub use run::{Run, RunResult};
17
18use pine_ast::Program;
19use pine_core::{Bar, Data, PineVersion, Timeframe, VersionError};
20use pine_diagnostics::Diagnostic;
21use pine_interpreter::{
22 AlertConditionOutput, BoxOutput, FillOutput, GlobalOutput, IndicatorOutput, InputOutput,
23 Interpreter, LabelOutput, LibraryLoader, LineOutput, LogOutput, PineOutput, PlotOutput,
24 RuntimeError, TableOutput, Value,
25};
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 diagnostics = pine_sema::analyze(&program, &builtins);
259 if !diagnostics.is_empty() {
260 return Err(Error::Sema(diagnostics));
261 }
262
263 let mut interpreter = Interpreter::new();
265 if let Some(library_loader) = self.library_loader {
266 interpreter.set_library_loader(library_loader);
267 }
268 interpreter.request_provider = self.request_provider.map(Rc::from);
270 interpreter.chart_period = chart_period;
271
272 for (name, value) in namespaces {
274 interpreter.set_const_variable(&name, value);
275 }
276
277 for (name, value) in self.custom_variables {
278 interpreter.set_const_variable(&name, value);
279 }
280
281 Ok(Script {
282 program,
283 interpreter,
284 bars,
285 equity_curve: Vec::new(),
286 last_close: 0.0,
287 equity_peak: f64::NEG_INFINITY,
288 equity_trough: f64::INFINITY,
289 max_drawdown: 0.0,
290 max_runup: 0.0,
291 })
292 }
293}
294
295pub struct Script<O: PineOutput> {
302 program: Program,
303 interpreter: Interpreter<O>,
304 bars: Vec<Bar>,
306 equity_curve: Vec<f64>,
308 last_close: f64,
310 equity_peak: f64,
312 equity_trough: f64,
313 max_drawdown: f64,
314 max_runup: f64,
315}
316
317impl<O: PineOutput> Script<O> {
318 fn execute(&mut self, bar: &Bar) -> Result<O, Error> {
321 use interpreter::{Series, Value};
323
324 for (id, value) in [
326 ("open", bar.open),
327 ("high", bar.high),
328 ("low", bar.low),
329 ("close", bar.close),
330 ("volume", bar.volume),
331 ("hl2", (bar.high + bar.low) / 2.0),
332 ("hlc3", (bar.high + bar.low + bar.close) / 3.0),
333 ("hlcc4", (bar.high + bar.low + bar.close * 2.0) / 4.0),
334 ("ohlc4", (bar.open + bar.high + bar.low + bar.close) / 4.0),
335 ] {
336 self.interpreter.advance_series(
337 id,
338 Value::Series(Series {
339 id: id.to_string(),
340 current: Box::new(Value::Number(value)),
341 }),
342 );
343 }
344
345 self.interpreter
346 .set_variable("bar_index", Value::Number(bar.index as f64));
347
348 for (name, value) in pine_builtins::register_per_bar(bar) {
350 self.interpreter.set_variable(&name, value);
351 }
352
353 self.advance_broker(bar);
357
358 let output = self.interpreter.execute(&self.program)?;
359
360 if let Some(broker) = self.interpreter.broker.as_ref() {
362 self.equity_curve.push(broker.equity(bar.close));
363 self.last_close = bar.close;
364 }
365
366 Ok(output)
367 }
368
369 fn advance_broker(&mut self, bar: &Bar) {
374 use interpreter::Value;
375
376 let close = bar.close;
377
378 let Some(broker) = self.interpreter.broker.as_mut() else {
380 return;
381 };
382 broker.advance(bar);
383
384 let position = broker.position();
385 let equity = broker.equity(close);
386 let initial = broker.initial_capital();
387 let equity_hi = broker.equity(bar.high);
390 let equity_lo = broker.equity(bar.low);
391 let intrabar_low = equity_hi.min(equity_lo);
392 let intrabar_high = equity_hi.max(equity_lo);
393 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
394 let open_trades = broker.open_trades().len() as i64;
395 let closed_trades = broker.closed_trades().len() as i64;
396
397 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
398 let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
399 for trade in broker.closed_trades() {
400 let profit = trade.profit(close); if profit > 0.0 {
402 gross_profit += profit;
403 wins += 1;
404 } else if profit < 0.0 {
405 gross_loss -= profit; losses += 1;
407 } else {
408 evens += 1;
409 }
410 }
411
412 if self.equity_peak == f64::NEG_INFINITY {
416 self.equity_peak = initial;
417 self.equity_trough = initial;
418 }
419 self.equity_peak = self.equity_peak.max(equity);
420 self.equity_trough = self.equity_trough.min(equity);
421 self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
422 self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
423
424 let net_profit = equity - initial - open_profit;
427 let avg_price = if position.size == 0.0 {
429 Value::Na
430 } else {
431 Value::Number(position.avg_price)
432 };
433
434 let refreshed = [
435 ("position_size", Value::Number(position.size)),
436 ("position_avg_price", avg_price),
437 ("equity", Value::Number(equity)),
438 ("netprofit", Value::Number(net_profit)),
439 ("openprofit", Value::Number(open_profit)),
440 ("grossprofit", Value::Number(gross_profit)),
441 ("grossloss", Value::Number(gross_loss)),
442 ("max_drawdown", Value::Number(self.max_drawdown)),
443 ("max_runup", Value::Number(self.max_runup)),
444 ("opentrades", Value::Int(open_trades)),
445 ("closedtrades", Value::Int(closed_trades)),
446 ("wintrades", Value::Int(wins)),
447 ("losstrades", Value::Int(losses)),
448 ("eventrades", Value::Int(evens)),
449 ];
450 for (name, value) in refreshed {
451 self.interpreter.set_object_field("strategy", name, value);
452 }
453 }
454
455 pub fn run(mut self) -> Result<Run<O>, Error> {
458 let bars = std::mem::take(&mut self.bars);
459 let outputs = bars
460 .iter()
461 .map(|bar| self.execute(bar))
462 .collect::<Result<Vec<O>, Error>>()?;
463 let backtest = self.take_backtest();
464 Ok(Run { outputs, backtest })
465 }
466
467 fn take_backtest(&mut self) -> Option<Backtest> {
468 let broker = self.interpreter.broker.as_ref()?;
469 let close = self.last_close;
470
471 let mut trades: Vec<_> = broker.closed_trades().to_vec();
473 trades.extend(broker.open_trades().into_iter().cloned());
474
475 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
476 let initial_capital = broker.initial_capital();
477 let position_size = broker.position().size;
478
479 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
480 let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
481 for trade in broker.closed_trades() {
482 let profit = trade.profit(close);
483 if profit > 0.0 {
484 gross_profit += profit;
485 win_trades += 1;
486 } else if profit < 0.0 {
487 gross_loss -= profit;
488 loss_trades += 1;
489 } else {
490 even_trades += 1;
491 }
492 }
493
494 let equity = std::mem::take(&mut self.equity_curve);
495 let final_equity = equity.last().copied().unwrap_or(initial_capital);
496
497 Some(Backtest {
498 initial_capital,
499 net_profit: final_equity - initial_capital - open_profit,
500 open_profit,
501 gross_profit,
502 gross_loss,
503 max_drawdown: self.max_drawdown,
504 max_runup: self.max_runup,
505 win_trades,
506 loss_trades,
507 even_trades,
508 position_size,
509 mark_price: close,
510 equity,
511 trades,
512 })
513 }
514}
515
516pub fn execute(source: &str, data: Data) -> Result<(), Error> {
517 ScriptBuilder::<DefaultPineOutput>::with_code(source)
518 .with_data(data)
519 .compile()?
520 .run()
521 .map(|_| ())
522}