diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 3604f0e..88196c2 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -22,11 +22,6 @@ name = "interpreter" harness = false path = "src/interpreter.rs" -[[bench]] -name = "technical_analysis" -harness = false -path = "src/technical_analysis.rs" - [dependencies] criterion = { package = "codspeed-criterion-compat", version = "2.7", features = [ "html_reports", diff --git a/benches/src/interpreter.rs b/benches/src/interpreter.rs index 8abf729..b276127 100644 --- a/benches/src/interpreter.rs +++ b/benches/src/interpreter.rs @@ -1,24 +1,11 @@ -mod test_data; - use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use pine_lang::core::DefaultPineOutput; -use pine_lang::{execute, ScriptBuilder}; -use test_data::generate_bars; - -const TEST_SCRIPTS: &[(&str, &str)] = &[ - ("simple", include_str!("../test_data/simple.pine")), - ( - "moving_averages", - include_str!("../test_data/moving_averages.pine"), - ), - ("rsi", include_str!("../test_data/rsi.pine")), - ("macd", include_str!("../test_data/macd.pine")), - ("complex", include_str!("../test_data/complex.pine")), -]; +use pine_core::MAX_LOOKBACK; +use pine_lang::execute; +use pinecone_benches::{generate_bars, TEST_SCRIPTS}; -fn bench_single_bar(c: &mut Criterion) { +fn bench_full_run(c: &mut Criterion) { let mut group = c.benchmark_group("interpreter/single_bar"); - let data = generate_bars(200); // Generate enough bars for historical lookback + let data = generate_bars(MAX_LOOKBACK * 2); for (name, source) in TEST_SCRIPTS { group.bench_with_input(BenchmarkId::from_parameter(name), source, |b, source| { @@ -31,22 +18,5 @@ fn bench_single_bar(c: &mut Criterion) { group.finish(); } -fn bench_compile_only(c: &mut Criterion) { - let mut group = c.benchmark_group("interpreter/compile"); - - for (name, source) in TEST_SCRIPTS { - group.bench_with_input(BenchmarkId::from_parameter(name), source, |b, source| { - b.iter(|| { - let _ = ScriptBuilder::::with_code(black_box(source)) - .with_data(pine_lang::core::Data::default()) - .compile() - .unwrap(); - }); - }); - } - - group.finish(); -} - -criterion_group!(benches, bench_compile_only, bench_single_bar); +criterion_group!(benches, bench_full_run); criterion_main!(benches); diff --git a/benches/src/lexer.rs b/benches/src/lexer.rs index b9df1d0..3527abf 100644 --- a/benches/src/lexer.rs +++ b/benches/src/lexer.rs @@ -1,16 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use pine_lexer::Lexer; - -const TEST_SCRIPTS: &[(&str, &str)] = &[ - ("simple", include_str!("../test_data/simple.pine")), - ( - "moving_averages", - include_str!("../test_data/moving_averages.pine"), - ), - ("rsi", include_str!("../test_data/rsi.pine")), - ("macd", include_str!("../test_data/macd.pine")), - ("complex", include_str!("../test_data/complex.pine")), -]; +use pinecone_benches::TEST_SCRIPTS; fn bench_lexer(c: &mut Criterion) { let mut group = c.benchmark_group("lexer"); diff --git a/benches/src/parser.rs b/benches/src/parser.rs index 5ad92ca..71a87a0 100644 --- a/benches/src/parser.rs +++ b/benches/src/parser.rs @@ -1,17 +1,7 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use pine_lexer::Lexer; use pine_parser::Parser; - -const TEST_SCRIPTS: &[(&str, &str)] = &[ - ("simple", include_str!("../test_data/simple.pine")), - ( - "moving_averages", - include_str!("../test_data/moving_averages.pine"), - ), - ("rsi", include_str!("../test_data/rsi.pine")), - ("macd", include_str!("../test_data/macd.pine")), - ("complex", include_str!("../test_data/complex.pine")), -]; +use pinecone_benches::TEST_SCRIPTS; fn bench_parser(c: &mut Criterion) { let mut group = c.benchmark_group("parser"); diff --git a/benches/src/technical_analysis.rs b/benches/src/technical_analysis.rs deleted file mode 100644 index 3276677..0000000 --- a/benches/src/technical_analysis.rs +++ /dev/null @@ -1,116 +0,0 @@ -mod test_data; - -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use pine_lang::execute; -use test_data::generate_bars; - -fn bench_moving_averages(c: &mut Criterion) { - let mut group = c.benchmark_group("ta/moving_averages"); - - let scripts = [ - ("sma", "sma20 = ta.sma(close, 20)"), - ("ema", "ema20 = ta.ema(close, 20)"), - ("wma", "wma20 = ta.wma(close, 20)"), - ("rma", "rma20 = ta.rma(close, 20)"), - ("hma", "hma20 = ta.hma(close, 20)"), - ]; - - for lookback in [50, 100, 200].iter() { - let data = generate_bars(*lookback); - - for (name, source) in scripts.iter() { - group.bench_with_input(BenchmarkId::new(*name, lookback), source, |b, source| { - b.iter(|| { - execute(black_box(source), data.clone()).unwrap(); - }); - }); - } - } - - group.finish(); -} - -fn bench_oscillators(c: &mut Criterion) { - let mut group = c.benchmark_group("ta/oscillators"); - - let scripts = [ - ("rsi", "rsi14 = ta.rsi(close, 14)"), - ("cci", "cci20 = ta.cci(close, 20)"), - ("mom", "mom10 = ta.mom(close, 10)"), - ("roc", "roc12 = ta.roc(close, 12)"), - ("cmo", "cmo14 = ta.cmo(close, 14)"), - ]; - - for lookback in [50, 100, 200].iter() { - let data = generate_bars(*lookback); - - for (name, source) in scripts.iter() { - group.bench_with_input(BenchmarkId::new(*name, lookback), source, |b, source| { - b.iter(|| { - execute(black_box(source), data.clone()).unwrap(); - }); - }); - } - } - - group.finish(); -} - -fn bench_volatility(c: &mut Criterion) { - let mut group = c.benchmark_group("ta/volatility"); - - let scripts = [ - ("atr", "atr14 = ta.atr(14)"), - ("stdev", "stdev20 = ta.stdev(close, 20)"), - ("tr", "tr_val = ta.tr(true)"), - ]; - - for lookback in [50, 100, 200].iter() { - let data = generate_bars(*lookback); - - for (name, source) in scripts.iter() { - group.bench_with_input(BenchmarkId::new(*name, lookback), source, |b, source| { - b.iter(|| { - execute(black_box(source), data.clone()).unwrap(); - }); - }); - } - } - - group.finish(); -} - -fn bench_comparison(c: &mut Criterion) { - let mut group = c.benchmark_group("ta/comparison"); - - let scripts = [ - ("cross", "crossed = ta.cross(close, ta.sma(close, 20))"), - ("crossover", "co = ta.crossover(close, ta.sma(close, 20))"), - ("crossunder", "cu = ta.crossunder(close, ta.sma(close, 20))"), - ("rising", "up = ta.rising(close, 3)"), - ("falling", "down = ta.falling(close, 3)"), - ]; - - for lookback in [50, 100, 200].iter() { - let data = generate_bars(*lookback); - - for (name, source) in scripts.iter() { - group.bench_with_input(BenchmarkId::new(*name, lookback), source, |b, source| { - b.iter(|| { - execute(black_box(source), data.clone()).unwrap(); - }); - }); - } - } - - group.finish(); -} - -criterion_group!( - benches, - bench_moving_averages, - bench_oscillators, - bench_volatility, - bench_comparison -); -criterion_main!(benches); diff --git a/benches/src/test_data.rs b/benches/src/test_data.rs index 23d0197..b3b0de5 100644 --- a/benches/src/test_data.rs +++ b/benches/src/test_data.rs @@ -1,6 +1,17 @@ use pine_core::Bar; use pine_core::Data; +pub const TEST_SCRIPTS: &[(&str, &str)] = &[ + ("simple", include_str!("../test_data/simple.pine")), + ( + "moving_averages", + include_str!("../test_data/moving_averages.pine"), + ), + ("rsi", include_str!("../test_data/rsi.pine")), + ("macd", include_str!("../test_data/macd.pine")), + ("complex", include_str!("../test_data/complex.pine")), +]; + /// Generate synthetic OHLCV bar data for benchmarking pub fn generate_bars(count: usize) -> Data { let mut bars = Vec::with_capacity(count); diff --git a/crates/pine-builtins/src/ta/mod.rs b/crates/pine-builtins/src/ta/mod.rs index 8904979..478a083 100644 --- a/crates/pine-builtins/src/ta/mod.rs +++ b/crates/pine-builtins/src/ta/mod.rs @@ -1,5 +1,5 @@ use pine_builtin_macro::BuiltinFunction; -use pine_core::{PineOutput, PineVersion, MAX_LOOKBACK}; +use pine_core::{PineOutput, PineVersion, SeriesBuffer, MAX_LOOKBACK}; use pine_interpreter::{Builtin, Interpreter, PerBarAdvance, RuntimeError, Series, Value}; use std::cell::{Cell, RefCell}; use std::collections::HashMap; @@ -121,7 +121,7 @@ pub fn register( Value::Series(Series { id: format!("ta.{name}"), current: Box::new(Value::Number(seed)), - history: Some(Rc::new(RefCell::new(Vec::new()))), + history: Some(Rc::new(RefCell::new(SeriesBuffer::default()))), }), ); } @@ -132,7 +132,7 @@ pub fn register( let vwap_series = Rc::new(RefCell::new(Value::Series(Series { id: "ta.vwap".to_string(), current: Box::new(Value::Na), - history: Some(Rc::new(RefCell::new(Vec::new()))), + history: Some(Rc::new(RefCell::new(SeriesBuffer::default()))), }))); ta_ns.insert("vwap".to_string(), { let cell = Rc::clone(&vwap_series); @@ -174,7 +174,7 @@ fn series_now(ctx: &Interpreter, name: &str) -> Option { } fn series_prev(ctx: &Interpreter, name: &str) -> Option { - ctx.user_series_history.get(name)?.last()?.as_number().ok() + ctx.user_series_history.get(name)?.get(0)?.as_number().ok() } /// The current bar's OHLCV plus the previous bar's `close`/`volume` — everything @@ -302,11 +302,7 @@ fn step_series(series: &mut Value, push: bool, next: f64) { if push { if let Some(history) = &s.history { let mut history = history.borrow_mut(); - history.push((*s.current).clone()); - if history.len() > MAX_LOOKBACK { - let excess = history.len() - MAX_LOOKBACK; - history.drain(..excess); - } + history.push((*s.current).clone(), MAX_LOOKBACK); } } *s.current = Value::Number(next); diff --git a/crates/pine-interpreter/src/lib.rs b/crates/pine-interpreter/src/lib.rs index 2a15dc2..ac6ad3a 100644 --- a/crates/pine-interpreter/src/lib.rs +++ b/crates/pine-interpreter/src/lib.rs @@ -4,7 +4,7 @@ mod signature; pub use num::Num; pub use signature::{BuiltinSignature, Param, ParamType}; -use pine_core::{Color, DefaultPineOutput, PineOutput, MAX_LOOKBACK}; +use pine_core::{Color, DefaultPineOutput, PineOutput, SeriesBuffer, MAX_LOOKBACK}; use pine_ast::{Argument, BinOp, Expr, Literal, MethodParam, Program, Stmt, TypeField, UnOp}; use std::cell::RefCell; @@ -20,15 +20,14 @@ pub use pine_core::LibraryLoader; /// Takes the history map rather than `&mut self` so callers can hold a borrow of /// another interpreter field while recording. fn push_history( - history: &mut HashMap>>, + history: &mut HashMap>>, name: &str, value: Value, ) { - let entries = history.entry(name.to_string()).or_default(); - entries.push(value); - if entries.len() > MAX_LOOKBACK { - entries.drain(..entries.len() - MAX_LOOKBACK); - } + history + .entry(name.to_string()) + .or_default() + .push(value, MAX_LOOKBACK); } /// Apply a numeric binary operator under Pine's int/float rule (see [`Num`]). @@ -104,7 +103,7 @@ struct Variable { pub struct Series { pub id: String, pub current: Box>, - pub history: Option>>>>, + pub history: Option>>>>, } /// The lazy scalar an object carries, so a single name can be *both* a namespace @@ -449,8 +448,8 @@ struct MethodDef { /// variable (a call, arithmetic, …). Mirrors `user_series_history`, keyed by the /// `Expr::Index` node's stable id, so `(expr)[n]` matches `v = expr; v[n]`. struct SeriesSite { - /// Past bars, oldest first; the last entry is the previous bar. - history: Vec>, + /// Past bars, newest first; entry 0 is the previous bar. + history: SeriesBuffer>, /// This bar's value, once the site has been evaluated on it. current: Option>, /// `bar_seq` when `current` was recorded, so history rolls once per bar. @@ -460,7 +459,7 @@ struct SeriesSite { impl SeriesSite { fn new() -> Self { Self { - history: Vec::new(), + history: SeriesBuffer::default(), current: None, bar: 0, } @@ -484,9 +483,9 @@ pub struct Interpreter { /// Output storage for plots, labels, logs, etc. pub output: O, /// Per-variable history for user-computed series (`var` declarations). - /// history[len-1] = previous bar, history[len-2] = two bars ago, etc. + /// get(0) = previous bar, get(1) = two bars ago, etc. /// Populated on each `Stmt::Assignment`; supports Pine's `name[n]` lookback. - pub user_series_history: HashMap>>, + pub user_series_history: HashMap>>, /// History for subscripted non-variable series expressions (`ta.sma(..)[1]`, /// `(high+low)[1]`), keyed by the `Expr::Index` node's id — the same /// site-keyed pattern as `function_local_state`. @@ -1497,18 +1496,14 @@ impl Interpreter { let index_val = index_num as usize; // A named variable with tracked history looks up - // user_series_history: history[len-1] = previous bar. A tracked + // user_series_history: get(0) = previous bar. A tracked // variable with insufficient depth yields na (warm-up). Variables // WITHOUT tracked history (e.g. builtin Series like `close` fed by // the host) fall through to the shared path below. if index_val > 0 { if let Expr::Variable { name: var_name, .. } = expr.as_ref() { if let Some(h) = self.user_series_history.get(var_name) { - return Ok(if h.len() >= index_val { - h[h.len() - index_val].clone() - } else { - Value::Na - }); + return Ok(h.get(index_val - 1).cloned().unwrap_or(Value::Na)); } // A plain non-series value with no history (a user var // assigned only this bar) indexes as na, not an error. @@ -1531,11 +1526,7 @@ impl Interpreter { return Ok((*series.current).clone()); } let h = history.borrow(); - return Ok(if h.len() >= index_val { - h[h.len() - index_val].clone() - } else { - Value::Na - }); + return Ok(h.get(index_val - 1).cloned().unwrap_or(Value::Na)); } } @@ -1566,20 +1557,16 @@ impl Interpreter { // MAX_LOOKBACK, exactly like user_series_history, so memory // stays flat over a long run. if let Some(previous) = site.current.take() { - site.history.push(previous); - if site.history.len() > MAX_LOOKBACK { - let drop = site.history.len() - MAX_LOOKBACK; - site.history.drain(..drop); - } + site.history.push(previous, MAX_LOOKBACK); } site.bar = seq; } site.current = Some(current); - Ok(if site.history.len() >= index_val { - site.history[site.history.len() - index_val].clone() - } else { - Value::Na - }) + Ok(site + .history + .get(index_val - 1) + .cloned() + .unwrap_or(Value::Na)) } Expr::Switch { value, cases } => {