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, Metrics};
20pub use pine_core::{DataProvider, DirLoader, FileResolver, LibraryLoader};
21pub use run::{Run, RunResult};
22
23use pine_ast::Program;
24use pine_core::{
25 AlertConditionOutput, BoxOutput, DrawingOutput, FillOutput, GlobalOutput, InputOutput,
26 LabelOutput, 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 Error {
54 pub fn location(&self) -> Option<(u32, u32)> {
57 match self {
58 Error::Lexer(e) => Some(e.location()),
59 Error::Parser(e) => Some(e.location()),
60 _ => None,
61 }
62 }
63}
64
65impl std::fmt::Display for Error {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Error::Lexer(e) => write!(f, "Lexer error: {}", e),
69 Error::Parser(e) => write!(f, "Parser error: {}", e),
70 Error::Runtime(e) => write!(f, "Runtime error: {}", e),
71 Error::Version(e) => write!(f, "Version error: {}", e),
72 Error::Data(e) => write!(f, "Data error: {}", e),
73 Error::Sema(diags) => {
75 for (i, d) in diags.iter().enumerate() {
76 if i > 0 {
77 writeln!(f)?;
78 }
79 write!(f, "{}", d)?;
80 }
81 Ok(())
82 }
83 }
84 }
85}
86
87impl std::error::Error for Error {}
88
89impl From<RuntimeError> for Error {
90 fn from(e: RuntimeError) -> Self {
91 Error::Runtime(e)
92 }
93}
94
95impl From<LexerError> for Error {
96 fn from(e: LexerError) -> Self {
97 Error::Lexer(e)
98 }
99}
100
101impl From<ParserError> for Error {
102 fn from(e: ParserError) -> Self {
103 Error::Parser(e)
104 }
105}
106
107impl From<VersionError> for Error {
108 fn from(e: VersionError) -> Self {
109 Error::Version(e)
110 }
111}
112
113pub struct Analysis {
114 pub diagnostics: Vec<Diagnostic>,
115 pub symbols: sema::SymbolTable,
116}
117
118pub fn analyze(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Analysis, Error> {
120 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
121 let tokens = Lexer::with_version(source, version).tokenize()?;
122 let program = Parser::new(tokens).parse_program()?;
123
124 let (mut env, _): (HashMap<String, Value<DefaultPineOutput>>, _) =
125 pine_builtins::register_namespace_objects(version, None, None);
126 for (name, value) in pine_builtins::per_bar_variables(&Bar::default(), None) {
127 env.insert(name, value);
128 }
129
130 let (mut diagnostics, symbols) = pine_sema::analyze_with_symbols(&program, &env, loader);
131 diagnostics.extend(pine_lint::lint(&program));
132 diagnostics.sort_by_key(|d| d.pos.unwrap_or((u32::MAX, u32::MAX)));
133 Ok(Analysis {
134 diagnostics,
135 symbols,
136 })
137}
138
139pub fn check(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Vec<Diagnostic>, Error> {
141 Ok(analyze(source, loader)?.diagnostics)
142}
143
144pub fn lint_source(source: &str) -> Result<Vec<Diagnostic>, Error> {
147 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
148 let tokens = Lexer::with_version(source, version).tokenize()?;
149 let program = Parser::new(tokens).parse_program()?;
150 Ok(pine_lint::lint(&program))
151}
152
153pub fn inputs_from_json(
157 json: &str,
158) -> Result<HashMap<String, pine_core::InputValue>, serde_json::Error> {
159 serde_json::from_str(json)
160}
161
162pub struct ScriptBuilder<O: PineOutput> {
163 source: String,
164 custom_variables: HashMap<String, Value<O>>,
165 inputs: HashMap<String, pine_core::InputValue>,
166 library_loader: Option<Box<dyn LibraryLoader>>,
167 request_provider: Option<Box<dyn DataProvider>>,
168 ticker: Option<String>,
169 timeframe: Timeframe,
170 data: Option<Data>,
171 bar_count: Option<usize>,
172 broker_factory: Option<Box<dyn pine_broker::BrokerFactory>>,
173}
174
175impl<O: PineOutput> ScriptBuilder<O> {
176 pub fn with_code(source: &str) -> ScriptBuilder<O> {
177 Self {
178 source: source.to_string(),
179 custom_variables: HashMap::new(),
180 inputs: HashMap::new(),
181 library_loader: None,
182 request_provider: None,
183 ticker: None,
184 timeframe: Timeframe::default(),
185 data: None,
186 bar_count: None,
187 broker_factory: None,
188 }
189 }
190
191 pub fn with_inputs(mut self, inputs: HashMap<String, pine_core::InputValue>) -> Self {
195 self.inputs = inputs;
196 self
197 }
198
199 pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
202 self.custom_variables = variables;
203 self
204 }
205
206 pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
208 self.library_loader = Some(loader);
209 self
210 }
211
212 pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
215 self.request_provider = Some(provider);
216 self
217 }
218
219 pub fn with_broker(mut self, factory: Box<dyn pine_broker::BrokerFactory>) -> Self {
222 self.broker_factory = Some(factory);
223 self
224 }
225
226 pub fn with_ticker(mut self, ticker: String) -> Self {
227 self.ticker = Some(ticker);
228 self
229 }
230
231 pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
234 self.timeframe = timeframe;
235 self
236 }
237
238 pub fn with_bar_count(mut self, bar_count: usize) -> Self {
241 self.bar_count = Some(bar_count);
242 self
243 }
244
245 pub fn with_data(mut self, data: Data) -> Self {
253 self.data = Some(data);
254 self
255 }
256
257 pub fn compile(self) -> Result<Script<O>, Error>
259 where
260 O: LogOutput
261 + PlotOutput
262 + LabelOutput
263 + BoxOutput
264 + InputOutput
265 + LineOutput
266 + TableOutput
267 + MetadataOutput
268 + GlobalOutput
269 + AlertConditionOutput
270 + FillOutput
271 + DrawingOutput,
272 {
273 let data = match self.data {
274 Some(data) => data,
275 None => {
276 let provider = self
277 .request_provider
278 .as_ref()
279 .ok_or_else(|| Error::Data("no data or request provider set".into()))?;
280
281 let ticker = self.ticker.clone().unwrap_or_default();
282 provider
283 .request(&ticker, self.timeframe.clone())
284 .map_err(Error::Data)?
285 }
286 };
287
288 let syminfo = data.syminfo;
289 let timeframe = self.timeframe;
290
291 let mut bars = data.bars;
293 if let Some(n) = self.bar_count {
294 let len = bars.len();
295 bars = bars.split_off(len.saturating_sub(n.max(1)));
296 }
297
298 let chart_period = bars
301 .windows(2)
302 .next()
303 .map(|pair| pair[1].time - pair[0].time);
304
305 let source = self.source.as_str();
306 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
307
308 let mut lexer = Lexer::with_version(source, version);
309 let tokens = lexer.tokenize()?;
310
311 let mut parser = Parser::new(tokens);
312 let statements = parser.parse()?;
313 let program = Program::new(statements);
314
315 let (mut consts, advances) = pine_builtins::register_namespace_objects(
318 version,
319 Some(syminfo),
320 Some(timeframe.clone()),
321 );
322 for (name, value) in self.custom_variables {
323 consts.insert(name, value);
324 }
325
326 let mut builtins = consts.clone();
327 for (name, value) in pine_builtins::per_bar_variables(&Bar::default(), None) {
328 builtins.insert(name, value);
329 }
330
331 let errors: Vec<_> =
333 pine_sema::analyze(&program, &builtins, self.library_loader.as_deref())
334 .into_iter()
335 .filter(|diagnostic| diagnostic.severity == pine_diagnostics::Severity::Error)
336 .collect();
337 if !errors.is_empty() {
338 return Err(Error::Sema(errors));
339 }
340
341 let mut interpreter = Interpreter::new();
343 interpreter.library_loader = self.library_loader;
344 interpreter.request_provider = self.request_provider.map(Rc::from);
345 interpreter.chart_period = chart_period;
346 if let Some(broker_factory) = self.broker_factory {
347 interpreter.broker_factory = Some(broker_factory);
348 }
349 interpreter.set_const_variables(consts);
350 interpreter.per_bar_advances = advances;
351 interpreter.inputs = self.inputs;
352
353 Ok(Script {
354 program,
355 interpreter,
356 timeframe,
357 bars,
358 equity_curve: Vec::new(),
359 last_close: 0.0,
360 equity_peak: f64::NEG_INFINITY,
361 equity_trough: f64::INFINITY,
362 max_drawdown: 0.0,
363 max_runup: 0.0,
364 max_drawdown_percent: 0.0,
365 max_runup_percent: 0.0,
366 max_contracts_all: 0.0,
367 max_contracts_long: 0.0,
368 max_contracts_short: 0.0,
369 })
370 }
371}
372
373pub struct Script<O: PineOutput> {
380 program: Program,
381 interpreter: Interpreter<O>,
382 timeframe: Timeframe,
385 bars: Vec<Bar>,
387 equity_curve: Vec<f64>,
389 last_close: f64,
391 equity_peak: f64,
393 equity_trough: f64,
394 max_drawdown: f64,
395 max_runup: f64,
396 max_drawdown_percent: f64,
399 max_runup_percent: f64,
400 max_contracts_all: f64,
402 max_contracts_long: f64,
403 max_contracts_short: f64,
404}
405
406impl<O: PineOutput> Script<O> {
407 pub fn execute(&mut self, bar: &Bar, last_bar: Option<&Bar>) -> Result<O, Error> {
410 use interpreter::Value;
411
412 self.interpreter.current_time = Some(bar.time);
413
414 for (name, value) in pine_builtins::per_bar_variables(bar, last_bar) {
415 if matches!(value, Value::Series(_)) {
416 self.interpreter.advance_series(&name, value);
417 } else {
418 self.interpreter.set_variable(&name, value);
419 }
420 }
421
422 self.advance_broker(bar);
426
427 let output = self.interpreter.execute(&self.program)?;
428
429 if let Some(broker) = self.interpreter.broker.as_ref() {
431 self.equity_curve.push(broker.equity(bar.close));
432 self.last_close = bar.close;
433 }
434
435 Ok(output)
436 }
437
438 fn advance_broker(&mut self, bar: &Bar) {
443 use interpreter::Value;
444
445 let close = bar.close;
446
447 let Some(broker) = self.interpreter.broker.as_mut() else {
449 return;
450 };
451 broker.advance(bar);
452
453 let position = broker.position();
454 let equity = broker.equity(close);
455 let initial = broker.initial_capital();
456 let equity_hi = broker.equity(bar.high);
459 let equity_lo = broker.equity(bar.low);
460 let intrabar_low = equity_hi.min(equity_lo);
461 let intrabar_high = equity_hi.max(equity_lo);
462 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
463 let closed_trades = broker.closed_trades().len() as i64;
464
465 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
466 let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
467 for trade in broker.closed_trades() {
468 let profit = trade.profit(close); if profit > 0.0 {
470 gross_profit += profit;
471 wins += 1;
472 } else if profit < 0.0 {
473 gross_loss -= profit; losses += 1;
475 } else {
476 evens += 1;
477 }
478 }
479
480 let (mut trade_pcts, mut win_pcts, mut loss_pcts) = (Vec::new(), Vec::new(), Vec::new());
484 for trade in broker.closed_trades() {
485 let profit = trade.profit(close);
486 let basis = trade.entry_price * trade.size.abs();
487 let ret = if basis != 0.0 {
488 profit / basis * 100.0
489 } else {
490 0.0
491 };
492 trade_pcts.push(ret);
493 if profit > 0.0 {
494 win_pcts.push(ret);
495 } else if profit < 0.0 {
496 loss_pcts.push(ret);
497 }
498 }
499 let position_entry_name = broker
500 .open_trades()
501 .last()
502 .map_or(Value::Na, |t| Value::String(t.entry_id.clone()));
503
504 if self.equity_peak == f64::NEG_INFINITY {
508 self.equity_peak = initial;
509 self.equity_trough = initial;
510 }
511 self.equity_peak = self.equity_peak.max(equity);
512 self.equity_trough = self.equity_trough.min(equity);
513 self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
514 self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
515 if self.equity_peak > 0.0 {
516 let dd = (self.equity_peak - intrabar_low) / self.equity_peak * 100.0;
517 self.max_drawdown_percent = self.max_drawdown_percent.max(dd);
518 }
519 if self.equity_trough > 0.0 {
520 let ru = (intrabar_high - self.equity_trough) / self.equity_trough * 100.0;
521 self.max_runup_percent = self.max_runup_percent.max(ru);
522 }
523 self.max_contracts_all = self.max_contracts_all.max(position.size.abs());
525 if position.size > 0.0 {
526 self.max_contracts_long = self.max_contracts_long.max(position.size);
527 } else if position.size < 0.0 {
528 self.max_contracts_short = self.max_contracts_short.max(-position.size);
529 }
530
531 let net_profit = equity - initial - open_profit;
534 let avg_price = if position.size == 0.0 {
536 Value::Na
537 } else {
538 Value::Number(position.avg_price)
539 };
540
541 let refreshed = [
542 ("position_size", Value::Number(position.size)),
543 ("position_avg_price", avg_price),
544 ("equity", Value::Number(equity)),
545 ("netprofit", Value::Number(net_profit)),
546 ("openprofit", Value::Number(open_profit)),
547 ("grossprofit", Value::Number(gross_profit)),
548 ("grossloss", Value::Number(gross_loss)),
549 ("max_drawdown", Value::Number(self.max_drawdown)),
550 ("max_runup", Value::Number(self.max_runup)),
551 ("wintrades", Value::Int(wins)),
554 ("losstrades", Value::Int(losses)),
555 ("eventrades", Value::Int(evens)),
556 ];
557 for (name, value) in refreshed {
558 self.interpreter.set_object_field("strategy", name, value);
559 }
560
561 let pct = |x: f64| {
564 if initial != 0.0 {
565 x / initial * 100.0
566 } else {
567 0.0
568 }
569 };
570 let per_trade = |total: f64, count: i64| {
571 if count > 0 {
572 Value::Number(total / count as f64)
573 } else {
574 Value::Na
575 }
576 };
577 let mean = |v: &[f64]| {
578 if v.is_empty() {
579 Value::Na
580 } else {
581 Value::Number(v.iter().sum::<f64>() / v.len() as f64)
582 }
583 };
584 let derived = [
585 ("netprofit_percent", Value::Number(pct(net_profit))),
586 ("openprofit_percent", Value::Number(pct(open_profit))),
587 ("grossprofit_percent", Value::Number(pct(gross_profit))),
588 ("grossloss_percent", Value::Number(pct(gross_loss))),
589 (
590 "max_drawdown_percent",
591 Value::Number(self.max_drawdown_percent),
592 ),
593 ("max_runup_percent", Value::Number(self.max_runup_percent)),
594 (
595 "max_contracts_held_all",
596 Value::Number(self.max_contracts_all),
597 ),
598 (
599 "max_contracts_held_long",
600 Value::Number(self.max_contracts_long),
601 ),
602 (
603 "max_contracts_held_short",
604 Value::Number(self.max_contracts_short),
605 ),
606 ("avg_trade", per_trade(net_profit, closed_trades)),
607 ("avg_winning_trade", per_trade(gross_profit, wins)),
608 ("avg_losing_trade", per_trade(-gross_loss, losses)),
611 ("avg_trade_percent", mean(&trade_pcts)),
612 ("avg_winning_trade_percent", mean(&win_pcts)),
613 ("avg_losing_trade_percent", mean(&loss_pcts)),
614 ("position_entry_name", position_entry_name),
615 ];
616 for (name, value) in derived {
617 self.interpreter.set_object_field("strategy", name, value);
618 }
619 }
620
621 pub fn run(mut self) -> Result<Run<O>, Error> {
624 let bars = std::mem::take(&mut self.bars);
625 let last_bar = bars.last().cloned();
626 let outputs = bars
627 .iter()
628 .map(|bar| self.execute(bar, last_bar.as_ref()))
629 .collect::<Result<Vec<O>, Error>>()?;
630 let backtest = self.take_backtest();
631 Ok(Run { outputs, backtest })
632 }
633
634 fn take_backtest(&mut self) -> Option<Backtest> {
635 let broker = self.interpreter.broker.as_ref()?;
636 let close = self.last_close;
637
638 let mut trades: Vec<_> = broker.closed_trades().to_vec();
640 trades.extend(broker.open_trades().into_iter().cloned());
641
642 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
643 let initial_capital = broker.initial_capital();
644 let position_size = broker.position().size;
645
646 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
647 let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
648 for trade in broker.closed_trades() {
649 let profit = trade.profit(close);
650 if profit > 0.0 {
651 gross_profit += profit;
652 win_trades += 1;
653 } else if profit < 0.0 {
654 gross_loss -= profit;
655 loss_trades += 1;
656 } else {
657 even_trades += 1;
658 }
659 }
660
661 let equity = std::mem::take(&mut self.equity_curve);
662 let final_equity = equity.last().copied().unwrap_or(initial_capital);
663
664 Some(Backtest {
665 initial_capital,
666 net_profit: final_equity - initial_capital - open_profit,
667 open_profit,
668 gross_profit,
669 gross_loss,
670 max_drawdown: self.max_drawdown,
671 max_runup: self.max_runup,
672 win_trades,
673 loss_trades,
674 even_trades,
675 position_size,
676 mark_price: close,
677 equity,
678 trades,
679 halted: broker.halted_bar(),
680 timeframe: self.timeframe.clone(),
681 })
682 }
683}
684
685pub fn execute(source: &str, data: Data) -> Result<(), Error> {
686 ScriptBuilder::<DefaultPineOutput>::with_code(source)
687 .with_data(data)
688 .compile()?
689 .run()
690 .map(|_| ())
691}
692
693#[cfg(test)]
694mod tests {
695 use super::inputs_from_json;
696 use pine_core::InputValue;
697
698 #[test]
699 fn decodes_input_overrides_from_json() {
700 let map = inputs_from_json(r#"{"Length": 20, "Ratio": 1.5, "On": true, "Mode": "fast"}"#)
701 .unwrap();
702 assert_eq!(map["Length"], InputValue::Int(20));
703 assert_eq!(map["Ratio"], InputValue::Float(1.5));
704 assert_eq!(map["On"], InputValue::Bool(true));
705 assert_eq!(map["Mode"], InputValue::Str("fast".to_string()));
706 }
707}