mod run;
mod table;
pub use table::STUDIES;
use serde_json::{Map, Value, json};
use super::SessionAnchor;
use crate::error::Result;
use crate::series::TimeSeries;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StudyFamily {
MovingAverage,
Bands,
Momentum,
Trend,
Volatility,
Volume,
Statistical,
Price,
Session,
}
pub const STUDY_FAMILIES: [StudyFamily; 9] = [
StudyFamily::MovingAverage,
StudyFamily::Bands,
StudyFamily::Momentum,
StudyFamily::Trend,
StudyFamily::Volatility,
StudyFamily::Volume,
StudyFamily::Statistical,
StudyFamily::Price,
StudyFamily::Session,
];
impl StudyFamily {
pub fn as_str(self) -> &'static str {
match self {
StudyFamily::MovingAverage => "moving-average",
StudyFamily::Bands => "bands",
StudyFamily::Momentum => "momentum",
StudyFamily::Trend => "trend",
StudyFamily::Volatility => "volatility",
StudyFamily::Volume => "volume",
StudyFamily::Statistical => "statistical",
StudyFamily::Price => "price",
StudyFamily::Session => "session",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StudyUnit {
Inherit,
Delta,
Percent,
Ratio,
Signal,
Volume,
Index,
Bars,
}
impl StudyUnit {
pub fn as_str(self) -> &'static str {
match self {
StudyUnit::Inherit => "inherit",
StudyUnit::Delta => "delta",
StudyUnit::Percent => "percent",
StudyUnit::Ratio => "ratio",
StudyUnit::Signal => "signal",
StudyUnit::Volume => "volume",
StudyUnit::Index => "index",
StudyUnit::Bars => "bars",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StudyInput {
pub role: &'static str,
pub default: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StudyParam {
pub name: &'static str,
pub kind: ParamKind,
pub optional: bool,
pub requires: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ParamKind {
Number {
integer: bool,
default: Option<f64>,
example: Option<f64>,
min: Option<f64>,
max: Option<f64>,
suggest: Option<(f64, f64)>,
},
Enum {
of: &'static [&'static str],
default: Option<&'static str>,
example: Option<&'static str>,
},
}
impl StudyParam {
pub fn default_number(&self) -> Option<f64> {
match self.kind {
ParamKind::Number { default, .. } => default,
ParamKind::Enum { .. } => None,
}
}
fn to_json(self) -> Value {
let mut o = Map::new();
match self.kind {
ParamKind::Number {
integer,
default,
example,
min,
max,
suggest,
} => {
o.insert(
"kind".into(),
json!(if integer { "integer" } else { "number" }),
);
for (key, v) in [
("default", default),
("example", example),
("min", min),
("max", max),
] {
if let Some(v) = v {
o.insert(key.into(), json_number(v));
}
}
if let Some((lo, hi)) = suggest {
o.insert("suggest".into(), json!([json_number(lo), json_number(hi)]));
}
}
ParamKind::Enum {
of,
default,
example,
} => {
o.insert("kind".into(), json!("enum"));
if let Some(v) = default {
o.insert("default".into(), json!(v));
}
if let Some(v) = example {
o.insert("example".into(), json!(v));
}
o.insert("of".into(), json!(of));
}
}
if self.optional {
o.insert("optional".into(), json!(true));
}
if let Some(r) = self.requires {
o.insert("requires".into(), json!(r));
}
Value::Object(o)
}
}
fn json_number(v: f64) -> Value {
if v.fract() == 0.0 && v.abs() < 9.0e15 {
json!(v as i64)
} else {
json!(v)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StudyOutput {
pub id: &'static str,
pub unit: StudyUnit,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StudyNaming {
Output(&'static str),
Prefix(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StudyAnchor {
Session,
Time,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct StudyDescriptor {
pub name: &'static str,
pub family: StudyFamily,
pub summary: &'static str,
pub inputs: &'static [StudyInput],
pub params: &'static [StudyParam],
pub naming: StudyNaming,
pub outputs: &'static [StudyOutput],
pub anchor: Option<StudyAnchor>,
}
impl StudyDescriptor {
pub fn param(&self, name: &str) -> Option<&'static StudyParam> {
self.params.iter().find(|p| p.name == name)
}
pub fn output_names(&self) -> Vec<String> {
match self.naming {
StudyNaming::Output(name) => vec![name.to_string()],
StudyNaming::Prefix(prefix) => self
.outputs
.iter()
.map(|o| format!("{prefix}{}", o.id))
.collect(),
}
}
pub fn run(&self, series: &TimeSeries, options: &Map<String, Value>) -> Result<TimeSeries> {
run::run(self, series, options, None)
}
pub fn run_with(
&self,
series: &TimeSeries,
options: &Map<String, Value>,
sessions: SessionAnchor<'_>,
) -> Result<TimeSeries> {
run::run(self, series, options, Some(sessions))
}
pub fn to_json(&self) -> Value {
let mut o = Map::new();
o.insert("name".into(), json!(self.name));
o.insert("family".into(), json!(self.family.as_str()));
o.insert("summary".into(), json!(self.summary));
let inputs: Vec<Value> = self
.inputs
.iter()
.map(|i| match i.default {
Some(d) => json!({ "role": i.role, "default": d }),
None => json!({ "role": i.role }),
})
.collect();
o.insert("inputs".into(), Value::Array(inputs));
let params: Map<String, Value> = self
.params
.iter()
.map(|p| (p.name.to_string(), p.to_json()))
.collect();
o.insert("params".into(), Value::Object(params));
let (kind, default) = match self.naming {
StudyNaming::Output(d) => ("output", d),
StudyNaming::Prefix(d) => ("prefix", d),
};
o.insert("naming".into(), json!({ "kind": kind, "default": default }));
let outputs: Vec<Value> = self
.outputs
.iter()
.map(|out| json!({ "id": out.id, "unit": out.unit.as_str() }))
.collect();
o.insert("outputs".into(), Value::Array(outputs));
if let Some(anchor) = self.anchor {
let a = match anchor {
StudyAnchor::Session => "session",
StudyAnchor::Time => "time",
};
o.insert("anchor".into(), json!(a));
}
Value::Object(o)
}
}
pub fn study_descriptor(name: &str) -> Option<&'static StudyDescriptor> {
STUDIES.iter().find(|d| d.name == name)
}