mod ast;
mod builtins;
mod lexer;
mod parser;
mod runtime;
mod strategy;
mod types;
pub use ast::{Expr, Program, Stmt};
pub use builtins::register_builtins;
pub use lexer::{Lexer, Token, TokenKind};
pub use parser::{ParseError, Parser};
pub use runtime::Runtime;
pub use strategy::{PlotOutput, Pos, StrategyMetrics, StrategyState, Trade};
pub use types::{RuntimeError, TradeDirection, Value};
use crate::model::Bar;
use crate::studies::{Indicator, IndicatorValue};
#[derive(Clone)]
pub struct PineScriptIndicator {
name: String,
script: String,
runtime: Runtime,
values: Vec<IndicatorValue>,
overlay: bool,
colors: Vec<egui::Color32>,
visible: bool,
}
impl PineScriptIndicator {
pub fn new(script: String) -> Result<Self, ParseError> {
let lexer = Lexer::new(&script);
let mut parser = Parser::new(lexer);
let program = parser.parse()?;
let mut runtime = Runtime::new();
register_builtins(&mut runtime);
let name = Self::extract_name(&program).unwrap_or_else(|| "Pine Script".to_string());
let overlay = Self::extract_overlay(&program);
Ok(Self {
name,
script,
runtime,
values: Vec::new(),
overlay,
colors: vec![egui::Color32::BLUE],
visible: true,
})
}
fn extract_name(program: &Program) -> Option<String> {
for stmt in &program.statements {
if let Stmt::Expression(expr) = stmt
&& let Expr::FunctionCall { name, args } = expr
&& (name == "indicator" || name == "strategy")
&& let Some(Expr::StringLiteral(title)) = args.first()
{
return Some(title.clone());
}
}
None
}
fn extract_overlay(program: &Program) -> bool {
for stmt in &program.statements {
if let Stmt::Expression(expr) = stmt
&& let Expr::FunctionCall { name, args: _ } = expr
&& name == "indicator"
{
return true;
}
}
false
}
pub fn execute(&mut self, bars: &[Bar]) -> Result<(), RuntimeError> {
self.values.clear();
self.runtime.set_bars(bars);
self.runtime.execute(&self.script)?;
self.values = self.runtime.get_plot_values();
Ok(())
}
}
impl Indicator for PineScriptIndicator {
fn name(&self) -> &str {
&self.name
}
fn calculate(&mut self, bars: &[Bar]) {
if let Err(e) = self.execute(bars) {
eprintln!("Pine Script error: {e}");
}
}
fn values(&self) -> &[IndicatorValue] {
&self.values
}
fn is_overlay(&self) -> bool {
self.overlay
}
fn colors(&self) -> Vec<egui::Color32> {
self.colors.clone()
}
fn set_colors(&mut self, colors: Vec<egui::Color32>) {
self.colors = colors;
}
fn is_visible(&self) -> bool {
self.visible
}
fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
fn line_cnt(&self) -> usize {
1
}
fn clone_box(&self) -> Box<dyn Indicator> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_sma_script() {
let script = r#"
//@version=5
indicator("My SMA", overlay=true)
length = 20
sma_val = ta.sma(close, length)
plot(sma_val)
"#;
let result = PineScriptIndicator::new(script.to_string());
assert!(result.is_ok());
let indicator = result.unwrap();
assert_eq!(indicator.name(), "My SMA");
assert!(indicator.is_overlay());
}
}