use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use pine_ast::{Expr, Program, Stmt, VarKind};
use pine_builtin_macro::BuiltinFunction;
use pine_core::{Data, Timeframe};
use pine_interpreter::{Interpreter, PineOutput, RuntimeError, Series, Value};
type SecondarySeries<O> = Rc<Vec<(i64, Value<O>)>>;
pub fn register<O: PineOutput>() -> Value<O> {
let mut fields: HashMap<String, Value<O>> = HashMap::new();
fields.insert(
"security".to_string(),
RequestSecurity::<O>::builtin_value(),
);
fields.insert(
"security_lower_tf".to_string(),
RequestSecurityLowerTf::<O>::builtin_value(),
);
Value::Object {
type_name: "request".to_string(),
fields: Rc::new(RefCell::new(fields)),
call: None,
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "request.security", stateful)]
struct RequestSecurity<O: PineOutput> {
symbol: String,
timeframe: String,
#[arg(lazy)]
expression: Value<O>,
#[arg(default = None)]
gaps: Option<Value<O>>,
#[arg(default = None)]
lookahead: Option<Value<O>>,
#[arg(default = None)]
ignore_invalid_symbol: Option<bool>,
#[arg(default = None)]
currency: Option<String>,
#[arg(default = None)]
calc_bars_count: Option<f64>,
#[state]
series: Option<SecondarySeries<O>>,
}
impl<O: PineOutput> RequestSecurity<O> {
fn execute(&mut self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
let _ = (
&self.gaps,
&self.lookahead,
&self.ignore_invalid_symbol,
&self.currency,
&self.calc_bars_count,
);
let (Value::Expr(expr), Ok(timeframe)) =
(&self.expression, self.timeframe.parse::<Timeframe>())
else {
return Ok(Value::Na);
};
let expr = Rc::clone(expr);
if self.series.is_none() {
self.series = Some(Rc::new(request_series(ctx, &self.symbol, timeframe, &expr)));
}
let series = self.series.as_ref().expect("series built above");
Ok(aligned(series, current_time(ctx)))
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "request.security_lower_tf", stateful)]
struct RequestSecurityLowerTf<O: PineOutput> {
symbol: String,
timeframe: String,
#[arg(lazy)]
expression: Value<O>,
#[arg(default = None)]
ignore_invalid_symbol: Option<bool>,
#[arg(default = None)]
currency: Option<String>,
#[arg(default = None)]
calc_bars_count: Option<f64>,
#[state]
series: Option<SecondarySeries<O>>,
}
impl<O: PineOutput> RequestSecurityLowerTf<O> {
fn execute(&mut self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
let _ = (
&self.ignore_invalid_symbol,
&self.currency,
&self.calc_bars_count,
);
let (Value::Expr(expr), Ok(tf)) = (&self.expression, self.timeframe.parse::<Timeframe>())
else {
return Ok(Value::Na);
};
let expr = Rc::clone(expr);
if let (Some(tf_ms), Some(chart)) = (tf.to_millis(), ctx.chart_period) {
if tf_ms > chart {
return Err(RuntimeError::TypeError(format!(
"request.security_lower_tf: timeframe \"{}\" is not lower than the chart timeframe",
self.timeframe
)));
}
}
if self.series.is_none() {
self.series = Some(Rc::new(request_series(ctx, &self.symbol, tf, &expr)));
}
let series = self.series.as_ref().expect("series built above");
let now = current_time(ctx);
let end = ctx.chart_period.map_or(i64::MAX, |period| now + period);
let values: Vec<Value<O>> = series
.iter()
.filter(|(time, _)| *time >= now && *time < end)
.map(|(_, value)| value.clone())
.collect();
Ok(Value::Array(Rc::new(RefCell::new(values))))
}
}
fn request_series<O: PineOutput>(
ctx: &Interpreter<O>,
symbol: &str,
timeframe: Timeframe,
expr: &Expr,
) -> Vec<(i64, Value<O>)> {
let data = ctx
.request_provider
.clone()
.and_then(|provider| provider.request(symbol, timeframe).ok());
data.map_or_else(Vec::new, |data| {
secondary_series(&ctx.snapshot(), expr, data)
})
}
fn secondary_series<O: PineOutput>(
base_vars: &HashMap<String, Value<O>>,
expr: &Expr,
data: Data,
) -> Vec<(i64, Value<O>)> {
let mut interp = Interpreter::<O>::new();
for (name, value) in base_vars {
interp.set_variable(name, value.clone());
}
let program = Program::new(vec![Stmt::VarDecl {
name: "__req".to_string(),
type_qualifier: None,
type_annotation: None,
initializer: Some(expr.clone()),
var_kind: VarKind::Plain,
}]);
let mut series = Vec::with_capacity(data.bars.len());
for bar in &data.bars {
bind_bar(&mut interp, bar);
if interp.execute(&program).is_err() {
break;
}
let value = match interp.get_variable("__req") {
Some(Value::Series(series)) => (*series.current).clone(),
Some(value) => value.clone(),
None => Value::Na,
};
series.push((bar.time, value));
}
series
}
fn bind_bar<O: PineOutput>(interp: &mut Interpreter<O>, bar: &pine_core::Bar) {
for (id, value) in [
("open", bar.open),
("high", bar.high),
("low", bar.low),
("close", bar.close),
("volume", bar.volume),
("hl2", (bar.high + bar.low) / 2.0),
("hlc3", (bar.high + bar.low + bar.close) / 3.0),
("hlcc4", (bar.high + bar.low + bar.close * 2.0) / 4.0),
("ohlc4", (bar.open + bar.high + bar.low + bar.close) / 4.0),
] {
interp.advance_series(
id,
Value::Series(Series {
id: id.to_string(),
current: Box::new(Value::Number(value)),
}),
);
}
interp.set_variable("bar_index", Value::Number(bar.index as f64));
for (name, value) in crate::register_per_bar(bar) {
interp.set_variable(&name, value);
}
}
fn aligned<O: PineOutput>(series: &[(i64, Value<O>)], now: i64) -> Value<O> {
let confirmed = series.partition_point(|(time, _)| *time <= now);
confirmed
.checked_sub(1)
.and_then(|i| series.get(i))
.map_or(Value::Na, |(_, value)| value.clone())
}
fn current_time<O: PineOutput>(ctx: &Interpreter<O>) -> i64 {
match ctx.get_variable("time") {
Some(Value::Number(ms)) => *ms as i64,
Some(Value::Int(ms)) => *ms,
_ => 0,
}
}