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, 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/// 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 Error {
54    /// The 1-based `(line, column)` an editor should point at, when the error
55    /// carries a position. Version errors have none.
56    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            // One diagnostic per line, so multiple errors are simply appended.
74            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
118/// Parse, semantically analyze and lint `source`.
119pub 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
139/// The diagnostics from [`analyze`].
140pub fn check(source: &str, loader: Option<&dyn LibraryLoader>) -> Result<Vec<Diagnostic>, Error> {
141    Ok(analyze(source, loader)?.diagnostics)
142}
143
144/// Parse and lint `source`, returning only the lint findings (no semantic
145/// analysis). `// @skip(...)` directives are honored.
146pub 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
153/// Decode `input.*` overrides from a JSON object — `{"Length": 20, "Smooth":
154/// true, "Source": "close"}` — keyed by input title, for
155/// [`ScriptBuilder::with_inputs`].
156pub 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    /// Host overrides for the script's `input.*` calls, keyed by input title.
192    /// Each `input.*` returns (and validates) the override for its title if one
193    /// is present, else its declared default. See [`inputs_from_json`].
194    pub fn with_inputs(mut self, inputs: HashMap<String, pine_core::InputValue>) -> Self {
195        self.inputs = inputs;
196        self
197    }
198
199    /// Host-supplied variables the script can reference, registered as consts
200    /// alongside the builtin namespaces.
201    pub fn with_custom_variables(mut self, variables: HashMap<String, Value<O>>) -> Self {
202        self.custom_variables = variables;
203        self
204    }
205
206    /// Resolves `import` statements. Without one, importing a library fails.
207    pub fn with_library_loader(mut self, loader: Box<dyn LibraryLoader>) -> Self {
208        self.library_loader = Some(loader);
209        self
210    }
211
212    /// Supplies bars for `request.security`. Without one, `request.security`
213    /// returns na.
214    pub fn with_request_provider(mut self, provider: Box<dyn DataProvider>) -> Self {
215        self.request_provider = Some(provider);
216        self
217    }
218
219    /// Swaps the broker a `strategy` trades against. Without one, the built-in
220    /// [`DefaultBrokerFactory`](pine_broker::DefaultBrokerFactory) is used.
221    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    /// The chart timeframe exposed to the script as `timeframe.*`. Without one,
232    /// the namespace is populated with defaults.
233    pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
234        self.timeframe = timeframe;
235        self
236    }
237
238    /// Run over only the last `bar_count` bars of the feed. Without one, the
239    /// whole feed is used.
240    pub fn with_bar_count(mut self, bar_count: usize) -> Self {
241        self.bar_count = Some(bar_count);
242        self
243    }
244
245    /// The market to run over: the bars, and the symbol and timeframe they
246    /// belong to.
247    ///
248    /// The data describes itself, so it fills in `syminfo.*` and `timeframe.*`
249    /// too. An explicit [`ScriptBuilder::with_syminfo`] or
250    /// [`ScriptBuilder::with_timeframe`] still wins, whichever order they are
251    /// called in.
252    pub fn with_data(mut self, data: Data) -> Self {
253        self.data = Some(data);
254        self
255    }
256
257    /// Compile PineScript source code into a Script with default output
258    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        // Keep only the last `bar_count` bars when the caller limited the run.
292        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        // The chart's bar spacing, so `request.security_lower_tf` can reject a
299        // request that is not actually lower than the chart timeframe.
300        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        // The interpreter's const environment: the registered namespaces plus any
316        // host-supplied globals. Built once and handed over as-is.
317        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        // Semantic pre-check: reject if sema produces errors.
332        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        // Create interpreter and load builtin namespace objects
342        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
373/// A compiled PineScript program, and the bars it will run over.
374///
375/// State accumulates across bars — series history, `var` locals, and every
376/// stateful builtin's window — exactly as it does in TradingView. That makes a
377/// `Script` single-use: [`Script::run`] takes it by value so a second run
378/// cannot inherit the first one's state.
379pub struct Script<O: PineOutput> {
380    program: Program,
381    interpreter: Interpreter<O>,
382    /// The chart timeframe, carried onto the `Backtest` so its metrics can
383    /// annualise per-bar figures.
384    timeframe: Timeframe,
385    /// Bars from the builder's source; empty when none was given.
386    bars: Vec<Bar>,
387    /// Account value at each bar's close, accumulated while a `strategy` runs.
388    equity_curve: Vec<f64>,
389    /// The last bar's close, used to mark open trades at the run's end.
390    last_close: f64,
391    /// Running equity extremes for `strategy.max_drawdown`/`max_runup`.
392    equity_peak: f64,
393    equity_trough: f64,
394    max_drawdown: f64,
395    max_runup: f64,
396    /// Drawdown/run-up as a fraction of the peak/trough, tracked separately
397    /// because the percentage extreme need not coincide with the cash extreme.
398    max_drawdown_percent: f64,
399    max_runup_percent: f64,
400    /// Largest position (in contracts) ever held, overall and per side.
401    max_contracts_all: f64,
402    max_contracts_long: f64,
403    max_contracts_short: f64,
404}
405
406impl<O: PineOutput> Script<O> {
407    /// Run one bar. Private: bars must be replayed in order from the first, so
408    /// [`Script::run`] is the only way in.
409    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        // Fill orders left pending by the previous bar before the body runs, so
423        // it reads the position and equity they produced. A no-op unless the
424        // script declared a `strategy`.
425        self.advance_broker(bar);
426
427        let output = self.interpreter.execute(&self.program)?;
428
429        // Read after the body so the bar a `strategy` is declared on is counted.
430        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    /// Advance the simulated broker one bar and refresh the read-only
439    /// `strategy.*` values from it. The interpreter only holds the broker
440    /// handle; the backtest accounting that maps it onto script variables lives
441    /// here, in the host.
442    fn advance_broker(&mut self, bar: &Bar) {
443        use interpreter::Value;
444
445        let close = bar.close;
446
447        // Read from the broker, then drop the borrow to update `self`'s state.
448        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        // Equity is monotonic in price, so its intrabar extremes are the marks
457        // at the bar's high and low — lower is adverse, higher favourable.
458        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); // closed, so the price is ignored
469            if profit > 0.0 {
470                gross_profit += profit;
471                wins += 1;
472            } else if profit < 0.0 {
473                gross_loss -= profit; // positive magnitude, as Pine reports it
474                losses += 1;
475            } else {
476                evens += 1;
477            }
478        }
479
480        // Read the rest off the broker while its borrow is live: each closed
481        // trade's percent return (for the average-trade-percent figures) and the
482        // open position's entry name.
483        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        // Drawdown/run-up measure the intrabar extreme against a peak/trough
505        // that tracks close equity — an intrabar swing does not move the mark.
506        // Seed with the starting capital (the declaration bar's equity).
507        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        // Largest position held, overall and per side.
524        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        // Pine's identity equity = initial + netprofit + openprofit; derive
532        // netprofit from it so commission can't make the two drift.
533        let net_profit = equity - initial - open_profit;
534        // na, not 0, when flat — matching Pine.
535        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            // `opentrades` / `closedtrades` are value-objects that read their
552            // count straight from the broker, so they are not refreshed here.
553            ("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        // Derived statistics: percentages of the starting capital, and per-trade
562        // averages. `na` when there are no trades to average, matching Pine.
563        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            // Losing trades are reported as a negative average, so negate the
609            // positive gross-loss magnitude.
610            ("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    /// Replay the script over every bar from its source, returning what each
622    /// one produced.
623    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        // Closed trades first, then those still open.
639        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}