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 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 struct Analysis {
102 pub diagnostics: Vec<Diagnostic>,
103 pub symbols: sema::SymbolTable,
104}
105
106pub fn analyze(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Analysis, Error> {
108 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
109 let tokens = Lexer::with_version(source, version).tokenize()?;
110 let program = Parser::new(tokens).parse_program()?;
111
112 let (mut env, _): (HashMap<String, Value<DefaultPineOutput>>, _) =
113 pine_builtins::register_namespace_objects(version, None, None);
114 for (name, value) in pine_builtins::per_bar_variables(&Bar::default(), None) {
115 env.insert(name, value);
116 }
117
118 let (mut diagnostics, symbols) = pine_sema::analyze_with_symbols(&program, &env, loader);
119 diagnostics.extend(pine_lint::lint(&program));
120 diagnostics.sort_by_key(|d| d.pos.unwrap_or((u32::MAX, u32::MAX)));
121 Ok(Analysis {
122 diagnostics,
123 symbols,
124 })
125}
126
127pub fn check(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Vec<Diagnostic>, Error> {
129 Ok(analyze(source, loader)?.diagnostics)
130}
131
132pub fn lint_source(source: &str) -> Result<Vec<Diagnostic>, Error> {
135 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
136 let tokens = Lexer::with_version(source, version).tokenize()?;
137 let program = Parser::new(tokens).parse_program()?;
138 Ok(pine_lint::lint(&program))
139}
140
141pub fn inputs_from_json(
145 json: &str,
146) -> Result<HashMap<String, pine_core::InputValue>, serde_json::Error> {
147 serde_json::from_str(json)
148}
149
150pub struct ScriptBuilder<O: PineOutput> {
151 source: String,
152 custom_variables: HashMap<String, Value<O>>,
153 inputs: HashMap<String, pine_core::InputValue>,
154 library_loader: Option<Box<dyn LibraryLoader>>,
155 request_provider: Option<Box<dyn DataProvider>>,
156 ticker: Option<String>,
157 timeframe: Timeframe,
158 data: Option<Data>,
159 bar_count: Option<usize>,
160 broker_factory: Option<Box<dyn pine_broker::BrokerFactory>>,
161}
162
163impl<O: PineOutput> ScriptBuilder<O> {
164 pub fn with_code(source: &str) -> ScriptBuilder<O> {
165 Self {
166 source: source.to_string(),
167 custom_variables: HashMap::new(),
168 inputs: HashMap::new(),
169 library_loader: None,
170 request_provider: None,
171 ticker: None,
172 timeframe: Timeframe::default(),
173 data: None,
174 bar_count: None,
175 broker_factory: None,
176 }
177 }
178
179 pub fn with_inputs(mut self, inputs: HashMap<String, pine_core::InputValue>) -> Self {
183 self.inputs = inputs;
184 self
185 }
186
187 pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
190 self.custom_variables = variables;
191 self
192 }
193
194 pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
196 self.library_loader = Some(loader);
197 self
198 }
199
200 pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
203 self.request_provider = Some(provider);
204 self
205 }
206
207 pub fn with_broker(mut self, factory: Box<dyn pine_broker::BrokerFactory>) -> Self {
210 self.broker_factory = Some(factory);
211 self
212 }
213
214 pub fn with_ticker(mut self, ticker: String) -> Self {
215 self.ticker = Some(ticker);
216 self
217 }
218
219 pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
222 self.timeframe = timeframe;
223 self
224 }
225
226 pub fn with_bar_count(mut self, bar_count: usize) -> Self {
229 self.bar_count = Some(bar_count);
230 self
231 }
232
233 pub fn with_data(mut self, data: Data) -> Self {
241 self.data = Some(data);
242 self
243 }
244
245 pub fn compile(self) -> Result<Script<O>, Error>
247 where
248 O: LogOutput
249 + PlotOutput
250 + LabelOutput
251 + BoxOutput
252 + InputOutput
253 + LineOutput
254 + TableOutput
255 + MetadataOutput
256 + GlobalOutput
257 + AlertConditionOutput
258 + FillOutput
259 + DrawingOutput,
260 {
261 let data = match self.data {
262 Some(data) => data,
263 None => {
264 let provider = self
265 .request_provider
266 .as_ref()
267 .ok_or_else(|| Error::Data("no data or request provider set".into()))?;
268
269 let ticker = self.ticker.clone().unwrap_or_default();
270 provider
271 .request(&ticker, self.timeframe.clone())
272 .map_err(Error::Data)?
273 }
274 };
275
276 let syminfo = data.syminfo;
277 let timeframe = self.timeframe;
278
279 let mut bars = data.bars;
281 if let Some(n) = self.bar_count {
282 let len = bars.len();
283 bars = bars.split_off(len.saturating_sub(n.max(1)));
284 }
285
286 let chart_period = bars
289 .windows(2)
290 .next()
291 .map(|pair| pair[1].time - pair[0].time);
292
293 let source = self.source.as_str();
294 let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
295
296 let mut lexer = Lexer::with_version(source, version);
297 let tokens = lexer.tokenize()?;
298
299 let mut parser = Parser::new(tokens);
300 let statements = parser.parse()?;
301 let program = Program::new(statements);
302
303 let (mut consts, advances) = pine_builtins::register_namespace_objects(
306 version,
307 Some(syminfo),
308 Some(timeframe.clone()),
309 );
310 for (name, value) in self.custom_variables {
311 consts.insert(name, value);
312 }
313
314 let mut builtins = consts.clone();
315 for (name, value) in pine_builtins::per_bar_variables(&Bar::default(), None) {
316 builtins.insert(name, value);
317 }
318
319 let errors: Vec<_> =
321 pine_sema::analyze(&program, &builtins, self.library_loader.as_deref())
322 .into_iter()
323 .filter(|diagnostic| diagnostic.severity == pine_diagnostics::Severity::Error)
324 .collect();
325 if !errors.is_empty() {
326 return Err(Error::Sema(errors));
327 }
328
329 let mut interpreter = Interpreter::new();
331 interpreter.library_loader = self.library_loader;
332 interpreter.request_provider = self.request_provider.map(Rc::from);
333 interpreter.chart_period = chart_period;
334 if let Some(broker_factory) = self.broker_factory {
335 interpreter.broker_factory = Some(broker_factory);
336 }
337 interpreter.set_const_variables(consts);
338 interpreter.per_bar_advances = advances;
339 interpreter.inputs = self.inputs;
340
341 Ok(Script {
342 program,
343 interpreter,
344 timeframe,
345 bars,
346 equity_curve: Vec::new(),
347 last_close: 0.0,
348 equity_peak: f64::NEG_INFINITY,
349 equity_trough: f64::INFINITY,
350 max_drawdown: 0.0,
351 max_runup: 0.0,
352 max_drawdown_percent: 0.0,
353 max_runup_percent: 0.0,
354 max_contracts_all: 0.0,
355 max_contracts_long: 0.0,
356 max_contracts_short: 0.0,
357 })
358 }
359}
360
361pub struct Script<O: PineOutput> {
368 program: Program,
369 interpreter: Interpreter<O>,
370 timeframe: Timeframe,
373 bars: Vec<Bar>,
375 equity_curve: Vec<f64>,
377 last_close: f64,
379 equity_peak: f64,
381 equity_trough: f64,
382 max_drawdown: f64,
383 max_runup: f64,
384 max_drawdown_percent: f64,
387 max_runup_percent: f64,
388 max_contracts_all: f64,
390 max_contracts_long: f64,
391 max_contracts_short: f64,
392}
393
394impl<O: PineOutput> Script<O> {
395 pub fn execute(&mut self, bar: &Bar, last_bar: Option<&Bar>) -> Result<O, Error> {
398 use interpreter::Value;
399
400 self.interpreter.current_time = Some(bar.time);
401
402 for (name, value) in pine_builtins::per_bar_variables(bar, last_bar) {
403 if matches!(value, Value::Series(_)) {
404 self.interpreter.advance_series(&name, value);
405 } else {
406 self.interpreter.set_variable(&name, value);
407 }
408 }
409
410 self.advance_broker(bar);
414
415 let output = self.interpreter.execute(&self.program)?;
416
417 if let Some(broker) = self.interpreter.broker.as_ref() {
419 self.equity_curve.push(broker.equity(bar.close));
420 self.last_close = bar.close;
421 }
422
423 Ok(output)
424 }
425
426 fn advance_broker(&mut self, bar: &Bar) {
431 use interpreter::Value;
432
433 let close = bar.close;
434
435 let Some(broker) = self.interpreter.broker.as_mut() else {
437 return;
438 };
439 broker.advance(bar);
440
441 let position = broker.position();
442 let equity = broker.equity(close);
443 let initial = broker.initial_capital();
444 let equity_hi = broker.equity(bar.high);
447 let equity_lo = broker.equity(bar.low);
448 let intrabar_low = equity_hi.min(equity_lo);
449 let intrabar_high = equity_hi.max(equity_lo);
450 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
451 let closed_trades = broker.closed_trades().len() as i64;
452
453 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
454 let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
455 for trade in broker.closed_trades() {
456 let profit = trade.profit(close); if profit > 0.0 {
458 gross_profit += profit;
459 wins += 1;
460 } else if profit < 0.0 {
461 gross_loss -= profit; losses += 1;
463 } else {
464 evens += 1;
465 }
466 }
467
468 let (mut trade_pcts, mut win_pcts, mut loss_pcts) = (Vec::new(), Vec::new(), Vec::new());
472 for trade in broker.closed_trades() {
473 let profit = trade.profit(close);
474 let basis = trade.entry_price * trade.size.abs();
475 let ret = if basis != 0.0 {
476 profit / basis * 100.0
477 } else {
478 0.0
479 };
480 trade_pcts.push(ret);
481 if profit > 0.0 {
482 win_pcts.push(ret);
483 } else if profit < 0.0 {
484 loss_pcts.push(ret);
485 }
486 }
487 let position_entry_name = broker
488 .open_trades()
489 .last()
490 .map_or(Value::Na, |t| Value::String(t.entry_id.clone()));
491
492 if self.equity_peak == f64::NEG_INFINITY {
496 self.equity_peak = initial;
497 self.equity_trough = initial;
498 }
499 self.equity_peak = self.equity_peak.max(equity);
500 self.equity_trough = self.equity_trough.min(equity);
501 self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
502 self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
503 if self.equity_peak > 0.0 {
504 let dd = (self.equity_peak - intrabar_low) / self.equity_peak * 100.0;
505 self.max_drawdown_percent = self.max_drawdown_percent.max(dd);
506 }
507 if self.equity_trough > 0.0 {
508 let ru = (intrabar_high - self.equity_trough) / self.equity_trough * 100.0;
509 self.max_runup_percent = self.max_runup_percent.max(ru);
510 }
511 self.max_contracts_all = self.max_contracts_all.max(position.size.abs());
513 if position.size > 0.0 {
514 self.max_contracts_long = self.max_contracts_long.max(position.size);
515 } else if position.size < 0.0 {
516 self.max_contracts_short = self.max_contracts_short.max(-position.size);
517 }
518
519 let net_profit = equity - initial - open_profit;
522 let avg_price = if position.size == 0.0 {
524 Value::Na
525 } else {
526 Value::Number(position.avg_price)
527 };
528
529 let refreshed = [
530 ("position_size", Value::Number(position.size)),
531 ("position_avg_price", avg_price),
532 ("equity", Value::Number(equity)),
533 ("netprofit", Value::Number(net_profit)),
534 ("openprofit", Value::Number(open_profit)),
535 ("grossprofit", Value::Number(gross_profit)),
536 ("grossloss", Value::Number(gross_loss)),
537 ("max_drawdown", Value::Number(self.max_drawdown)),
538 ("max_runup", Value::Number(self.max_runup)),
539 ("wintrades", Value::Int(wins)),
542 ("losstrades", Value::Int(losses)),
543 ("eventrades", Value::Int(evens)),
544 ];
545 for (name, value) in refreshed {
546 self.interpreter.set_object_field("strategy", name, value);
547 }
548
549 let pct = |x: f64| {
552 if initial != 0.0 {
553 x / initial * 100.0
554 } else {
555 0.0
556 }
557 };
558 let per_trade = |total: f64, count: i64| {
559 if count > 0 {
560 Value::Number(total / count as f64)
561 } else {
562 Value::Na
563 }
564 };
565 let mean = |v: &[f64]| {
566 if v.is_empty() {
567 Value::Na
568 } else {
569 Value::Number(v.iter().sum::<f64>() / v.len() as f64)
570 }
571 };
572 let derived = [
573 ("netprofit_percent", Value::Number(pct(net_profit))),
574 ("openprofit_percent", Value::Number(pct(open_profit))),
575 ("grossprofit_percent", Value::Number(pct(gross_profit))),
576 ("grossloss_percent", Value::Number(pct(gross_loss))),
577 (
578 "max_drawdown_percent",
579 Value::Number(self.max_drawdown_percent),
580 ),
581 ("max_runup_percent", Value::Number(self.max_runup_percent)),
582 (
583 "max_contracts_held_all",
584 Value::Number(self.max_contracts_all),
585 ),
586 (
587 "max_contracts_held_long",
588 Value::Number(self.max_contracts_long),
589 ),
590 (
591 "max_contracts_held_short",
592 Value::Number(self.max_contracts_short),
593 ),
594 ("avg_trade", per_trade(net_profit, closed_trades)),
595 ("avg_winning_trade", per_trade(gross_profit, wins)),
596 ("avg_losing_trade", per_trade(-gross_loss, losses)),
599 ("avg_trade_percent", mean(&trade_pcts)),
600 ("avg_winning_trade_percent", mean(&win_pcts)),
601 ("avg_losing_trade_percent", mean(&loss_pcts)),
602 ("position_entry_name", position_entry_name),
603 ];
604 for (name, value) in derived {
605 self.interpreter.set_object_field("strategy", name, value);
606 }
607 }
608
609 pub fn run(mut self) -> Result<Run<O>, Error> {
612 let bars = std::mem::take(&mut self.bars);
613 let last_bar = bars.last().cloned();
614 let outputs = bars
615 .iter()
616 .map(|bar| self.execute(bar, last_bar.as_ref()))
617 .collect::<Result<Vec<O>, Error>>()?;
618 let backtest = self.take_backtest();
619 Ok(Run { outputs, backtest })
620 }
621
622 fn take_backtest(&mut self) -> Option<Backtest> {
623 let broker = self.interpreter.broker.as_ref()?;
624 let close = self.last_close;
625
626 let mut trades: Vec<_> = broker.closed_trades().to_vec();
628 trades.extend(broker.open_trades().into_iter().cloned());
629
630 let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
631 let initial_capital = broker.initial_capital();
632 let position_size = broker.position().size;
633
634 let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
635 let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
636 for trade in broker.closed_trades() {
637 let profit = trade.profit(close);
638 if profit > 0.0 {
639 gross_profit += profit;
640 win_trades += 1;
641 } else if profit < 0.0 {
642 gross_loss -= profit;
643 loss_trades += 1;
644 } else {
645 even_trades += 1;
646 }
647 }
648
649 let equity = std::mem::take(&mut self.equity_curve);
650 let final_equity = equity.last().copied().unwrap_or(initial_capital);
651
652 Some(Backtest {
653 initial_capital,
654 net_profit: final_equity - initial_capital - open_profit,
655 open_profit,
656 gross_profit,
657 gross_loss,
658 max_drawdown: self.max_drawdown,
659 max_runup: self.max_runup,
660 win_trades,
661 loss_trades,
662 even_trades,
663 position_size,
664 mark_price: close,
665 equity,
666 trades,
667 halted: broker.halted_bar(),
668 timeframe: self.timeframe.clone(),
669 })
670 }
671}
672
673pub fn execute(source: &str, data: Data) -> Result<(), Error> {
674 ScriptBuilder::<DefaultPineOutput>::with_code(source)
675 .with_data(data)
676 .compile()?
677 .run()
678 .map(|_| ())
679}
680
681#[cfg(test)]
682mod tests {
683 use super::inputs_from_json;
684 use pine_core::InputValue;
685
686 #[test]
687 fn decodes_input_overrides_from_json() {
688 let map = inputs_from_json(r#"{"Length": 20, "Ratio": 1.5, "On": true, "Mode": "fast"}"#)
689 .unwrap();
690 assert_eq!(map["Length"], InputValue::Int(20));
691 assert_eq!(map["Ratio"], InputValue::Float(1.5));
692 assert_eq!(map["On"], InputValue::Bool(true));
693 assert_eq!(map["Mode"], InputValue::Str("fast".to_string()));
694 }
695}