Skip to main content

pine_lang/
lib.rs

1// Re-export all public types from sub-crates
2pub 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/// Error type for Pine operations
32#[derive(Debug)]
33pub enum Error {
34    Lexer(LexerError),
35    Parser(ParserError),
36    Runtime(RuntimeError),
37    /// Semantic analysis failed; the program is invalid. Carries every
38    /// diagnostic found.
39    Sema(Vec<Diagnostic>),
40    /// The script's `//@version=N` annotation names a version this toolchain
41    /// cannot compile.
42    Version(VersionError),
43    /// No bars to run over: neither data nor a provider was given, or the
44    /// provider could not produce the requested feed.
45    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            // One diagnostic per line, so multiple errors are simply appended.
57            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    /// Host-supplied variables the script can reference, registered as consts
122    /// alongside the builtin namespaces.
123    pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
124        self.custom_variables = variables;
125        self
126    }
127
128    /// Resolves `import` statements. Without one, importing a library fails.
129    pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
130        self.library_loader = Some(loader);
131        self
132    }
133
134    /// Supplies bars for `request.security`. Without one, `request.security`
135    /// returns na.
136    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    /// The chart timeframe exposed to the script as `timeframe.*`. Without one,
147    /// the namespace is populated with defaults.
148    pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
149        self.timeframe = timeframe;
150        self
151    }
152
153    /// Run over only the last `bar_count` bars of the feed. Without one, the
154    /// whole feed is used.
155    pub fn with_bar_count(mut self, bar_count: usize) -> Self {
156        self.bar_count = Some(bar_count);
157        self
158    }
159
160    /// The market to run over: the bars, and the symbol and timeframe they
161    /// belong to.
162    ///
163    /// The data describes itself, so it fills in `syminfo.*` and `timeframe.*`
164    /// too. An explicit [`ScriptBuilder::with_syminfo`] or
165    /// [`ScriptBuilder::with_timeframe`] still wins, whichever order they are
166    /// called in.
167    pub fn with_data(mut self, data: Data) -> Self {
168        self.data = Some(data);
169        self
170    }
171
172    /// Compile PineScript source code into a Script with default output
173    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        // Keep only the last `bar_count` bars when the caller limited the run.
206        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        // The chart's bar spacing, so `request.security_lower_tf` can reject a
213        // request that is not actually lower than the chart timeframe.
214        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        // The names sema accepts without a user declaration: the registered
233        // namespaces, the per-bar variables `execute` sets (barstate + OHLCV),
234        // and any host-supplied custom variables.
235        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        // Semantic pre-check: reject if sema produces errors.
258        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        // Create interpreter and load builtin namespace objects
268        let mut interpreter = Interpreter::new();
269        if let Some(library_loader) = self.library_loader {
270            interpreter.set_library_loader(library_loader);
271        }
272        // The feed `request.security` draws from, reached through `ctx`.
273        interpreter.request_provider = self.request_provider.map(Rc::from);
274        interpreter.chart_period = chart_period;
275
276        // Register namespace objects as const variables
277        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
299/// A compiled PineScript program, and the bars it will run over.
300///
301/// State accumulates across bars — series history, `var` locals, and every
302/// stateful builtin's window — exactly as it does in TradingView. That makes a
303/// `Script` single-use: [`Script::run`] takes it by value so a second run
304/// cannot inherit the first one's state.
305pub struct Script<O: PineOutput> {
306    program: Program,
307    interpreter: Interpreter<O>,
308    /// Bars from the builder's source; empty when none was given.
309    bars: Vec<Bar>,
310    /// Account value at each bar's close, accumulated while a `strategy` runs.
311    equity_curve: Vec<f64>,
312    /// The last bar's close, used to mark open trades at the run's end.
313    last_close: f64,
314    /// Running equity extremes for `strategy.max_drawdown`/`max_runup`.
315    equity_peak: f64,
316    equity_trough: f64,
317    max_drawdown: f64,
318    max_runup: f64,
319}
320
321impl<O: PineOutput> Script<O> {
322    /// Run one bar. Private: bars must be replayed in order from the first, so
323    /// [`Script::run`] is the only way in.
324    fn execute(&mut self, bar: &Bar) -> Result<O, Error> {
325        // Load bar data as Series variables so TA functions can access historical data
326        use interpreter::{Series, Value};
327
328        // The series id is also the key the historical provider is queried with.
329        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        // Per-bar namespaces (barstate) are rebuilt from this bar's flags.
353        for (name, value) in pine_builtins::register_per_bar(bar) {
354            self.interpreter.set_variable(&name, value);
355        }
356
357        // Fill orders left pending by the previous bar before the body runs, so
358        // it reads the position and equity they produced. A no-op unless the
359        // script declared a `strategy`.
360        self.advance_broker(bar);
361
362        let output = self.interpreter.execute(&self.program)?;
363
364        // Read after the body so the bar a `strategy` is declared on is counted.
365        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    /// Advance the simulated broker one bar and refresh the read-only
374    /// `strategy.*` values from it. The interpreter only holds the broker
375    /// handle; the backtest accounting that maps it onto script variables lives
376    /// here, in the host.
377    fn advance_broker(&mut self, bar: &Bar) {
378        use interpreter::Value;
379
380        let close = bar.close;
381
382        // Read from the broker, then drop the borrow to update `self`'s state.
383        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        // Equity is monotonic in price, so its intrabar extremes are the marks
392        // at the bar's high and low — lower is adverse, higher favourable.
393        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); // closed, so the price is ignored
405            if profit > 0.0 {
406                gross_profit += profit;
407                wins += 1;
408            } else if profit < 0.0 {
409                gross_loss -= profit; // positive magnitude, as Pine reports it
410                losses += 1;
411            } else {
412                evens += 1;
413            }
414        }
415
416        // Drawdown/run-up measure the intrabar extreme against a peak/trough
417        // that tracks close equity — an intrabar swing does not move the mark.
418        // Seed with the starting capital (the declaration bar's equity).
419        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        // Pine's identity equity = initial + netprofit + openprofit; derive
429        // netprofit from it so commission can't make the two drift.
430        let net_profit = equity - initial - open_profit;
431        // na, not 0, when flat — matching Pine.
432        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    /// Replay the script over every bar from its source, returning what each
460    /// one produced.
461    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        // Closed trades first, then those still open.
476        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}