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_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;
20pub use pine_core::{DataProvider, DirLoader, FileResolver, LibraryLoader};
21pub use run::{Run, RunResult};
22
23use pine_ast::Program;
24use pine_core::{
25    AlertConditionOutput, BoxOutput, FillOutput, GlobalOutput, InputOutput, LabelOutput,
26    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/// Error type for Pine operations
37#[derive(Debug)]
38pub enum Error {
39    Lexer(LexerError),
40    Parser(ParserError),
41    Runtime(RuntimeError),
42    /// Semantic analysis failed; the program is invalid. Carries every
43    /// diagnostic found.
44    Sema(Vec<Diagnostic>),
45    /// The script's `//@version=N` annotation names a version this toolchain
46    /// cannot compile.
47    Version(VersionError),
48    /// No bars to run over: neither data nor a provider was given, or the
49    /// provider could not produce the requested feed.
50    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            // One diagnostic per line, so multiple errors are simply appended.
62            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
101/// Parse and semantically analyze `source` without bars or execution, returning
102/// every diagnostic.
103pub fn check(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Vec<Diagnostic>, Error> {
104    let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
105    let tokens = Lexer::with_version(source, version).tokenize()?;
106    let program = Program::new(Parser::new(tokens).parse()?);
107
108    let mut env: HashMap<String, Value<DefaultPineOutput>> =
109        pine_builtins::register_namespace_objects(version, None, None);
110    for (name, value) in pine_builtins::per_bar_variables(&Bar::default()) {
111        env.insert(name, value);
112    }
113
114    Ok(pine_sema::analyze(&program, &env, loader))
115}
116
117pub struct ScriptBuilder<O: PineOutput> {
118    source: String,
119    custom_variables: HashMap<String, Value<O>>,
120    library_loader: Option<Box<dyn LibraryLoader>>,
121    request_provider: Option<Box<dyn DataProvider>>,
122    ticker: Option<String>,
123    timeframe: Timeframe,
124    data: Option<Data>,
125    bar_count: Option<usize>,
126    broker_factory: Option<Box<dyn pine_broker::BrokerFactory>>,
127}
128
129impl<O: PineOutput> ScriptBuilder<O> {
130    pub fn with_code(source: &str) -> ScriptBuilder<O> {
131        Self {
132            source: source.to_string(),
133            custom_variables: HashMap::new(),
134            library_loader: None,
135            request_provider: None,
136            ticker: None,
137            timeframe: Timeframe::default(),
138            data: None,
139            bar_count: None,
140            broker_factory: None,
141        }
142    }
143
144    /// Host-supplied variables the script can reference, registered as consts
145    /// alongside the builtin namespaces.
146    pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
147        self.custom_variables = variables;
148        self
149    }
150
151    /// Resolves `import` statements. Without one, importing a library fails.
152    pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
153        self.library_loader = Some(loader);
154        self
155    }
156
157    /// Supplies bars for `request.security`. Without one, `request.security`
158    /// returns na.
159    pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
160        self.request_provider = Some(provider);
161        self
162    }
163
164    /// Swaps the broker a `strategy` trades against. Without one, the built-in
165    /// [`DefaultBrokerFactory`](pine_broker::DefaultBrokerFactory) is used.
166    pub fn with_broker(mut self, factory: Box<dyn pine_broker::BrokerFactory>) -> Self {
167        self.broker_factory = Some(factory);
168        self
169    }
170
171    pub fn with_ticker(mut self, ticker: String) -> Self {
172        self.ticker = Some(ticker);
173        self
174    }
175
176    /// The chart timeframe exposed to the script as `timeframe.*`. Without one,
177    /// the namespace is populated with defaults.
178    pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
179        self.timeframe = timeframe;
180        self
181    }
182
183    /// Run over only the last `bar_count` bars of the feed. Without one, the
184    /// whole feed is used.
185    pub fn with_bar_count(mut self, bar_count: usize) -> Self {
186        self.bar_count = Some(bar_count);
187        self
188    }
189
190    /// The market to run over: the bars, and the symbol and timeframe they
191    /// belong to.
192    ///
193    /// The data describes itself, so it fills in `syminfo.*` and `timeframe.*`
194    /// too. An explicit [`ScriptBuilder::with_syminfo`] or
195    /// [`ScriptBuilder::with_timeframe`] still wins, whichever order they are
196    /// called in.
197    pub fn with_data(mut self, data: Data) -> Self {
198        self.data = Some(data);
199        self
200    }
201
202    /// Compile PineScript source code into a Script with default output
203    pub fn compile(self) -> Result<Script<O>, Error>
204    where
205        O: LogOutput
206            + PlotOutput
207            + LabelOutput
208            + BoxOutput
209            + InputOutput
210            + LineOutput
211            + TableOutput
212            + MetadataOutput
213            + GlobalOutput
214            + AlertConditionOutput
215            + FillOutput,
216    {
217        let data = match self.data {
218            Some(data) => data,
219            None => {
220                let provider = self
221                    .request_provider
222                    .as_ref()
223                    .ok_or_else(|| Error::Data("no data or request provider set".into()))?;
224
225                let ticker = self.ticker.clone().unwrap_or_default();
226                provider
227                    .request(&ticker, self.timeframe.clone())
228                    .map_err(Error::Data)?
229            }
230        };
231
232        let syminfo = data.syminfo;
233        let timeframe = self.timeframe;
234
235        // Keep only the last `bar_count` bars when the caller limited the run.
236        let mut bars = data.bars;
237        if let Some(n) = self.bar_count {
238            let len = bars.len();
239            bars = bars.split_off(len.saturating_sub(n.max(1)));
240        }
241
242        // The chart's bar spacing, so `request.security_lower_tf` can reject a
243        // request that is not actually lower than the chart timeframe.
244        let chart_period = bars
245            .windows(2)
246            .next()
247            .map(|pair| pair[1].time - pair[0].time);
248
249        let source = self.source.as_str();
250        let version = PineVersion::detect(source)?.unwrap_or(PineVersion::LATEST);
251
252        let mut lexer = Lexer::with_version(source, version);
253        let tokens = lexer.tokenize()?;
254
255        let mut parser = Parser::new(tokens);
256        let statements = parser.parse()?;
257        let program = Program::new(statements);
258
259        // The interpreter's const environment: the registered namespaces plus any
260        // host-supplied globals. Built once and handed over as-is.
261        let mut consts =
262            pine_builtins::register_namespace_objects(version, Some(syminfo), Some(timeframe));
263        for (name, value) in self.custom_variables {
264            consts.insert(name, value);
265        }
266
267        let mut builtins = consts.clone();
268        for (name, value) in pine_builtins::per_bar_variables(&Bar::default()) {
269            builtins.insert(name, value);
270        }
271
272        // Semantic pre-check: reject if sema produces errors.
273        let errors: Vec<_> =
274            pine_sema::analyze(&program, &builtins, self.library_loader.as_deref())
275                .into_iter()
276                .filter(|diagnostic| diagnostic.severity == pine_diagnostics::Severity::Error)
277                .collect();
278        if !errors.is_empty() {
279            return Err(Error::Sema(errors));
280        }
281
282        // Create interpreter and load builtin namespace objects
283        let mut interpreter = Interpreter::new();
284        interpreter.library_loader = self.library_loader;
285        interpreter.request_provider = self.request_provider.map(Rc::from);
286        interpreter.chart_period = chart_period;
287        if let Some(broker_factory) = self.broker_factory {
288            interpreter.broker_factory = Some(broker_factory);
289        }
290        interpreter.set_const_variables(consts);
291
292        Ok(Script {
293            program,
294            interpreter,
295            bars,
296            equity_curve: Vec::new(),
297            last_close: 0.0,
298            equity_peak: f64::NEG_INFINITY,
299            equity_trough: f64::INFINITY,
300            max_drawdown: 0.0,
301            max_runup: 0.0,
302        })
303    }
304}
305
306/// A compiled PineScript program, and the bars it will run over.
307///
308/// State accumulates across bars — series history, `var` locals, and every
309/// stateful builtin's window — exactly as it does in TradingView. That makes a
310/// `Script` single-use: [`Script::run`] takes it by value so a second run
311/// cannot inherit the first one's state.
312pub struct Script<O: PineOutput> {
313    program: Program,
314    interpreter: Interpreter<O>,
315    /// Bars from the builder's source; empty when none was given.
316    bars: Vec<Bar>,
317    /// Account value at each bar's close, accumulated while a `strategy` runs.
318    equity_curve: Vec<f64>,
319    /// The last bar's close, used to mark open trades at the run's end.
320    last_close: f64,
321    /// Running equity extremes for `strategy.max_drawdown`/`max_runup`.
322    equity_peak: f64,
323    equity_trough: f64,
324    max_drawdown: f64,
325    max_runup: f64,
326}
327
328impl<O: PineOutput> Script<O> {
329    /// Run one bar. Private: bars must be replayed in order from the first, so
330    /// [`Script::run`] is the only way in.
331    pub fn execute(&mut self, bar: &Bar) -> Result<O, Error> {
332        use interpreter::Value;
333
334        for (name, value) in pine_builtins::per_bar_variables(bar) {
335            if matches!(value, Value::Series(_)) {
336                self.interpreter.advance_series(&name, value);
337            } else {
338                self.interpreter.set_variable(&name, value);
339            }
340        }
341
342        // Fill orders left pending by the previous bar before the body runs, so
343        // it reads the position and equity they produced. A no-op unless the
344        // script declared a `strategy`.
345        self.advance_broker(bar);
346
347        let output = self.interpreter.execute(&self.program)?;
348
349        // Read after the body so the bar a `strategy` is declared on is counted.
350        if let Some(broker) = self.interpreter.broker.as_ref() {
351            self.equity_curve.push(broker.equity(bar.close));
352            self.last_close = bar.close;
353        }
354
355        Ok(output)
356    }
357
358    /// Advance the simulated broker one bar and refresh the read-only
359    /// `strategy.*` values from it. The interpreter only holds the broker
360    /// handle; the backtest accounting that maps it onto script variables lives
361    /// here, in the host.
362    fn advance_broker(&mut self, bar: &Bar) {
363        use interpreter::Value;
364
365        let close = bar.close;
366
367        // Read from the broker, then drop the borrow to update `self`'s state.
368        let Some(broker) = self.interpreter.broker.as_mut() else {
369            return;
370        };
371        broker.advance(bar);
372
373        let position = broker.position();
374        let equity = broker.equity(close);
375        let initial = broker.initial_capital();
376        // Equity is monotonic in price, so its intrabar extremes are the marks
377        // at the bar's high and low — lower is adverse, higher favourable.
378        let equity_hi = broker.equity(bar.high);
379        let equity_lo = broker.equity(bar.low);
380        let intrabar_low = equity_hi.min(equity_lo);
381        let intrabar_high = equity_hi.max(equity_lo);
382        let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
383        let open_trades = broker.open_trades().len() as i64;
384        let closed_trades = broker.closed_trades().len() as i64;
385
386        let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
387        let (mut wins, mut losses, mut evens) = (0i64, 0i64, 0i64);
388        for trade in broker.closed_trades() {
389            let profit = trade.profit(close); // closed, so the price is ignored
390            if profit > 0.0 {
391                gross_profit += profit;
392                wins += 1;
393            } else if profit < 0.0 {
394                gross_loss -= profit; // positive magnitude, as Pine reports it
395                losses += 1;
396            } else {
397                evens += 1;
398            }
399        }
400
401        // Drawdown/run-up measure the intrabar extreme against a peak/trough
402        // that tracks close equity — an intrabar swing does not move the mark.
403        // Seed with the starting capital (the declaration bar's equity).
404        if self.equity_peak == f64::NEG_INFINITY {
405            self.equity_peak = initial;
406            self.equity_trough = initial;
407        }
408        self.equity_peak = self.equity_peak.max(equity);
409        self.equity_trough = self.equity_trough.min(equity);
410        self.max_drawdown = self.max_drawdown.max(self.equity_peak - intrabar_low);
411        self.max_runup = self.max_runup.max(intrabar_high - self.equity_trough);
412
413        // Pine's identity equity = initial + netprofit + openprofit; derive
414        // netprofit from it so commission can't make the two drift.
415        let net_profit = equity - initial - open_profit;
416        // na, not 0, when flat — matching Pine.
417        let avg_price = if position.size == 0.0 {
418            Value::Na
419        } else {
420            Value::Number(position.avg_price)
421        };
422
423        let refreshed = [
424            ("position_size", Value::Number(position.size)),
425            ("position_avg_price", avg_price),
426            ("equity", Value::Number(equity)),
427            ("netprofit", Value::Number(net_profit)),
428            ("openprofit", Value::Number(open_profit)),
429            ("grossprofit", Value::Number(gross_profit)),
430            ("grossloss", Value::Number(gross_loss)),
431            ("max_drawdown", Value::Number(self.max_drawdown)),
432            ("max_runup", Value::Number(self.max_runup)),
433            ("opentrades", Value::Int(open_trades)),
434            ("closedtrades", Value::Int(closed_trades)),
435            ("wintrades", Value::Int(wins)),
436            ("losstrades", Value::Int(losses)),
437            ("eventrades", Value::Int(evens)),
438        ];
439        for (name, value) in refreshed {
440            self.interpreter.set_object_field("strategy", name, value);
441        }
442    }
443
444    /// Replay the script over every bar from its source, returning what each
445    /// one produced.
446    pub fn run(mut self) -> Result<Run<O>, Error> {
447        let bars = std::mem::take(&mut self.bars);
448        let outputs = bars
449            .iter()
450            .map(|bar| self.execute(bar))
451            .collect::<Result<Vec<O>, Error>>()?;
452        let backtest = self.take_backtest();
453        Ok(Run { outputs, backtest })
454    }
455
456    fn take_backtest(&mut self) -> Option<Backtest> {
457        let broker = self.interpreter.broker.as_ref()?;
458        let close = self.last_close;
459
460        // Closed trades first, then those still open.
461        let mut trades: Vec<_> = broker.closed_trades().to_vec();
462        trades.extend(broker.open_trades().into_iter().cloned());
463
464        let open_profit: f64 = broker.open_trades().iter().map(|t| t.profit(close)).sum();
465        let initial_capital = broker.initial_capital();
466        let position_size = broker.position().size;
467
468        let (mut gross_profit, mut gross_loss) = (0.0, 0.0);
469        let (mut win_trades, mut loss_trades, mut even_trades) = (0, 0, 0);
470        for trade in broker.closed_trades() {
471            let profit = trade.profit(close);
472            if profit > 0.0 {
473                gross_profit += profit;
474                win_trades += 1;
475            } else if profit < 0.0 {
476                gross_loss -= profit;
477                loss_trades += 1;
478            } else {
479                even_trades += 1;
480            }
481        }
482
483        let equity = std::mem::take(&mut self.equity_curve);
484        let final_equity = equity.last().copied().unwrap_or(initial_capital);
485
486        Some(Backtest {
487            initial_capital,
488            net_profit: final_equity - initial_capital - open_profit,
489            open_profit,
490            gross_profit,
491            gross_loss,
492            max_drawdown: self.max_drawdown,
493            max_runup: self.max_runup,
494            win_trades,
495            loss_trades,
496            even_trades,
497            position_size,
498            mark_price: close,
499            equity,
500            trades,
501        })
502    }
503}
504
505pub fn execute(source: &str, data: Data) -> Result<(), Error> {
506    ScriptBuilder::<DefaultPineOutput>::with_code(source)
507        .with_data(data)
508        .compile()?
509        .run()
510        .map(|_| ())
511}