diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a4bb7dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.rs] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = tab +tab_width = 4 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6524494 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +/.vscode/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ed2b6fb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = ["AMvDev "] +description = "Yet another Technical Analysis library. For rust now." +edition = "2018" +name = "yata" +version = "0.1.1" + +[dependencies] +serde = {version = "*", features = ["derive"], optional = true} + +[profile.release] +codegen-units = 1 +debug = false +debug-assertions = false +incremental = true +lto = true +opt-level = 3 +overflow-checks = false +panic = 'abort' +rpath = false + +[features] +default = ["serde"] +period_type_u16 = [] +period_type_u32 = [] +period_type_u64 = [] +value_type_f32 = [] diff --git a/LICENSE b/LICENSE index 4ed90b9..2e0a05f 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright 2020 AMvDev (amv-dev@protonmail.com) Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/README.md b/README.md index 8047b91..f590d0f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ -# tech-an +# YATA +Yet Another Technical Analysis library + +Yet Another Technical Analysis library + +YaTA implements most common technical analysis [methods](crate::methods) and [indicators](crate::indicators) + +It also provides you an iterface to create your own indicators. + +Some commonly used methods: + +- Accumulation-distribution index; +- Cross / CrossAbove / CrossUnder; +- Derivative (differential) +- Highest / Lowest / Highest-Lowest Delta +- Hull moving average +- Integral (sum) +- Linear regression moving average +- Momentum +- Pivot points +- Simple moving average +- Weighted moving average +- Volume weighted moving average +- Exponential moving average family: EMA, DMA, TMA DEMA, TEMA +- Symmetrically weighted moving average + +And many others. + +# Current usafe status + +Currently there is no `unsafe` code in the crate. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..0e9c091 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +edition = "2018" +hard_tabs = true +tab_spaces = 4 + +# struct_field_align_threshold = 20 +# enum_discrim_align_threshold = 20 +# fn_single_line = true diff --git a/src/core/action.rs b/src/core/action.rs new file mode 100644 index 0000000..693486c --- /dev/null +++ b/src/core/action.rs @@ -0,0 +1,468 @@ +use crate::core::ValueType; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::ops::{Neg, Sub}; + +type SignalType = u8; +const BOUND: SignalType = 255; + +/// Action is basic type of Indicator's signals +/// +/// It may be positive (means *Buy* some amount). It may be negative (means *Sell* some amount). Or there may be no signal at all. +/// +/// `Action` may be analog {1, 0, -1} or digital in range [-1.0; 1.0] +#[derive(Clone, Copy, Eq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Action { + /// Buy signal + Buy(SignalType), + /// No signal + None, + /// Sell signal + Sell(SignalType), +} + +impl Action { + /// Shortcut for *Buy All* signal + pub const BUY_ALL: Self = Self::Buy(BOUND); + + /// Shortcut for *Sell All* signal + pub const SELL_ALL: Self = Self::Sell(BOUND); + + /// Create instance from *analog* signal (which can be only -1, 0 or 1) + /// + /// Any positive number converts to BUY_ALL + /// + /// Any negatie number converts to SELL_ALL + /// + /// Zero converts to None + pub fn from_analog(value: i8) -> Self { + Self::from(value) + } + + /// Converts value with the interval [-1.0; 1.0] + pub fn ratio(&self) -> Option { + (*self).into() + } + + /// Returns a sign (1 or -1) of internal value if value exists and not zero. + /// + /// Otherwise returns 0 + pub fn analog(&self) -> i8 { + (*self).into() + } + + /// Returns a sign of internal value if value exists + /// + /// Otherwise returns None + pub fn sign(&self) -> Option { + (*self).into() + } + + /// Return an internal representation of the value if signal exists or None if it doesn't. + pub fn value(&self) -> Option { + match *self { + Self::None => None, + Self::Buy(v) | Self::Sell(v) => Some(v), + } + } + + /// Checks if there is no signal + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// Checks if there is signal + pub fn is_some(&self) -> bool { + !self.is_none() + } +} + +impl PartialEq for Action { + fn eq(&self, other: &Self) -> bool { + match (*self, *other) { + (Self::None, Self::None) => true, + (Self::Buy(a), Self::Buy(b)) | (Self::Sell(a), Self::Sell(b)) => a == b, + (Self::Buy(0), Self::Sell(0)) | (Self::Sell(0), Self::Buy(0)) => true, + _ => false, + } + } +} + +impl Default for Action { + fn default() -> Self { + Self::None + } +} + +impl From for Action { + fn from(value: bool) -> Self { + match value { + true => Self::BUY_ALL, + false => Self::None, + } + } +} + +impl From for Action { + fn from(value: i8) -> Self { + match value { + 0 => Self::None, + v => { + if v > 0 { + Self::BUY_ALL + } else { + Self::SELL_ALL + } + } + } + } +} + +impl From for i8 { + fn from(value: Action) -> Self { + match value { + Action::Buy(value) => (value > 0) as i8, + Action::None => 0, + Action::Sell(value) => -((value > 0) as i8), + } + } +} + +impl From> for Action { + fn from(value: Option) -> Self { + match value { + None => Self::None, + Some(v) => v.into(), + } + } +} + +impl From for Option { + fn from(value: Action) -> Self { + match value { + Action::None => None, + _ => Some(value.into()), + } + } +} + +impl From for Action { + fn from(v: f64) -> Self { + if v.is_nan() { + return Self::None; + } + + let normalized = v.max(-1.0).min(1.0); + + let value = (normalized.abs() * (BOUND as ValueType)).round() as SignalType; + + if normalized.is_sign_negative() { + if value >= BOUND { + Self::SELL_ALL + } else { + Self::Sell(value) + } + } else { + if value >= BOUND { + Self::BUY_ALL + } else { + Self::Buy(value) + } + } + } +} + +impl From> for Action { + fn from(value: Option) -> Self { + match value { + None => Self::None, + Some(value) => value.into(), + } + } +} + +impl From for Action { + fn from(v: f32) -> Self { + if v.is_nan() { + return Self::None; + } + + let normalized = v.max(-1.0).min(1.0); + + let value = (normalized.abs() * (BOUND as f32)).round() as SignalType; + + if normalized.is_sign_negative() { + if value >= BOUND { + Self::SELL_ALL + } else { + Self::Sell(value) + } + } else { + if value >= BOUND { + Self::BUY_ALL + } else { + Self::Buy(value) + } + } + } +} + +impl From> for Action { + fn from(value: Option) -> Self { + match value { + None => Self::None, + Some(value) => value.into(), + } + } +} + +impl From for Option { + fn from(value: Action) -> Self { + match value { + Action::None => None, + Action::Buy(value) => Some((value as ValueType) / (BOUND as ValueType)), + Action::Sell(value) => Some(-(value as ValueType) / (BOUND as ValueType)), + } + } +} + +impl + Copy> From<&T> for Action { + fn from(value: &T) -> Self { + (*value).into() + } +} + +// impl> From for i8 { +// fn from(value: T) -> Self { +// //value. +// } +// } + +impl Neg for Action { + type Output = Self; + + fn neg(self) -> Self::Output { + match self { + Self::None => Self::None, + Self::Buy(value) => Self::Sell(value), + Self::Sell(value) => Self::Buy(value), + } + } +} + +impl Sub for Action { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::None, Self::None) => Self::None, + (s, Self::None) => s, + (Self::None, s) => -s, + (Self::Buy(v1), Self::Buy(v2)) => { + if v1 >= v2 { + Self::Buy(v1 - v2) + } else { + Self::Sell(v2 - v1) + } + } + (Self::Sell(v1), Self::Sell(v2)) => { + if v1 >= v2 { + Self::Sell(v1 - v2) + } else { + Self::Buy(v2 - v1) + } + } + (s1, s2) => s1 - (-s2), + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "N"), + Self::Buy(value) => write!(f, "+{}", value), + Self::Sell(value) => write!(f, "-{}", value), + } + } +} + +impl fmt::Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "N"), + Self::Buy(_) => write!(f, "+{:.2}", self.ratio().unwrap()), + Self::Sell(_) => write!(f, "-{:.2}", self.ratio().unwrap().abs()), + } + } +} + +#[cfg(test)] +mod tests { + use super::{Action, BOUND}; + use crate::core::ValueType; + + #[test] + fn test_action_ratio() { + assert_eq!(Some(1.0), Action::Buy(BOUND).ratio()); + assert_eq!(Some(-1.0), Action::Sell(BOUND).ratio()); + assert_eq!(Some(0.0), Action::Sell(0).ratio()); + assert_eq!(Some(0.0), Action::Buy(0).ratio()); + assert_eq!(Action::Sell(0), Action::Buy(0)); + } + #[test] + fn test_action_from_float() { + let half_bound = if BOUND % 2 == 1 { + BOUND / 2 + 1 + } else { + BOUND / 2 + }; + // f64 + assert_eq!(Action::from(0.0f64), Action::Buy(0)); + assert_eq!(Action::from(-0.5f64), Action::Sell(half_bound)); + assert_eq!(Action::from(1.0f64), Action::BUY_ALL); + assert_eq!(Action::from(-1.0f64), Action::SELL_ALL); + assert_eq!(Action::from(2.0f64), Action::BUY_ALL); + assert_eq!(Action::from(-2.0f64), Action::SELL_ALL); + + // f32 + assert_eq!(Action::from(0.0f32), Action::Buy(0)); + assert_eq!(Action::from(-0.5f32), Action::Sell(half_bound)); + assert_eq!(Action::from(1.0f32), Action::BUY_ALL); + assert_eq!(Action::from(-1.0f32), Action::SELL_ALL); + assert_eq!(Action::from(2.0f32), Action::BUY_ALL); + assert_eq!(Action::from(-2.0f32), Action::SELL_ALL); + + // other + assert_eq!(Action::from(1. / BOUND as ValueType), Action::Buy(1)); + assert_eq!(Action::from(-1. / BOUND as ValueType), Action::Sell(1)); + assert_eq!(Action::from(-2. / BOUND as ValueType), Action::Sell(2)); + } + + #[test] + fn test_action_from_into() { + (1..=BOUND).for_each(|x| { + let action = if x < BOUND { + Action::Buy(x) + } else { + Action::BUY_ALL + }; + let ratio = action.ratio().unwrap(); + let action2: Action = ratio.into(); + + assert!(ratio > 0.); + assert_eq!( + action, + ratio.into(), + "at index {} with action {:?} ratio {}, action#2 {:?}", + x, + action, + ratio, + action2, + ); + + let action = if x < BOUND { + Action::Sell(x) + } else { + Action::SELL_ALL + }; + let ratio = action.ratio().unwrap(); + let action2: Action = ratio.into(); + + assert!(ratio < 0.); + assert_eq!( + action, + ratio.into(), + "at index {} with action {:?} ratio {}, action#2 {:?}", + x, + action, + ratio, + action2, + ); + }); + } + + #[test] + fn test_action_from_float_histogram() { + let half_value = Action::Buy(1).ratio().unwrap() / 2.0; + + (0..=BOUND).for_each(|x| { + let xx = x as ValueType; + assert_eq!(Action::Buy(x), (half_value * 2. * xx).into()); + assert_eq!(Action::Sell(x), (-half_value * 2. * xx).into()); + + if x > 0 { + let y = x - 1; + assert_eq!( + Action::Buy(y), + (half_value * 2. * xx - half_value - 1e-10).into() + ); + assert_eq!( + Action::Sell(y), + (-(half_value * 2. * xx - half_value - 1e-10)).into() + ); + } + }); + + assert_eq!(Action::Buy(1), (half_value * 3. - 1e-10).into()); + assert_eq!(Action::Buy(2), (half_value * 3.).into()); + } + + #[test] + fn test_action_from_i8() { + (i8::MIN..=i8::MAX).for_each(|s| { + let action = Action::from(s); + if s == 0 { + assert_eq!(action, Action::None); + } else if s > 0 { + assert_eq!(action, Action::BUY_ALL); + } else { + assert_eq!(action, Action::SELL_ALL); + } + }); + } + + #[test] + fn test_action_from_i8_optional() { + (i8::MIN..=i8::MAX).for_each(|s| { + let action = Action::from(Some(s)); + if s == 0 { + assert_eq!(action, Action::None); + } else if s > 0 { + assert_eq!(action, Action::BUY_ALL); + } else { + assert_eq!(action, Action::SELL_ALL); + } + }); + } + + #[test] + fn test_action_neg() { + (0..=BOUND).for_each(|x| { + let s = Action::Buy(x); + let b = Action::Sell(x); + + assert_eq!(s, -b); + assert_eq!(-s, b); + }); + } + + #[test] + fn test_action_eq() { + assert_eq!(Action::None, Action::None); + assert_ne!(Action::Buy(0), Action::None); + assert_ne!(Action::Sell(0), Action::None); + assert_eq!(Action::Buy(0), Action::Buy(0)); + assert_eq!(Action::Sell(0), Action::Sell(0)); + assert_eq!(Action::Buy(0), Action::Sell(0)); + assert_eq!(Action::Sell(0), Action::Buy(0)); + assert_ne!(Action::Sell(2), Action::Buy(5)); + assert_ne!(Action::Buy(2), Action::Sell(5)); + assert_ne!(Action::Buy(2), Action::Buy(5)); + assert_eq!(Action::Buy(5), Action::Buy(5)); + assert_ne!(Action::Sell(2), Action::Sell(5)); + assert_eq!(Action::Sell(5), Action::Sell(5)); + } +} diff --git a/src/core/candles.rs b/src/core/candles.rs new file mode 100644 index 0000000..3c3c962 --- /dev/null +++ b/src/core/candles.rs @@ -0,0 +1,131 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use std::str::FromStr; + +use crate::core::{Sequence, ValueType, OHLC, OHLCV}; + +/// Source enum represents common parts of a *Candle* +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Source { + /// *Close* part of a candle + #[cfg_attr(feature = "serde", serde(rename = "close"))] + Close, + + /// *Open* part of a candle + #[cfg_attr(feature = "serde", serde(rename = "open"))] + Open, + + /// *High* part of a candle + #[cfg_attr(feature = "serde", serde(rename = "high"))] + High, + + /// *Low* part of a candle + #[cfg_attr(feature = "serde", serde(rename = "low"))] + Low, + + /// (*High*+*Low*)/2 part of a candle + #[cfg_attr(feature = "serde", serde(rename = "hl2"))] + HL2, + + /// Typical price of a candle + #[cfg_attr(feature = "serde", serde(rename = "tp"))] + TP, + + /// *Volume* part of a candle + #[cfg_attr(feature = "serde", serde(rename = "volume"))] + Volume, +} + +impl FromStr for Source { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().trim() { + "close" => Ok(Self::Close), + "high" => Ok(Self::High), + "low" => Ok(Self::Low), + "volume" => Ok(Self::Volume), + "tp" => Ok(Self::TP), + "hl2" => Ok(Self::HL2), + "open" => Ok(Self::Open), + + _ => Err(format!("Unknown source {}", s)), + } + } +} + +impl From<&str> for Source { + fn from(s: &str) -> Self { + Self::from_str(s).unwrap() + } +} + +impl From for Source { + fn from(s: String) -> Self { + Self::from_str(s.as_str()).unwrap() + } +} + +/// Simple Candlestick structure for implementing [OHLC] and [OHLCV] +/// +/// Can be also used by an alias [Candlestick] +#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Candle { + /// *Open* value of the candle + #[cfg_attr(feature = "serde", serde(rename = "open"))] + pub open: ValueType, + + /// *High* value of the candle + #[cfg_attr(feature = "serde", serde(rename = "high"))] + pub high: ValueType, + + /// *Low* value of the candle + #[cfg_attr(feature = "serde", serde(rename = "low"))] + pub low: ValueType, + + /// *Close* value of the candle + #[cfg_attr(feature = "serde", serde(rename = "close"))] + pub close: ValueType, + + /// *Volume* value of the candle + #[cfg_attr(feature = "serde", serde(rename = "volume"))] + pub volume: ValueType, +} + +/// Just an alias for [Candle] +pub type Candlestick = Candle; + +impl OHLC for Candle { + #[inline] + fn open(&self) -> ValueType { + self.open + } + + #[inline] + fn high(&self) -> ValueType { + self.high + } + + #[inline] + fn low(&self) -> ValueType { + self.low + } + + #[inline] + fn close(&self) -> ValueType { + self.close + } +} + +impl OHLCV for Candle { + #[inline] + fn volume(&self) -> ValueType { + self.volume + } +} + +/// Just an alias for the Sequence of any `T` +pub type Candles = Sequence; diff --git a/src/core/indicator/config.rs b/src/core/indicator/config.rs new file mode 100644 index 0000000..a98c0ec --- /dev/null +++ b/src/core/indicator/config.rs @@ -0,0 +1,41 @@ +use super::IndicatorInstance; +use crate::core::OHLC; + +/// Each indicator has it's own **Configuration** with parameters +/// +/// Each that config should implement `IndicatorConfig` trait +/// +/// See example with [`Example Indicator`](crate::indicators::example) +// Config cannot be Copy because it might consist ov Vec-s. F.e. if indicator using Conv method with custom weights. +pub trait IndicatorConfig: Clone { + /// Validates if **Configuration** is OK + fn validate(&self) -> bool; + + /// Sets dynamically **Configuration** parameters + fn set(&mut self, name: &str, value: String); + + /// Should return `true` if indicator uses *volume* data + fn is_volume_based(&self) -> bool { + false + } + + /// Returns an [IndicatorResult](crate::core::IndicatorResult) size processing by the indicator `(count of raw value, count of signals)` + fn size(&self) -> (u8, u8); +} + +/// To initialize an indicator's **State** indicator should implement `IndicatorInitializer` +pub trait IndicatorInitializer { + /// Type of **State** + type Instance: IndicatorInstance; + + /// Initializes the **State** based on current **Configuration** + fn init(self, initial_value: T) -> Self::Instance; +} + +// pub trait IndicatorConfigDyn: IndicatorConfig { +// fn validate(&self) -> bool; + +// fn set(&mut self, name: &str, value: String); + +// fn init(&self, initial_value: T) -> Box>; +// } diff --git a/src/core/indicator/instance.rs b/src/core/indicator/instance.rs new file mode 100644 index 0000000..6fd3c65 --- /dev/null +++ b/src/core/indicator/instance.rs @@ -0,0 +1,74 @@ +use super::{IndicatorConfig, IndicatorResult}; +use crate::core::{Sequence, OHLC}; + +/// Base trait for implementing indicators **State** +pub trait IndicatorInstance { + // type Config: IndicatorConfig + IndicatorInitializer; + /// Type of Indicator **Configuration** + type Config: IndicatorConfig; + + /// Returns a reference to the indicator **Configuration** + fn config(&self) -> &Self::Config + where + Self: Sized; + + // fn config(&self) -> &dyn IndicatorConfig; + + /// Preceed given candle and returns [`IndicatorResult`](crate::core::IndicatorResult) + fn next(&mut self, candle: T) -> IndicatorResult + where + Self: Sized; + + /// Returns a name of the indicator + fn name(&self) -> &str { + let parts = std::any::type_name::().split("::"); + parts.last().unwrap_or_default() + } + + /// Evaluates the **State** over the given `Sequence` of candles and returns sequence of `IndicatorResult`. + #[inline] + fn over(&mut self, candles: &Sequence) -> Vec + where + Self: Sized, + { + candles.iter().map(|&x| self.next(x)).collect() + } + + /// Returns true if indicator is using volume data + fn is_volume_based(&self) -> bool + where + Self: Sized, + { + self.config().is_volume_based() + } + + /// Returns count of indicator's raw values and count of indicator's signals. + /// + /// See more at [IndicatorConfig](crate::core::IndicatorConfig#tymethod.size) + fn size(&self) -> (u8, u8) + where + Self: Sized, + { + self.config().size() + } +} + +// pub trait IndicatorInstanceDyn: Debug { +// fn config(&self) -> &dyn IndicatorConfigDyn; + +// fn next(&mut self, candle: T) -> IndicatorResult; + +// fn name(&self) -> &str { +// let parts = std::any::type_name::().split("::"); +// parts.last().unwrap_or_default() +// } + +// fn is_volume_based(&self) -> bool { false } + +// #[inline] +// fn over(&mut self, candles: &Sequence) -> Vec { +// candles.iter().map(|&x| self.next(x)).collect() +// } + +// fn size(&self) -> (u8, u8); +// } diff --git a/src/core/indicator/mod.rs b/src/core/indicator/mod.rs new file mode 100644 index 0000000..c5fce33 --- /dev/null +++ b/src/core/indicator/mod.rs @@ -0,0 +1,13 @@ +//! Every indicator has it's own **Configuration** and **State**. +//! +//! Every indicator **Configuration** should implement [IndicatorConfig] and [IndicatorInitializer]. +//! +//! Every indicator **State** should implement [IndicatorInstance]. + +mod config; +mod instance; +mod result; + +pub use config::*; +pub use instance::*; +pub use result::*; diff --git a/src/core/indicator/result.rs b/src/core/indicator/result.rs new file mode 100644 index 0000000..d37b21d --- /dev/null +++ b/src/core/indicator/result.rs @@ -0,0 +1,104 @@ +use crate::core::{Action, ValueType}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Every `Indicator` proceed an input of [OHLC](crate::core::OHLC) or [OHLCV](crate::core::OHLCV) and returns an `IndicatorResult` which consist of some returned raw values and some calculated signals +/// +/// `Indicator` may return up to 4 signals and 4 raw values at each step +#[derive(Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct IndicatorResult { + signals: [Action; IndicatorResult::SIZE], + values: [ValueType; IndicatorResult::SIZE], + length: (u8, u8), +} + +impl IndicatorResult { + /// Size of preallocated result array + /// For the most of cases it should not be used anywhere outside this crate + pub const SIZE: usize = 4; + + /// Returns a slice of signals of current indicator result + pub fn signals(&self) -> &[Action] { + let len = self.length.1 as usize; + &self.signals[..len] + } + + /// Returns a slice of raw indicator values of current indicator result + pub fn values(&self) -> &[ValueType] { + &self.values + } + + /// Returns count of signals + pub fn signals_length(&self) -> u8 { + self.length.1 + } + + /// Returns count of raw values + pub fn values_length(&self) -> u8 { + self.length.0 + } + + /// Returns a tuple of count of raw values and count of signals + pub fn size(&self) -> (u8, u8) { + self.length + } + + /// Returns a raw value at given index + #[inline] + pub fn value(&self, index: usize) -> ValueType { + debug_assert!(index < self.length.0 as usize); + self.values[index] + } + + /// Returns a signal at given index + #[inline] + pub fn signal(&self, index: usize) -> Action { + debug_assert!(index < self.length.1 as usize); + self.signals[index] + } + + /// Creates a new instance of `IndicatorResult` with provided *values* and *signals* + #[inline] + pub fn new(values_slice: &[ValueType], signals_slice: &[Action]) -> Self { + let mut values = [0 as ValueType; Self::SIZE]; + let mut signals = [Action::default(); Self::SIZE]; + + let values_length = Self::SIZE.min(values_slice.len()); + values[..values_length].copy_from_slice(&values_slice[..values_length]); + + let signals_length = Self::SIZE.min(signals_slice.len()); + signals[..signals_length].copy_from_slice(&signals_slice[..signals_length]); + + Self { + values, + signals, + length: (values_length as u8, signals_length as u8), + } + } +} + +impl fmt::Debug for IndicatorResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let values: Vec = self + .values + .iter() + .take(self.length.1 as usize) + .map(|&x| format!("{:>7.4}", x)) + .collect(); + let signals: Vec = self + .signals + .iter() + .take(self.length.1 as usize) + .map(|s| s.to_string()) + .collect(); + write!( + f, + "S: [{:}], V: [{:}]", + signals.join(", "), + values.join(", ") + ) + } +} diff --git a/src/core/method.rs b/src/core/method.rs new file mode 100644 index 0000000..4a20806 --- /dev/null +++ b/src/core/method.rs @@ -0,0 +1,146 @@ +use super::Sequence; +use std::fmt; + +/// Trait for creating methods for timeseries +/// +/// # Regular methods usage +/// +/// ### Iterate over vector's values +/// +/// ``` +/// use yata::methods::SMA; +/// use yata::prelude::*; +/// +/// let s:Vec = vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]; +/// let mut ma = SMA::new(2, s[0]); +/// +/// s.iter().enumerate().for_each(|(index, &value)| { +/// assert_eq!(ma.next(value), (value + s[index.saturating_sub(1)])/2.); +/// }); +/// ``` +/// +/// ### Get a whole new vector over the input vector +/// +/// ``` +/// use yata::methods::SMA; +/// use yata::prelude::*; +/// +/// let s:Vec = vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]; +/// let mut ma = SMA::new(2, s[0]); +/// +/// let result = ma.over(s.iter().copied()); +/// assert_eq!(result.as_slice(), &[1., 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]); +/// ``` +/// +/// ### Change vector values using method +/// +/// ``` +/// use yata::core::Sequence; +/// use yata::methods::SMA; +/// use yata::prelude::*; +/// +/// let mut s:Sequence = Sequence::from(vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]); +/// let mut ma = SMA::new(2, s[0]); +/// +/// s.apply(&mut ma); +/// assert_eq!(s.as_slice(), &[1., 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]); +/// ``` +/// +/// # Be advised +/// There is no `reset` method on the trait. If you need reset a state of the `Method` instance, you should just create a new one. +pub trait Method: fmt::Debug { + /// Method parameters + type Params; + /// Input value type + type Input: Copy; + /// Output value type + type Output: Copy; // = Self::Input; + + /// Static method for creating an instance of the method with given `parameters` and initial `value` (simply first input value) + fn new(parameters: Self::Params, initial_value: Self::Input) -> Self + where + Self: Sized; + + /// Generates next output value based on the given input `value` + fn next(&mut self, value: Self::Input) -> Self::Output; + + /// Returns a name of the method + fn name(&self) -> &str { + let parts = std::any::type_name::().split("::"); + parts.last().unwrap_or_default() + } + + /// Returns memory size of the method `(size, align)` + fn memsize(&self) -> (usize, usize) + where + Self: Sized, + { + (std::mem::size_of::(), std::mem::align_of::()) + } + + /// Creates an `iterator` which produces values by the `Method` over given input data `iterator` + fn iter_data(&mut self, input: I) -> MethodOverIterator + where + I: Iterator, + Self: Sized, + { + MethodOverIterator::new(self, input) + } + + /// Iterates the `Method` over the given `Sequence` and returns timeserie of output values + /// + /// # Guarantees + /// + /// The length of an output `Sequence` is always equal to the length of input one + /// ``` + /// use yata::methods::SMA; + /// use yata::prelude::*; + /// + /// let s:Vec = vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]; + /// let mut ma = SMA::new(5, s[0]); + /// + /// let result = ma.over(s.iter().copied()); + /// assert_eq!(result.len(), s.len()); + /// ``` + /// + /// ``` + /// use yata::methods::SMA; + /// use yata::prelude::*; + /// + /// let s:Vec = vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]; + /// let mut ma = SMA::new(100, s[0]); + /// + /// let result = ma.over(s.iter().copied()); + /// assert_eq!(result.len(), s.len()); + /// ``` + #[inline] + fn over(&mut self, sequence: I) -> Sequence + where + I: Iterator, + Self: Sized, + { + sequence.map(|x| self.next(x)).collect() + } +} + +#[derive(Debug)] +pub struct MethodOverIterator<'a, T: Method, I: Iterator> { + method: &'a mut T, + over: I, +} + +impl<'a, T: Method, I: Iterator> MethodOverIterator<'a, T, I> { + pub fn new(method: &'a mut T, over: I) -> Self { + Self { method, over } + } +} + +impl<'a, T: Method, I: Iterator> Iterator for MethodOverIterator<'a, T, I> { + type Item = T::Output; + + fn next(&mut self) -> Option { + let input = self.over.next()?; + let output = self.method.next(input); + Some(output) + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..63c1fe7 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,79 @@ +// #![warn(missing_docs)] +#![warn(missing_debug_implementations)] +//! Some useful features and definitions + +mod action; +mod candles; +mod indicator; +mod method; +mod ohlcv; +mod sequence; +mod window; + +pub use action::Action; +pub use candles::*; +pub use indicator::*; +pub use method::Method; +pub use ohlcv::{OHLC, OHLCV}; +pub use sequence::Sequence; +pub use window::Window; + +/// Main value type for calculations +/// +/// Default is `f64` +/// +/// If you want use `f32` which is (may be) faster, you can use `cargo build --features value_type_f32` +/// +/// Or in your `cargo.toml`: +/// +/// ```toml +/// [dependencies] +/// yata = { value_type_f32 = true } +/// ``` +/// +/// Read more at [Features section](https://doc.rust-lang.org/cargo/reference/features.html#the-features-section) +/// +/// # See also +/// +/// [PeriodType] +#[cfg(not(feature = "value_type_f32"))] +pub type ValueType = f64; +#[cfg(feature = "value_type_f32")] +pub type ValueType = f32; + +/// PeriodType is a type for using on methods and indicators params. +/// +/// For default it is u8 (from 0 to 255). That means you can use up to `SMA::new(254)`, `WMA::new(254)`, etc... +/// That's right, there are not 255, but 254 (u8::MAX - 1) +/// +/// If you want use larger periods, you can switch it by using crate features: `period_type_u16`, `period_type_u32`, `period_type_u64`. +/// +/// F.e. `cargo build --features period_type_u16` +/// +/// or in your `cargo.toml`: +/// +/// ```toml +/// [dependencies] +/// yata = { period_type_u16 = true } +/// ``` +/// +/// Read more at [Features section](https://doc.rust-lang.org/cargo/reference/features.html#the-features-section) +/// +/// # See also +/// +/// [ValueType] +#[cfg(not(any( + feature = "period_type_u16", + feature = "period_type_u32", + feature = "period_type_u64" +)))] +pub type PeriodType = u8; +#[cfg(all( + feature = "period_type_u16", + not(any(feature = "period_type_u32", feature = "period_type_u64")) +))] +pub type PeriodType = u16; +#[cfg(all(feature = "period_type_u32", not(feature = "period_type_u64")))] +pub type PeriodType = u32; +#[cfg(feature = "period_type_u64")] +pub type PeriodType = u64; diff --git a/src/core/ohlcv.rs b/src/core/ohlcv.rs new file mode 100644 index 0000000..cf29be3 --- /dev/null +++ b/src/core/ohlcv.rs @@ -0,0 +1,249 @@ +use super::{Sequence, Source, ValueType}; +use std::fmt::Debug; + +/// Basic trait for implementing [Open-High-Low-Close timeseries data](https://en.wikipedia.org/wiki/Candlestick_chart) +pub trait OHLC: Copy + Debug + Default { + /// Should return an *open* value of the period + fn open(&self) -> ValueType; + + /// Should return an *highest* value of the period + fn high(&self) -> ValueType; + + /// Should return an *lowest* value of the period + fn low(&self) -> ValueType; + + /// Should return an *close* value of the candle + fn close(&self) -> ValueType; + + /// Calculates [Typical price](https://en.wikipedia.org/wiki/Typical_price). + /// It's just a simple (High + Low + Close) / 3 + /// + /// # Examples + /// + /// ``` + /// use yata::prelude::*; + /// use yata::core::Candle; + /// + /// let candle = Candle { + /// high: 10.0, + /// low: 5.0, + /// close: 9.0, + /// ..Candle::default() + /// }; + /// + /// assert_eq!(candle.tp(), 8.0); + /// ``` + #[inline] + fn tp(&self) -> ValueType { + (self.high() + self.low() + self.close()) / 3. + } + + /// Calculates arithmetic average of `high` and `low` values of the candle + /// + /// # Examples + /// + /// ``` + /// use yata::prelude::*; + /// use yata::core::Candle; + /// + /// let candle = Candle { + /// high: 10.0, + /// low: 5.0, + /// ..Candle::default() + /// }; + /// + /// assert_eq!(candle.hl2(), 7.5); + /// ``` + #[inline] + fn hl2(&self) -> ValueType { + (self.high() + self.low()) * 0.5 + } + + /// CLV = \[(close - low) - (high - close)\] / (high - low) + /// + /// # Examples + /// + /// ``` + /// use yata::prelude::*; + /// use yata::core::Candle; + /// let candle = Candle { + /// high: 5.0, + /// low: 2.0, + /// close: 4.0, + /// ..Candle::default() + /// }; + /// + /// assert_eq!(candle.clv(), ((candle.close()-candle.low()) - (candle.high() - candle.close()))/(candle.high() - candle.low())); + /// assert_eq!(candle.clv(), ((4. - 2.) - (5. - 4.))/(5. - 2.)); + /// ``` + #[inline] + fn clv(&self) -> ValueType { + if self.high() != self.low() { + (2. * self.close() - self.low() - self.high()) / (self.high() - self.low()) + } else { + 0. + } + } + + /// Calculates [True Range](https://en.wikipedia.org/wiki/Average_true_range) over last two candles + /// + /// # Examples + /// + /// ``` + /// use yata::prelude::*; + /// use yata::core::Candle; + /// + /// let candle1 = Candle { + /// close: 70.0, + /// ..Candle::default() + /// }; + /// + /// let candle2 = Candle { + /// high: 100.0, + /// low: 50.0, + /// ..Candle::default() + /// }; + /// + /// let tr = candle2.tr(&candle1); + /// assert_eq!(tr, 50.); + /// ``` + /// + /// ``` + /// use yata::prelude::*; + /// use yata::core::Candle; + /// + /// let candle1 = Candle { + /// close: 30.0, + /// ..Candle::default() + /// }; + /// + /// let candle2 = Candle { + /// high: 100.0, + /// low: 50.0, + /// ..Candle::default() + /// }; + /// + /// let tr = candle2.tr(&candle1); + /// assert_eq!(tr, 70.); + /// ``` + #[inline] + fn tr(&self, prev_candle: &T) -> ValueType { + // Original formula + + // let (a, b, c) = ( + // self.high() - self.low(), + // (self.high() - prev_candle.close()).abs(), + // (prev_candle.close() - self.low()).abs(), + // ); + + // a.max(b).max(c) + + // ----------------------- + // more perfomance + // only 1 subtract operation instead of 3 + self.high().max(prev_candle.close()) - self.low().min(prev_candle.close()) + } + + /// Validates candle attributes + /// + /// Returns `true` if validates OK + /// + /// # Examples + /// + /// ``` + /// use yata::prelude::*; + /// use yata::core::Candle; + /// + /// let candle1 = Candle { + /// open: 7.0, + /// high: 10.0, + /// low: 7.0, + /// close: 11.0, // cannot be more than high + /// + /// ..Candle::default() + /// }; + /// let candle2 = Candle { + /// open: 10.0, + /// high: 10.0, + /// low: 11.0, // low cannot be more than any other value of the candle + /// close: 10.0, + /// + /// ..Candle::default() + /// }; + /// + /// assert!(!OHLC::validate(&candle1)); + /// assert!(!OHLC::validate(&candle2)); + /// ``` + #[inline] + fn validate(&self) -> bool { + !(self.close() > self.high() || self.close() < self.low() || self.high() < self.low()) + && self.close() > 0. + && self.open() > 0. + && self.high() > 0. + && self.low() > 0. + } + + /// Returns [Source] field value of the candle. + /// + /// # Examples + /// + /// ``` + /// use yata::prelude::*; + /// use yata::core::{Candle, Source}; + /// let candle = Candle { + /// open: 12.0, + /// high: 15.0, + /// low: 7.0, + /// close: 10.0, + /// ..Candle::default() + /// }; + /// assert_eq!(OHLCV::source(&candle, Source::Low), 7.0); + /// assert_eq!(OHLCV::source(&candle, "close".to_string().parse().unwrap()), 10.0); + /// ``` + #[inline] + fn source(&self, source: Source) -> ValueType { + match source { + Source::Close => self.close(), + Source::High => self.high(), + Source::Low => self.low(), + Source::TP => self.tp(), + Source::HL2 => self.hl2(), + Source::Open => self.open(), + Source::Volume => panic!("Volume is not implemented for OHLC"), + } + } +} + +/// Basic trait for implementing [Open-High-Low-Close-Volume timeseries data](https://en.wikipedia.org/wiki/Candlestick_chart) +pub trait OHLCV: OHLC { + /// Should return *volume* value for the period + fn volume(&self) -> ValueType; + + /// Validates candle attributes + /// + /// See more at [OHLC#method.validate]. + #[inline] + fn validate(&self) -> bool { + OHLC::validate(self) && self.volume() >= 0. + } + + /// Returns [Source] field value of the candle. + /// + /// See more at [OHLC#method.source]. + #[inline] + fn source(&self, source: Source) -> ValueType { + match source { + Source::Volume => self.volume(), + _ => OHLC::source(self, source), + } + } +} + +impl Sequence { + /// Validates a whole sequence + /// + /// Returns `true` if every candle validates OK + pub fn validate(&self) -> bool { + self.iter().all(|c| T::validate(c)) + } +} diff --git a/src/core/sequence.rs b/src/core/sequence.rs new file mode 100644 index 0000000..208944a --- /dev/null +++ b/src/core/sequence.rs @@ -0,0 +1,97 @@ +#[allow(unused_imports)] +use super::Method; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::iter::FromIterator; +use std::ops::Deref; +use std::ops::DerefMut; + +/// Wrapper for timeseries data vectors +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Sequence(Vec); + +impl Sequence { + /// Creates an empty `Sequence` instance + pub fn empty() -> Self { + Self(Vec::new()) + } + + /// Creates an empty `Sequence` instance with preallocated memory for `len` elements + pub fn new(len: usize) -> Self { + Self(Vec::with_capacity(len)) + } + + /// Changes vector values using method + /// + /// # Examples + /// + /// ``` + /// use yata::core::Sequence; + /// use yata::methods::SMA; + /// use yata::prelude::*; + /// + /// let mut s:Sequence = Sequence::from(vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]); + /// let mut ma = SMA::new(2, s[0]); + /// + /// s.apply(&mut ma); + /// assert_eq!(s.as_slice(), &[1., 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]); + /// ``` + /// + /// ``` + /// use yata::core::Sequence; + /// use yata::helpers::method; + /// + /// let mut s:Sequence = Sequence::from(vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]); + /// let mut ma = method("sma".into(), 2, s[0]); + /// + /// s.apply(ma.as_mut()); + /// assert_eq!(s.as_slice(), &[1., 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]); + /// ``` + pub fn apply

(&mut self, method: &mut dyn Method) { + self.iter_mut().for_each(|x| { + *x = method.next(*x); + }); + } + + /// Evaluates given `method` over this `sequence` and returns new `sequence` filled with method's output values + pub fn eval( + &self, + method: &mut dyn Method, + ) -> Sequence { + self.iter().map(|&x| method.next(x)).collect() + } +} + +impl Deref for Sequence { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Sequence { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for Sequence { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl From> for Vec { + fn from(v: Sequence) -> Self { + v.0 + } +} + +impl FromIterator for Sequence { + fn from_iter>(iter: I) -> Self { + let v: Vec = iter.into_iter().collect(); + Self(v) + } +} diff --git a/src/core/window.rs b/src/core/window.rs new file mode 100644 index 0000000..6dde542 --- /dev/null +++ b/src/core/window.rs @@ -0,0 +1,300 @@ +use super::PeriodType; +use std::mem; +use std::vec; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Some kind of a stack or a buffer of fixed size for remembering timeseries values +/// +/// When push new value into it, it remembers that value and returns the oldest value +/// +/// Also you can [iterate](Window::iter) over remembered values inside the `Window` +/// +/// # Examples +/// ``` +/// use yata::core::Window; +/// +/// let mut w = Window::new(3, 1); // [1, 1, 1] +/// +/// assert_eq!(w.push(2), 1); // [1, 1, 2] +/// assert_eq!(w.push(3), 1); // [1, 2, 3] +/// assert_eq!(w.push(4), 1); // [2, 3, 4] +/// assert_eq!(w.push(5), 2); // [3, 4, 5] +/// assert_eq!(w.push(6), 3); // [4, 5, 6] +/// ``` +/// +/// ``` +/// use yata::core::Window; +/// +/// let mut w = Window::new(3, 0); +/// +/// w.push(1); +/// w.push(2); +/// assert_eq!(w[0], 0); +/// assert_eq!(w[1], 1); +/// assert_eq!(w[2], 2); +/// +/// w.push(3); +/// assert_eq!(w[0], 1); +/// assert_eq!(w[1], 2); +/// assert_eq!(w[2], 3); +/// +/// w.push(4); +/// assert_eq!(w[0], 2); +/// assert_eq!(w[1], 3); +/// assert_eq!(w[2], 4); +/// ``` +/// +/// # See also +/// +/// [Past](crate::methods::Past) +/// +/// [windows](https://doc.rust-lang.org/std/primitive.slice.html#method.windows) +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Window +where + T: Copy, +{ + buf: Vec, + index: PeriodType, + size: PeriodType, + s_1: PeriodType, +} + +impl Window +where + T: Copy, +{ + /// Creates new Window object of size `size` with filled values `value` + pub fn new(size: PeriodType, value: T) -> Self { + debug_assert!(size <= (PeriodType::MAX - 1), "PeriodType overflow"); + Self { + buf: vec![value; size as usize], + index: 0, + size, + s_1: size.saturating_sub(1), + } + } + + /// Creates an empty `Window` instance (no buffer allocated) + pub fn empty() -> Self { + Self { + buf: Vec::new(), + index: 0, + size: 0, + s_1: 0, + } + } + + /// Pushes the `value` into the `Window`. + /// + /// Returns an oldest pushed value. + #[inline] + pub fn push(&mut self, value: T) -> T { + debug_assert!(!self.is_empty(), "Trying to use an empty window"); + + let old_value = mem::replace(&mut self.buf[self.index as usize], value); + + // Next string is branchless version of the code: + // if self.index == self.size - 1 { + // self.index = 0; + // } else { + // self.index += 1; + // } + self.index = (self.index != self.s_1) as PeriodType * (self.index + 1); + + old_value + } + + /// Returns an iterator over the `Window`'s values (by copy) (from the oldest to the newest). + /// + /// # Examples + /// + /// ``` + /// use yata::core::Window; + /// + /// let mut w = Window::new(3, 1); + /// + /// w.push(2); + /// w.push(3); + /// w.push(4); + /// w.push(5); + /// + /// let p: Vec = w.iter().collect(); + /// assert_eq!(p, [3, 4, 5]); + /// ``` + #[inline] + pub fn iter(&self) -> WindowIterator { + WindowIterator::new(&self) + } + + /// Returns an oldest value + #[inline] + pub fn first(&self) -> T { + self.buf[self.index as usize] + } + + /// Returns a last pushed value + /// + /// # Examples + /// + /// ``` + /// use yata::core::Window; + /// let mut w = Window::new(3, 1); + /// + /// assert_eq!(w.last(), 1); + /// w.push(2); + /// assert_eq!(w.last(), 2); + /// w.push(3); + /// assert_eq!(w.last(), 3); + /// w.push(4); + /// assert_eq!(w.last(), 4); + /// w.push(5); + /// assert_eq!(w.last(), 5); + /// w.push(6); + /// assert_eq!(w.last(), 6); + /// ``` + #[inline] + pub fn last(&self) -> T { + let is_zero = self.index == 0; + let index = !is_zero as PeriodType * self.index.saturating_sub(1) + + is_zero as PeriodType * self.s_1; + // let index = if self.index > 0 { + // self.index - 1 + // } else { + // self.s_1 + // }; + self.buf[index as usize] + } + + /// Checks if `Window` is empty (`length` == 0). Returns `true` if `Window` is empty or false otherwise. + pub fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + /// Casts `Window` to a regular vector of `T` + pub fn as_vec(&self) -> &Vec { + &self.buf + } + + /// Casts `Window` as a slice of `T` + pub fn as_slice(&self) -> &[T] { + self.buf.as_slice() + } + + /// Returns the length (elements count) of the `Window` + pub fn len(&self) -> PeriodType { + self.size + } +} + +impl Default for Window +where + T: Copy, +{ + fn default() -> Self { + Self::empty() + } +} + +impl std::ops::Index for Window +where + T: Copy, +{ + type Output = T; + + fn index(&self, index: PeriodType) -> &Self::Output { + debug_assert!(index < self.size, "Window index {:} is out of range", index); + + let saturated = self.index.saturating_add(index); + let overflow = (saturated >= self.size) as PeriodType; + let s = self.size - self.index; + let buf_index = (overflow * index.saturating_sub(s) + (1 - overflow) * saturated) as usize; + + &self.buf[buf_index] + } +} + +impl<'a, T> IntoIterator for &'a Window +where + T: Copy, +{ + type Item = T; + type IntoIter = WindowIterator<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +// impl std::ops::Deref for Window +// where T: Sized + Copy + Default +// { +// type Target = Vec; + +// fn deref(&self) -> &Self::Target { +// &self.buf +// } +// } + +#[derive(Debug)] +pub struct WindowIterator<'a, T> +where + T: Copy, +{ + window: &'a Window, + index: PeriodType, + size: PeriodType, +} + +impl<'a, T> WindowIterator<'a, T> +where + T: Copy, +{ + pub fn new(window: &'a Window) -> Self { + Self { + window, + index: window.index, + size: window.size, + } + } +} + +impl<'a, T> Iterator for WindowIterator<'a, T> +where + T: Copy, +{ + type Item = T; + + fn next(&mut self) -> Option { + if self.size == 0 { + return None; + } + + let value = self.window.buf[self.index as usize]; + + self.size -= 1; + let not_at_end = self.index != self.window.s_1; + self.index = not_at_end as PeriodType * (self.index + 1); + + Some(value) + } + + fn count(self) -> usize { + self.size as usize + } + + fn size_hint(&self) -> (usize, Option) { + let size = self.size as usize; + (size, Some(size)) + } + + fn last(self) -> Option { + Some(self.window.last()) + } +} + +impl<'a, T> ExactSizeIterator for WindowIterator<'a, T> where T: Copy {} +impl<'a, T> std::iter::FusedIterator for WindowIterator<'a, T> where T: Copy {} diff --git a/src/helpers/methods.rs b/src/helpers/methods.rs new file mode 100644 index 0000000..22fd5d4 --- /dev/null +++ b/src/helpers/methods.rs @@ -0,0 +1,270 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Method, PeriodType, ValueType}; +use crate::methods::*; + +use std::str::FromStr; +/// A shortcut for dynamically (runtime) generated regular methods +/// +/// Regular method is a method which has parameters of single [`PeriodType`], input is single [`ValueType`] and output is single [`ValueType`]. +/// +/// # See also +/// +/// [Default regular methods list](RegularMethods) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +pub type RegularMethod = + Box>; + +/// Regular methods dictionary +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum RegularMethods { + /// [Simple Moving Average](crate::methods::SMA) + #[cfg_attr(feature = "serde", serde(rename = "sma"))] + SMA, + + /// [Weighed Moving Average](crate::methods::WMA) + #[cfg_attr(feature = "serde", serde(rename = "wma"))] + WMA, + + /// [Hull Moving Average](crate::methods::HMA) + #[cfg_attr(feature = "serde", serde(rename = "hma"))] + HMA, + + /// [Running Moving Average](crate::methods::RMA) + #[cfg_attr(feature = "serde", serde(rename = "rma"))] + RMA, + + /// [Exponential Moving Average](crate::methods::EMA) + #[cfg_attr(feature = "serde", serde(rename = "ema"))] + EMA, + + /// [Double Exponential Moving Average](crate::methods::DMA) + #[cfg_attr(feature = "serde", serde(rename = "dma"))] + DMA, + + /// Another type of [Double Exponential Moving Average](crate::methods::DEMA) + #[cfg_attr(feature = "serde", serde(rename = "dema"))] + DEMA, + + /// [Triple Exponential Moving Average](crate::methods::TMA) + #[cfg_attr(feature = "serde", serde(rename = "tma"))] + TMA, + + /// Another type of [Triple Exponential Moving Average](crate::methods::DEMA) + #[cfg_attr(feature = "serde", serde(rename = "tema"))] + TEMA, + + /// [Simle Moving Median](crate::methods::SMM) + #[cfg_attr(feature = "serde", serde(rename = "smm"))] + SMM, + + /// [Symmetrically Weighted Moving Average](crate::methods::SWMA) + #[cfg_attr(feature = "serde", serde(rename = "swma"))] + SWMA, + + /// [Linear regression](crate::methods::LinReg) + #[cfg_attr(feature = "serde", serde(rename = "lin_reg"))] + LinReg, + + /// [Past](crate::methods::Past) moves timeseries forward + #[cfg_attr(feature = "serde", serde(rename = "move"))] + Past, + + /// Just an alias for `Past` + #[cfg_attr(feature = "serde", serde(rename = "move"))] + Move, + + /// [Derivative](crate::methods::Derivative) + #[cfg_attr(feature = "serde", serde(rename = "derivative"))] + Derivative, + + /// [Integral](crate::methods::Integral) + #[cfg_attr(feature = "serde", serde(rename = "integral"))] + Integral, + + /// [Standart deviation](crate::methods::StDev) + #[cfg_attr(feature = "serde", serde(rename = "st_dev"))] + StDev, + + /// [Momentum](crate::methods::Momentum) + #[cfg_attr(feature = "serde", serde(rename = "momentum"))] + Momentum, + + /// [Change](crate::methods::Change) + #[cfg_attr(feature = "serde", serde(rename = "momentum"))] + Change, + + /// [Rate Of Change](crate::methods::RateOfChange) + #[cfg_attr(feature = "serde", serde(rename = "rate_of_change"))] + RateOfChange, + + /// Just an alias for [Rate Of Change](crate::methods::RateOfChange) + #[cfg_attr(feature = "serde", serde(rename = "rate_of_change"))] + ROC, + + /// [Highest](crate::methods::Highest) + #[cfg_attr(feature = "serde", serde(rename = "highest"))] + Highest, + + /// [Lowest](crate::methods::Lowest) + #[cfg_attr(feature = "serde", serde(rename = "lowest"))] + Lowest, + + /// [HighestLowestDelta](crate::methods::HighestLowestDelta) + #[cfg_attr(feature = "serde", serde(rename = "highest_lowest_delta"))] + HighestLowestDelta, +} + +impl FromStr for RegularMethods { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().trim() { + "sma" => Ok(Self::SMA), + "wma" => Ok(Self::WMA), + "hma" => Ok(Self::HMA), + "rma" => Ok(Self::RMA), + "ema" => Ok(Self::EMA), + "dma" => Ok(Self::DMA), + "dema" => Ok(Self::DEMA), + "tma" => Ok(Self::TMA), + "tema" => Ok(Self::TEMA), + "smm" => Ok(Self::SMM), + "swma" => Ok(Self::SWMA), + "lin_reg" | "linreg" => Ok(Self::LinReg), + + "past" | "move" => Ok(Self::Past), + "derivative" => Ok(Self::Derivative), + "integral" => Ok(Self::Integral), + "st_dev" | "stdev" => Ok(Self::StDev), + "momentum" | "change" => Ok(Self::Momentum), + "rate_of_change" | "rateofchange" | "roc" => Ok(Self::RateOfChange), + "highest" => Ok(Self::Highest), + "lowest" => Ok(Self::Lowest), + "highest_lowest_delta" => Ok(Self::HighestLowestDelta), + + _ => Err(format!("Unknown regular method name {}", s)), + } + } +} + +impl From<&str> for RegularMethods { + fn from(s: &str) -> Self { + Self::from_str(s).unwrap() + } +} + +impl From for RegularMethods { + fn from(s: String) -> Self { + Self::from_str(s.as_str()).unwrap() + } +} + +/// Returns a heap-allocated [RegularMethod] for timeseries by given `name` and window `length`. +/// These methods are always gets an input value of type f64 and the same output value type +/// +/// Available methods: +/// * `sma` - [simple moving average](SMA) +/// * `wma` - [weighed moving average](WMA) +/// * `hma` - [hull moving average](HMA) +/// * `ema` - [exponential moving average](EMA) +/// * `rma` - [running moving average](RMA) +/// * `dma` - [double exponential moving average](DMA) +/// * `dema` - [another double exponential moving average](DEMA) +/// * `tma` - [triple exponential moving average](TMA) +/// * `tema` - [another triple exponential moving average](TEMA) +/// * `smm` - [simple moving median](SMM) +/// * `swma` - [symmetrically weighted moving average](SWMA) +/// * `lin_reg` - [linear regression moving average](LinReg) +/// * `past`, `move` - [moves timeseries forward](Past) +/// * `derivative` - [derivative](Derivative) +/// * `st_dev` - [standart deviation](StDev) +/// * `momentum`, `change` - [absolute change of values](Momentum) +/// * `rate_of_change` - [relative change of values](RateOfChange) +/// * [`highest`](Highest), [`lowest`](Lowest), [`highest_lowest_delta`](HighestLowestDelta) +/// +/// # Examples +/// +/// ``` +/// use yata::helpers::{method, RegularMethods}; +/// +/// let mut m = method(RegularMethods::SMA, 3, 1.0); +/// +/// m.next(1.0); +/// m.next(2.0); +/// +/// assert_eq!(m.next(3.0), 2.0); +/// assert_eq!(m.next(4.0), 3.0); +/// ``` +/// +/// ``` +/// use yata::core::Sequence; +/// use yata::helpers::{method, RegularMethods}; +/// +/// let mut s:Sequence = Sequence::from(vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]); +/// let mut ma = method("sma".into(), 2, s[0]); +/// +/// s.apply(ma.as_mut()); +/// assert_eq!(s.as_slice(), &[1., 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]); +/// ``` +/// +/// ``` +/// use yata::prelude::*; +/// use yata::core::{Sequence, ValueType}; +/// use yata::methods::WMA; +/// use yata::helpers::method; +/// +/// let my_method = String::from("wma"); +/// let mut s:Sequence = Sequence::from(vec![1.,2.,3.,4.,5.,6.,7.,8.,9.,10.]); +/// let mut wma1 = method(my_method.into(), 4, s[0]); +/// let mut wma2 = WMA::new(4, s[0]); +/// +/// let s1:Vec = s.iter().map(|&x| wma1.next(x)).collect(); +/// let s2:Vec = wma2.iter_data(s.iter().copied()).collect(); +/// assert_eq!(s1.as_slice(), s2.as_slice()); +/// ``` +/// +/// # See also +/// +/// [Default regular methods list](RegularMethods) + +pub fn method( + method: RegularMethods, + length: PeriodType, + initial_value: ValueType, +) -> RegularMethod { + match method { + RegularMethods::SMA => Box::new(SMA::new(length, initial_value)), + RegularMethods::WMA => Box::new(WMA::new(length, initial_value)), + RegularMethods::HMA => Box::new(HMA::new(length, initial_value)), + RegularMethods::RMA => Box::new(RMA::new(length, initial_value)), + RegularMethods::EMA => Box::new(EMA::new(length, initial_value)), + RegularMethods::DMA => Box::new(DMA::new(length, initial_value)), + RegularMethods::DEMA => Box::new(DEMA::new(length, initial_value)), + RegularMethods::TMA => Box::new(TMA::new(length, initial_value)), + RegularMethods::TEMA => Box::new(TEMA::new(length, initial_value)), + RegularMethods::SMM => Box::new(SMM::new(length, initial_value)), + RegularMethods::SWMA => Box::new(SWMA::new(length, initial_value)), + RegularMethods::LinReg => Box::new(LinReg::new(length, initial_value)), + + RegularMethods::Past | RegularMethods::Move => Box::new(Past::new(length, initial_value)), + RegularMethods::Derivative => Box::new(Derivative::new(length, initial_value)), + RegularMethods::Integral => Box::new(Integral::new(length, initial_value)), + RegularMethods::StDev => Box::new(StDev::new(length, initial_value)), + RegularMethods::Momentum | RegularMethods::Change => { + Box::new(Momentum::new(length, initial_value)) + } + RegularMethods::RateOfChange | RegularMethods::ROC => { + Box::new(RateOfChange::new(length, initial_value)) + } + RegularMethods::Highest => Box::new(Highest::new(length, initial_value)), + RegularMethods::Lowest => Box::new(Lowest::new(length, initial_value)), + RegularMethods::HighestLowestDelta => { + Box::new(HighestLowestDelta::new(length, initial_value)) + } + } +} diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs new file mode 100644 index 0000000..6258474 --- /dev/null +++ b/src/helpers/mod.rs @@ -0,0 +1,134 @@ +#![warn(missing_docs, missing_debug_implementations)] +//! Additional helping primitives +//! + +mod methods; +use crate::core::{Candle, ValueType}; +pub use methods::*; + +/// sign is like [f64.signum](https://doc.rust-lang.org/std/primitive.f64.html#method.signum) +/// except when value == 0.0, then sign returns 0.0 +/// +/// See also [signi] +/// +/// # Examples +/// +/// ``` +/// use yata::helpers::sign; +/// +/// assert_eq!(sign(4.65), 1.0); +/// assert_eq!(sign(-25.6), -1.0); +/// assert_eq!(sign(0.0), 0.0); +/// assert_eq!(sign(-0.0), 0.0); +/// assert_eq!(sign(0.000001), 1.0); +/// ``` +#[inline] +pub fn sign(value: ValueType) -> ValueType { + // if value > 0. { + // 1. + // } else if value < 0. { + // -1. + // } else { + // 0. + // } + ((value > 0.) as i8 - (value < 0.) as i8) as ValueType +} + +/// signi is like [f64.signum](https://doc.rust-lang.org/std/primitive.f64.html#method.signum), except 2 things +/// - when value == 0.0, then signi returns 0 +/// - signi always returns i8 +/// +/// See also [sign] +/// +/// # Examples +/// +/// ``` +/// use yata::helpers::signi; +/// +/// assert_eq!(signi(4.65), 1); +/// assert_eq!(signi(-25.6), -1); +/// assert_eq!(signi(0.0), 0); +/// assert_eq!(signi(-0.0), 0); +/// assert_eq!(signi(0.000001), 1); +/// assert_eq!(signi(-0.000001), -1); +/// ``` +#[inline] +pub fn signi(value: ValueType) -> i8 { + // if value > 0. { + // 1 + // } else if value < 0. { + // -1 + // } else { + // 0 + // } + + (value > 0.) as i8 - (value < 0.) as i8 +} + +/// Random Candles iterator for testing purposes +#[derive(Debug, Clone, Copy)] +pub struct RandomCandles(u16); + +impl RandomCandles { + const DEFAULT_PRICE: ValueType = 1.0; + const DEFAULT_VOLUME: ValueType = 10.0; + + /// Returns new instance of RandomCandles for testing purposes + pub fn new() -> Self { + Self::default() + } + + /// Returns very first candle in the sequence + pub fn first(&mut self) -> Candle { + let position = self.0; + self.0 = 0; + let candle = self.next().unwrap(); + self.0 = position; + + candle + } +} + +impl Default for RandomCandles { + fn default() -> Self { + Self(0) + } +} + +impl Iterator for RandomCandles { + type Item = Candle; + + fn next(&mut self) -> Option { + let prev_position = self.0.wrapping_sub(1) as ValueType; + let position = self.0 as ValueType; + + let close = Self::DEFAULT_PRICE + position.sin() / 2.; + let open = Self::DEFAULT_PRICE + prev_position.sin() / 2.; + + let high = close.max(open) + (position * 1.4).tan().abs(); + let low = close.min(open) - (position * 0.8).cos().abs() / 3.; + let volume = Self::DEFAULT_VOLUME * (position / 2.).sin() + Self::DEFAULT_VOLUME / 2.; + + let candle = Self::Item { + // candle: Candle { + open: open, + high: high, + low: low, + close: close, + volume: volume, + // }, + // timestamp: position as i64, + // ..Self::Item::default() + }; + + self.0 = self.0.wrapping_sub(1); + Some(candle) + } + + fn nth(&mut self, n: usize) -> Option { + self.0 = n as u16; + self.0 = self.0.wrapping_sub(1); + + self.next() + } +} diff --git a/src/indicators/aroon.rs b/src/indicators/aroon.rs new file mode 100644 index 0000000..465c1a1 --- /dev/null +++ b/src/indicators/aroon.rs @@ -0,0 +1,418 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{ + Action, IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult, +}; +use crate::core::{PeriodType, ValueType, Window, OHLC}; + +/* +N = 14 #common values: 14, 25, с каким периодом будет сравниваться значения +signalZone = 0.3 #сигналом будет считать показания выше 0.7 (70) и ниже 0.3 (30) + +При базовом отображении индикатор Aroon колеблется в диапазоне между 0 и 100. +Нахождение линий в верхней части (70-100) говорит о частом обновлении соответствующих экстремумов. +Нахождение линий в нижней части (0-30) шкалы свидетельствует о редком обновлении экстремумов. +Считается, что на рынке преобладают покупатели, если «верхняя» (зеленая) линия +находится выше 50, а «нижняя» (красная) линия находится ниже 50. При медвежьих +настроениях возникает противоположная ситуация: зеленая линия находится ниже 50, +а красная выше 50. Приближение одной из линий индикатора Aroon к 100 при падении +другой ниже 30 может быть признаком начала тренда. + +Если индикатор Aroon используется в виде осциллятора, то он будет представлять +собой одну линию, колеблющуюся в диапазоне от -100 до +100. Если осциллятор выше 0, +то «верхняя» (зеленая) линия базового индикатора находится над «нижней» (красной) линией. +Отрицательные значения осциллятора будут говорить о противоположной ситуации. +Значения осциллятора можно использовать для определения силы тренда. + +Индикатор Aroon для определения начала нового тренда + +Базовый индикатор Aroon можно использовать для определения начала тренда. +Признаком начала нового тренда будет пересечение линий индикатора, их закрепление +по разные стороны от центральной линии и достижение соответствующей линией значения 100. +Например, для выявления начала восходящего движения должно произойти следующее: + +*/ + +// Нерабочая версия +// Проверить и исправить +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Aroon { + pub signal_zone: ValueType, + pub n: PeriodType, +} + +impl IndicatorConfig for Aroon { + fn validate(&self) -> bool { true } + + fn set(&mut self, name: &str, value: String) { + match name { + "signal_zone" => self.signal_zone = value.parse().unwrap(), + "n" => self.n = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { (3, 3) } +} + +impl IndicatorInitializer for Aroon { + type Instance = AroonInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + i: 0, + max_value: candle.high(), + min_value: candle.low(), + max_index: 0, + min_index: 0, + candle, + index: 0, + n_1: cfg.n - 1, + invert_n: (cfg.n as ValueType).recip(), + window: Window::new(cfg.n, candle), + cfg, + } + } +} + +impl Default for Aroon { + fn default() -> Self { + Self { + signal_zone: 0.3, + n: 14, + } + } +} + +#[derive(Debug, Default)] +pub struct AroonInstance { + cfg: Aroon, + + i: PeriodType, + max_index: PeriodType, + max_value: ValueType, + min_index: PeriodType, + min_value: ValueType, + candle: T, + index: PeriodType, + n_1: PeriodType, + invert_n: ValueType, + window: Window, +} + +impl IndicatorInstance for AroonInstance { + type Config = Aroon; + + fn name(&self) -> &str { "Aroon" } + + #[inline] + fn config(&self) -> &Self::Config { &self.cfg } + + #[allow(unreachable_code, unused_variables)] + fn next(&mut self, candle: T) -> IndicatorResult { + todo!("Некорректная реализация"); + + self.max_index += 1; + self.min_index += 1; + + self.window.push(candle); + + let length = self.n_1; + if self.max_index > length || self.min_index > length { + let first = self.window.first(); + let mut max_index = self.cfg.n - 1; + let mut min_index = self.cfg.n - 1; + let mut max_value = first.high(); + let mut min_value = first.low(); + + self.window + .iter() + .enumerate() /*.skip(1)*/ + .for_each(|(i, c)| { + let j = self.cfg.n - (i as PeriodType) - 1; + if c.high() >= max_value { + max_index = j; + max_value = candle.high(); // Ошибка?????? Мб. c.high()??? + } + + if c.low() <= min_value { + min_index = j; + min_value = candle.low(); // Ошибка ????? мб c.low()???? + } + }); + + if self.min_index > length { + self.min_value = min_value; + self.min_index = min_index; + } + + if self.max_index > length { + self.max_value = max_value; + self.max_index = max_index; + } + + print!("{}:{}={}\n", self.min_index, self.i, self.min_value); + } else { + if candle.high() >= self.max_value { + self.max_index = 0; + self.max_value = candle.high(); + } + + if candle.low() <= self.min_value { + self.min_index = 0; + self.min_value = candle.low(); + } + } + + let aroon_u = (self.cfg.n - self.max_index) as ValueType * self.invert_n; + let aroon_d = (self.cfg.n - self.min_index) as ValueType * self.invert_n; + let aroon_o = aroon_u - aroon_d; + + let (mut u, mut d) = (0i8, 0i8); + + if aroon_u > 1. - self.cfg.signal_zone { + u += 1; + } + + if aroon_u < self.cfg.signal_zone { + u -= 1; + } + + if aroon_d > 1.0 - self.cfg.signal_zone { + d += 1; + } + + if aroon_d < self.cfg.signal_zone { + d -= 1; + } + + let o = (aroon_o - 0.5).ceil() as i8; + + self.i += 1; + let values = [aroon_u, aroon_d, aroon_o]; + let signals = [Action::from(o), Action::from(u), Action::from(d)]; + + IndicatorResult::new(&values, &signals) + + // NextEntry::from([ + // Entry { + // value: aroon_u, + // signal: o, + // }, + // Entry { + // value: aroon_d, + // signal: u, + // }, + // Entry { + // value: aroon_o, + // signal: d, + // }, + // ]) + } +} + +// // extern crate trading_core; +// +// use serde::{Serialize, Deserialize}; +// +// //use crate::core::{ Candles, IndicatorInstance, Sequence, Signal, SignalType }; +// use crate::core::{ Candles, IndicatorInstance, Value, Signal, ValueType, SignalType }; +// /* +// N = 14 #common values: 14, 25, с каким периодом будет сравниваться значения +// signalZone = 0.3 #сигналом будет считать показания выше 0.7 (70) и ниже 0.3 (30) +// +// При базовом отображении индикатор Aroon колеблется в диапазоне между 0 и 100. +// Нахождение линий в верхней части (70-100) говорит о частом обновлении соответствующих экстремумов. +// Нахождение линий в нижней части (0-30) шкалы свидетельствует о редком обновлении экстремумов. +// Считается, что на рынке преобладают покупатели, если «верхняя» (зеленая) линия +// находится выше 50, а «нижняя» (красная) линия находится ниже 50. При медвежьих +// настроениях возникает противоположная ситуация: зеленая линия находится ниже 50, +// а красная выше 50. Приближение одной из линий индикатора Aroon к 100 при падении +// другой ниже 30 может быть признаком начала тренда. +// +// Если индикатор Aroon используется в виде осциллятора, то он будет представлять +// собой одну линию, колеблющуюся в диапазоне от -100 до +100. Если осциллятор выше 0, +// то «верхняя» (зеленая) линия базового индикатора находится над «нижней» (красной) линией. +// Отрицательные значения осциллятора будут говорить о противоположной ситуации. +// Значения осциллятора можно использовать для определения силы тренда. +// +// Индикатор Aroon для определения начала нового тренда +// +// Базовый индикатор Aroon можно использовать для определения начала тренда. +// Признаком начала нового тренда будет пересечение линий индикатора, их закрепление +// по разные стороны от центральной линии и достижение соответствующей линией значения 100. +// Например, для выявления начала восходящего движения должно произойти следующее: +// +// */ +// +// #[derive(Debug)] +// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +// pub struct Aroon { +// pub signal_zone: ValueType, +// pub n: usize, +// } +// +// // Функцию new нельзя вынести в Trait, потому что иначе отказывается генерировать indicator через функцию get_indicator -> Box +// impl Aroon { +// pub fn new() -> Self where Self: Sized { +// Self::default() +// } +// } +// +// impl Default for Aroon { +// fn default() -> Self where Self: Sized { +// Self { +// signal_zone: 0.3, +// n: 14, +// } +// } +// } +// +// impl IndicatorInstance for Aroon { +// fn new() -> Self where Self: Sized { +// Self::default() +// } +// fn value(&self, candles: &Candles) -> Vec { +// let first_candle = match candles.first() { +// Some(candle) => candle, +// None => { +// return Vec::with_capacity(0); +// } +// }; +// +// let mut max_index:isize = 0; +// let mut min_index:isize = 0; +// let mut max_value = first_candle.high(); +// let mut min_value = first_candle.low(); +// +// let n = self.n as isize; +// let n_1 = n-1; +// let invert_n = (n as ValueType).recip(); +// +// let mut result = Vec::::with_capacity(3); +// for _ in 0..3 { +// result.push(Value::new(candles.len())); +// } +// +// for (index, candle) in candles.iter().enumerate() { +// let iindex = index as isize; +// let first_index = iindex - n_1; //включая текущую свечу +// +// if (max_index < first_index) || (min_index < first_index) { +// if max_index < first_index { +// max_index = first_index; +// max_value = candles[first_index].high(); +// } +// +// if min_index < first_index { +// min_index = first_index; +// min_value = candles[first_index].low(); +// } +// +// for i in (first_index + 1)..(iindex+1) { +// if candles[i].high() >= max_value { +// max_index = i; +// max_value = candle.high(); +// } +// +// if candles[i].low() <= min_value { +// min_index = i; +// min_value = candle.low(); +// } +// } +// } else { +// if candle.high() >= max_value { +// max_index = iindex; +// max_value = candle.high(); +// } +// +// if candle.low() <= min_value { +// min_index = iindex; +// min_value = candle.low(); +// } +// } +// +// let aroon_u = (n-(iindex-max_index)) as ValueType * invert_n; +// let aroon_d = (n-(iindex-min_index)) as ValueType * invert_n; +// +// let aroon_o = aroon_u - aroon_d; +// +// // r[0][index], r[1][index], r[2][index] = AroonU, AroonD, AroonO +// result[0].push(aroon_u); +// result[1].push(aroon_d); +// result[2].push(aroon_o); +// } +// +// result +// } +// +// fn signal(&self, candles: &Candles) -> Vec { +// let zone = self.signal_zone; +// let mut result = Vec::::with_capacity(3); +// +// for _ in 0..3 { +// result.push(Signal::new(candles.len())); +// } +// +// let values = self.value(candles); +// +// for index in 0..candles.len() { +// let _u = values[0][index]; +// let _d = values[1][index]; +// let o = values[2][index]; +// +// let mut u:SignalType = 0; +// let mut d:SignalType = 0; +// +// if _u > 1.-zone { +// u+=1; +// } +// +// if _u < zone { +// u-=1; +// } +// +// if _d > 1.0-zone { +// d+=1; +// } +// +// if _d < zone { +// d-=1; +// } +// +// result[0].push( (o - 0.5).ceil() as SignalType ); //int8(math.Ceil(o - 0.5))); +// result[1].push(u); +// result[2].push(d); +// } +// +// result +// } +// +// fn name(&self) -> &str { "Aroon" } +// +// fn set(&mut self, name: &str, value: String) { +// match name { +// "n" => self.n = value.parse().unwrap(), +// "period1" => self.n = value.parse().unwrap(), +// "signal_zone" => self.signal_zone = String::from(value).parse().unwrap(), +// +// _ => { +// dbg!(format!("Unknown attribute `{:}` with value `{:}` for `{:}`", name, value, std::any::type_name::(),)); +// }, +// } +// } +// } diff --git a/src/indicators/average_directional_index.rs b/src/indicators/average_directional_index.rs new file mode 100644 index 0000000..0a836e2 --- /dev/null +++ b/src/indicators/average_directional_index.rs @@ -0,0 +1,186 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, PeriodType, ValueType, Window, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::{method, RegularMethod, RegularMethods}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct AverageDirectionalIndex { + pub method1: RegularMethods, + pub di_length: PeriodType, + + pub method2: RegularMethods, + pub adx_smoothing: PeriodType, + + pub period1: PeriodType, + pub zone: ValueType, +} + +impl IndicatorConfig for AverageDirectionalIndex { + fn validate(&self) -> bool { + self.di_length >= 1 + && self.adx_smoothing >= 1 + && self.zone >= 0. + && self.zone <= 1. + && self.period1 >= 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "method1" => self.method1 = value.parse().unwrap(), + "di_length" => self.di_length = value.parse().unwrap(), + + "method2" => self.method2 = value.parse().unwrap(), + "adx_smoothing" => self.adx_smoothing = value.parse().unwrap(), + + "period1" => self.period1 = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (3, 1) + } +} + +impl IndicatorInitializer for AverageDirectionalIndex { + type Instance = AverageDirectionalIndexInstance; + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let tr = candle.tr(&candle); + + Self::Instance { + prev_candle: candle, + window: Window::new(cfg.period1, candle), + tr_ma: method(cfg.method1, cfg.di_length, tr), + plus_dm: method(cfg.method1, cfg.di_length, 0.0), + minus_dm: method(cfg.method1, cfg.di_length, 0.0), + ma2: method(cfg.method2, cfg.adx_smoothing, 0.0), + cfg, + } + } +} + +impl Default for AverageDirectionalIndex { + fn default() -> Self { + Self { + method1: RegularMethods::RMA, + di_length: 14, + method2: RegularMethods::RMA, + adx_smoothing: 14, + period1: 1, + zone: 0.2, + } + } +} + +#[derive(Debug)] +pub struct AverageDirectionalIndexInstance { + cfg: AverageDirectionalIndex, + + prev_candle: T, + window: Window, + tr_ma: RegularMethod, + plus_dm: RegularMethod, + minus_dm: RegularMethod, + ma2: RegularMethod, +} + +impl AverageDirectionalIndexInstance { + fn dir_mov(&mut self, candle: T) -> (ValueType, ValueType) { + let tr_ma = &mut self.tr_ma; + let plus_dm = &mut self.plus_dm; + let minus_dm = &mut self.minus_dm; + + let true_range = tr_ma.next(candle.tr(&self.prev_candle)); + let left_candle = self.window.push(candle); + + // prevIndex = zeroIndex(index - int(a.Period1)) + // prevCandle = a.candles[prevIndex] + + let (du, dd) = ( + candle.high() - left_candle.high(), + left_candle.low() - candle.low(), + ); + + let plus_dm_value = if du > dd && du > 0. { + plus_dm.next(du) + } else { + plus_dm.next(0.) + }; + + let minus_dm_value = if dd > du && dd > 0. { + minus_dm.next(dd) + } else { + minus_dm.next(0.) + }; + + self.prev_candle = candle; + + (plus_dm_value / true_range, minus_dm_value / true_range) + } + + fn adx(&mut self, plus: ValueType, minus: ValueType) -> ValueType { + let s = plus + minus; + + let ma2 = &mut self.ma2; + + if s == 0. { + return ma2.next(0.); + } + + let t = (plus - minus).abs() / s; + ma2.next(t) + } +} + +impl IndicatorInstance for AverageDirectionalIndexInstance { + type Config = AverageDirectionalIndex; + + fn name(&self) -> &str { + "AverageDirectionalIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let (plus, minus) = self.dir_mov(candle); + let adx = self.adx(plus, minus); + + // let signal: i8 = if adx > self.cfg.zone { + // if plus > minus { + // 1 + // } else if plus < minus { + // -1 + // } else { + // 0 + // } + // } else { + // 0 + // }; + + let signal = (adx > self.cfg.zone) as i8 * ((plus > minus) as i8 - (plus < minus) as i8); + + let values = [adx, plus, minus]; + let signals = [Action::from(signal)]; + + IndicatorResult::new(&values, &signals) + } +} diff --git a/src/indicators/awesome_oscillator.rs b/src/indicators/awesome_oscillator.rs new file mode 100644 index 0000000..ef98842 --- /dev/null +++ b/src/indicators/awesome_oscillator.rs @@ -0,0 +1,184 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, ValueType, Window, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::{method, signi, RegularMethod, RegularMethods}; +use crate::methods::{Cross, PivotHighSignal, PivotLowSignal}; + +/* +Билл Вильямс выделил три возможных варианта сигнала на покупку (+ три возможных +сигнала на продажу полностью противоположные сигналам на покупку) которые создает awesome oscillator. + +1. «Блюдце» — это единственный сигнал на покупку, который возникает, когда +гистограмма awesome oscillator находится выше нулевой линии. Для образования +сигнала «Блюдце» необходимо, по крайней мере, три столбца гистограммы. «Блюдце» +образуется, когда гистограмма меняет направление с нисходящего на восходящее, +т.е. у 1-го будет большее значение чем у 2-го, у 2-го меньшее чем у 1-го (красный столбец), +у 3-го больше чем у 2-го (зеленый столбец). При этом все столбцы гистограммы +awesome oscillator должны быть выше нулевой линии. + +сигнал awesome oscillator - Блюдце + +2. «Пересечение нулевой линии» — сигнал на покупку образуется, когда гистограмма +awesome oscillator переходит от отрицательных значений к положительным значениям. +Это происходит тогда, когда гистограмма пересекает нулевую линию. При наличии сигнала +к покупке «Пересечение нулевой линии», сигнальный столбец гистограммы всегда будет +зеленого цвета. + +сигнал awesome oscillator - Пересечение нулевой линии + +3. «Два Пика» — сигнал на покупку образуется, когда у вас есть направленный вниз +пик (самый низкий минимум), находящийся ниже нулевой линии awesome oscillator, +за которым следует другой направленный вниз пик, который выше (отрицательное число, +меньшее по абсолютному значению, поэтому оно находится ближе к нулевой линии), чем +предыдущий пик, смотрящий вниз. Гистограмма должна находиться ниже нулевой линии +между двумя пиками. Если гистограмма пересекает нулевую линию между пиками, сигнал +на покупку не действует. Однако создается сигнал на покупку «Пересечение нулевой линии». +Если формируется дополнительный, более высокий пик (который ближе к нулевой линии) и +гистограмма не пересекла нулевую линию, то образуется дополнительный сигнал на покупку. +Сигнальный столбец гистограммы должен быть зеленого цвета. + +Если столбец гистограммы awesome oscillator зеленого цвета, сигнала на продажу не может +быть. Если он красного цвета, то у вас не может быть сигнала на покупку по awesome +oscillator. Другой важный момент заключается в том, что если сигнал на покупку или +продажу образован, но не преодолевается текущим ценовым баром, затем столбец гистограммы +меняет цвет, этот сигнал аннулируется. +*/ + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct AwesomeOscillator { + pub period1: PeriodType, + pub period2: PeriodType, + pub method: RegularMethods, + pub left: PeriodType, + pub right: PeriodType, +} + +impl IndicatorConfig for AwesomeOscillator { + fn validate(&self) -> bool { + self.period1 > self.period2 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + "left" => self.left = value.parse().unwrap(), + "right" => self.right = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 2) + } +} + +impl IndicatorInitializer for AwesomeOscillator { + type Instance = AwesomeOscillatorInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let hl2 = candle.hl2(); + + Self::Instance { + ma1: method(cfg.method, cfg.period1, hl2), + ma2: method(cfg.method, cfg.period2, hl2), + cross_over: Cross::default(), + ph: Method::new((cfg.left, cfg.right), 0.0), + pl: Method::new((cfg.left, cfg.right), 0.0), + window: Window::new(cfg.right, 0.), + cfg, + } + } +} + +impl Default for AwesomeOscillator { + fn default() -> Self { + Self { + period1: 34, + period2: 5, + method: RegularMethods::SMA, + left: 1, + right: 1, + } + } +} + +#[derive(Debug)] +pub struct AwesomeOscillatorInstance { + cfg: AwesomeOscillator, + + ma1: RegularMethod, + ma2: RegularMethod, + cross_over: Cross, + ph: PivotHighSignal, + pl: PivotLowSignal, + window: Window, +} + +impl IndicatorInstance for AwesomeOscillatorInstance { + type Config = AwesomeOscillator; + + fn name(&self) -> &str { + "AwesomeOscillator" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let hl2 = candle.hl2(); + + let ma1 = &mut self.ma1; + let ma2 = &mut self.ma2; + let value = ma2.next(hl2) - ma1.next(hl2); + + let s2 = self.cross_over.next((value, 0.)); + + let ph: i8 = self.ph.next(value).into(); + let pl: i8 = self.pl.next(value).into(); + + let last_value = self.window.push(value); //self.window.first(); + let sign = signi(last_value); + + // let mut m_up = pl * sign; + // let mut m_down = ph * sign; + + // if m_up < 0 { + // m_up = 0; + // } + + // if m_down > 0 { + // m_down = 0; + // } + + // let s1 = m_up + m_down; + + let m_up = ((pl * sign) > 0) as i8; + let m_down = ((ph * sign) < 0) as i8; + + let s1 = m_up - m_down; + + let values = [value]; + let signals = [Action::from(s1), s2]; + + IndicatorResult::new(&values, &signals) + } +} diff --git a/src/indicators/bollinger_bands.rs b/src/indicators/bollinger_bands.rs new file mode 100644 index 0000000..9d1e01a --- /dev/null +++ b/src/indicators/bollinger_bands.rs @@ -0,0 +1,111 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, Source, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::{StDev, SMA}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct BollingerBands { + pub avg_size: PeriodType, + pub sigma: ValueType, + pub source: Source, +} + +impl IndicatorConfig for BollingerBands { + fn validate(&self) -> bool { + self.sigma >= 0.0 && self.avg_size > 2 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "avg_size" => self.avg_size = value.parse().unwrap(), + "sigma" => self.sigma = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (3, 1) + } +} + +impl IndicatorInitializer for BollingerBands { + type Instance = BollingerBandsInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = T::source(&candle, cfg.source); + Self::Instance { + ma: SMA::new(cfg.avg_size, src), + st_dev: StDev::new(cfg.avg_size, src), + cfg, + } + } +} + +impl Default for BollingerBands { + fn default() -> Self { + Self { + avg_size: 20, + sigma: 2.0, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct BollingerBandsInstance { + cfg: BollingerBands, + + ma: SMA, + st_dev: StDev, +} + +impl IndicatorInstance for BollingerBandsInstance { + type Config = BollingerBands; + + fn name(&self) -> &str { + "BollingerBands" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let source = candle.source(self.cfg.source); + let middle = self.ma.next(source); + let sq_error = self.st_dev.next(source); + + let upper = middle + sq_error * self.cfg.sigma; + let lower = middle - sq_error * self.cfg.sigma; + + // let signal = if source >= upper { + // 1 + // } else if source <= lower { + // -1 + // } else { + // 0 + // }; + let signal = (source >= upper) as i8 - (source <= lower) as i8; + + let values = [upper, middle, lower]; + let signals = [Action::from(signal)]; + IndicatorResult::new(&values, &signals) + } +} diff --git a/src/indicators/chaikin_money_flow.rs b/src/indicators/chaikin_money_flow.rs new file mode 100644 index 0000000..e2cf34c --- /dev/null +++ b/src/indicators/chaikin_money_flow.rs @@ -0,0 +1,101 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, ValueType, Window, OHLCV}; +use crate::methods::{Cross, ADI}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ChaikinMoneyFlow { + pub size: PeriodType, + // phantom: PhantomData, +} + +impl IndicatorConfig for ChaikinMoneyFlow { + fn validate(&self) -> bool { + self.size > 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "size" => self.size = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn is_volume_based(&self) -> bool { + true + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for ChaikinMoneyFlow { + type Instance = ChaikinMoneyFlowInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + adi: ADI::new(cfg.size, candle), + vol_sum: candle.volume() * cfg.size as ValueType, + window: Window::new(cfg.size, candle.volume()), + cross_over: Cross::default(), + cfg, + } + } +} + +impl Default for ChaikinMoneyFlow { + fn default() -> Self { + Self { + size: 20, + // phantom: PhantomData::default(), + } + } +} + +#[derive(Debug)] +pub struct ChaikinMoneyFlowInstance { + cfg: ChaikinMoneyFlow, + + adi: ADI, + vol_sum: ValueType, + window: Window, + cross_over: Cross, +} + +impl IndicatorInstance for ChaikinMoneyFlowInstance { + type Config = ChaikinMoneyFlow; + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let adi = self.adi.next(candle); + self.vol_sum += candle.volume() - self.window.push(candle.volume()); + let value = adi / self.vol_sum; + let signal = self.cross_over.next((value, 0.)); + + IndicatorResult::new(&[value], &[signal]) + } + + fn name(&self) -> &str { + "ChaikinMoneyFlow" + } +} diff --git a/src/indicators/chaikin_oscillator.rs b/src/indicators/chaikin_oscillator.rs new file mode 100644 index 0000000..bf62c0a --- /dev/null +++ b/src/indicators/chaikin_oscillator.rs @@ -0,0 +1,114 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, OHLCV}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Cross, ADI}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ChaikinOscillator { + pub period1: PeriodType, + pub period2: PeriodType, + pub method: RegularMethods, + pub window: PeriodType, // from 0 to ... +} + +impl IndicatorConfig for ChaikinOscillator { + fn validate(&self) -> bool { + self.period1 < self.period2 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn is_volume_based(&self) -> bool { + true + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for ChaikinOscillator { + type Instance = ChaikinOscillatorInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let adi = ADI::new(cfg.window, candle); + + Self::Instance { + ma1: method(cfg.method, cfg.period1, adi.get_value()), + ma2: method(cfg.method, cfg.period2, adi.get_value()), + adi, + cross_over: Cross::default(), + cfg, + } + } +} + +impl Default for ChaikinOscillator { + fn default() -> Self { + Self { + period1: 3, + period2: 10, + method: RegularMethods::EMA, + window: 0, + } + } +} + +#[derive(Debug)] +pub struct ChaikinOscillatorInstance { + cfg: ChaikinOscillator, + + adi: ADI, + ma1: RegularMethod, + ma2: RegularMethod, + cross_over: Cross, +} + +impl IndicatorInstance for ChaikinOscillatorInstance { + type Config = ChaikinOscillator; + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let adi = self.adi.next(candle); + + let data1 = self.ma1.next(adi); + let data2 = self.ma2.next(adi); + + let value = data1 - data2; + + let signal = self.cross_over.next((value, 0.)); + + IndicatorResult::new(&[value], &[signal]) + } + + fn name(&self) -> &str { + "ChaikinOscillator" + } +} diff --git a/src/indicators/chande_kroll_stop.rs b/src/indicators/chande_kroll_stop.rs new file mode 100644 index 0000000..f3d06d4 --- /dev/null +++ b/src/indicators/chande_kroll_stop.rs @@ -0,0 +1,156 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +// use std::str::FromStr; + +use crate::core::{Action, Method, PeriodType, Source, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::{Highest, Lowest, RMA}; + +//ChandeKrollStop p=10, x=1.0, q=9, version=1 {1,2,3} +//Индикатор не проверен и может иметь ошибки (Python-версия нерабочая) +// TODO: исправить + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ChandeKrollStop { + pub p: PeriodType, + pub x: ValueType, + pub q: PeriodType, + pub source: Source, + pub version: u8, // Version, // 1, 2 or 3 +} + +impl ChandeKrollStop { + pub const VERSION1: u8 = 1; + pub const VERSION2: u8 = 2; + pub const VERSION3: u8 = 3; +} + +impl IndicatorConfig for ChandeKrollStop { + fn validate(&self) -> bool { self.x >= 0.0 } + + fn set(&mut self, name: &str, value: String) { + match name { + "p" => self.p = value.parse().unwrap(), + "x" => self.x = value.parse().unwrap(), + "q" => self.q = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + "version" => self.version = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { (2, 1) } +} + +impl IndicatorInitializer for ChandeKrollStop { + type Instance = ChandeKrollStopInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + rma: RMA::new(cfg.p, candle.high() - candle.low()), + highest1: Highest::new(cfg.p, candle.high()), + lowest1: Lowest::new(cfg.p, candle.low()), + highest2: Highest::new(cfg.q, candle.high()), + lowest2: Lowest::new(cfg.q, candle.low()), + prev_candle: candle, + cfg, + } + } +} + +impl Default for ChandeKrollStop { + fn default() -> Self { + Self { + p: 10, + x: 1.0, + q: 9, + source: Source::Close, + version: 1, + } + } +} + +#[derive(Debug)] +pub struct ChandeKrollStopInstance { + cfg: ChandeKrollStop, + + rma: RMA, + highest1: Highest, + lowest1: Lowest, + highest2: Highest, + lowest2: Lowest, + prev_candle: T, +} + +impl IndicatorInstance for ChandeKrollStopInstance { + type Config = ChandeKrollStop; + + fn name(&self) -> &str { "ChandeKrollStop" } + + #[inline] + fn config(&self) -> &Self::Config { &self.cfg } + + #[allow(unreachable_code, unused_variables)] + fn next(&mut self, candle: T) -> IndicatorResult { + todo!("Проверить корректность реализации."); + + let tr = candle.tr(&self.prev_candle); + self.prev_candle = candle; + + let atr = self.rma.next(tr); + + let highest = self.highest1; + let lowest = self.lowest1; + + let first_high_stop; + let first_low_stop; + + match self.cfg.version { + Self::Config::VERSION1 => { + first_high_stop = highest.next(candle.high()) - atr * self.cfg.x; + first_low_stop = lowest.next(candle.low()) + atr * self.cfg.x; + } + Self::Config::VERSION2 => { + first_high_stop = highest.next(candle.low()) - atr * self.cfg.x; + first_low_stop = lowest.next(candle.high()) + atr * self.cfg.x; + } + Self::Config::VERSION3 => { + first_low_stop = highest.next(candle.low()) - atr * self.cfg.x; + first_high_stop = lowest.next(candle.high()) + atr * self.cfg.x; + } + _ => { + first_high_stop = highest.next(candle.high()) - atr * self.cfg.x; + first_low_stop = lowest.next(candle.low()) + atr * self.cfg.x; + } + }; + + let stop_short = self.highest2.next(first_high_stop); + let stop_long = self.lowest2.next(first_low_stop); + + let src = candle.source(self.cfg.source); + let mut s = 0; + + if src > stop_long { + s += 1; + } + + if src < stop_short { + s -= 1; + } + + IndicatorResult::new(&[stop_long, stop_short], &[Action::from(s)]) + } +} diff --git a/src/indicators/chande_momentum_oscillator.rs b/src/indicators/chande_momentum_oscillator.rs new file mode 100644 index 0000000..8f3ebe7 --- /dev/null +++ b/src/indicators/chande_momentum_oscillator.rs @@ -0,0 +1,129 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, ValueType, Window, OHLC}; +use crate::methods::{Change, CrossAbove, CrossUnder}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ChandeMomentumOscillator { + pub period: PeriodType, + pub zone: ValueType, + pub source: Source, +} + +impl IndicatorConfig for ChandeMomentumOscillator { + fn validate(&self) -> bool { + self.zone >= 0. && self.zone <= 1.0 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for ChandeMomentumOscillator { + type Instance = ChandeMomentumOscillatorInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + + Self::Instance { + pos_sum: 0., + neg_sum: 0., + change: Change::new(1, candle.source(cfg.source)), + window: Window::new(cfg.period, 0.), + cross_under: CrossUnder::default(), + cross_above: CrossAbove::default(), + cfg: cfg, + } + } +} + +impl Default for ChandeMomentumOscillator { + fn default() -> Self { + Self { + period: 9, + zone: 0.5, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct ChandeMomentumOscillatorInstance { + cfg: ChandeMomentumOscillator, + + pos_sum: ValueType, + neg_sum: ValueType, + change: Change, + window: Window, + cross_under: CrossUnder, + cross_above: CrossAbove, +} + +#[inline] +fn change(change: ValueType) -> (ValueType, ValueType) { + // let pos = if change > 0. { change } else { 0. }; + // let neg = if change < 0. { change * -1. } else { 0. }; + let pos = (change > 0.) as i8 as f64 * change; + let neg = (change < 0.) as i8 as f64 * -change; + + (pos, neg) +} + +impl IndicatorInstance for ChandeMomentumOscillatorInstance { + type Config = ChandeMomentumOscillator; + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let ch = self.change.next(candle.source(self.cfg.source)); + + let left_value = self.window.push(ch); + + let (left_pos, left_neg) = change(left_value); + let (right_pos, right_neg) = change(ch); + + self.pos_sum += right_pos - left_pos; + self.neg_sum += right_neg - left_neg; + + let value = if self.pos_sum != 0. || self.neg_sum != 0. { + (self.pos_sum - self.neg_sum) / (self.pos_sum + self.neg_sum) + } else { + 0. + }; + let signal = self.cross_under.next((value, -self.cfg.zone)) + - self.cross_above.next((value, self.cfg.zone)); + + IndicatorResult::new(&[value], &[signal]) + } + + fn name(&self) -> &str { + "ChandeMomentumOscillator" + } +} diff --git a/src/indicators/commodity_channel_index.rs b/src/indicators/commodity_channel_index.rs new file mode 100644 index 0000000..da29519 --- /dev/null +++ b/src/indicators/commodity_channel_index.rs @@ -0,0 +1,152 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, Source, ValueType, Window, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::SMA; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CommodityChannelIndex { + pub period: PeriodType, + pub zone: ValueType, + pub source: Source, + scale: ValueType, // doesnt change +} + +impl IndicatorConfig for CommodityChannelIndex { + fn validate(&self) -> bool { + self.zone >= 0.0 && self.zone <= 8.0 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for CommodityChannelIndex { + type Instance = CommodityChannelIndexInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let invert_length = (cfg.period as ValueType).recip(); + let value = candle.source(cfg.source); + + Self::Instance { + last_cci: 0., + last_signal: 0, + dev_sum: 0., + sma: SMA::new(cfg.period, value), + window: Window::new(cfg.period, 0.), + + invert_length, + cfg, + } + } +} + +impl Default for CommodityChannelIndex { + fn default() -> Self { + Self { + period: 18, + zone: 1.0, + source: Source::Close, + scale: 1.5, + } + } +} + +//period=20, zone=1.0, #from 0.0 to ~7.0 +//source='close' +#[derive(Debug)] +pub struct CommodityChannelIndexInstance { + cfg: CommodityChannelIndex, + + invert_length: ValueType, + last_cci: ValueType, + last_signal: i8, + dev_sum: ValueType, + sma: SMA, + window: Window, +} + +impl CommodityChannelIndexInstance { + fn dev(&mut self, value: ValueType, ma: ValueType) -> ValueType { + let d = (value - ma).abs(); + + let past_d = self.window.push(d); + self.dev_sum += (d - past_d) * self.invert_length; + self.dev_sum + } + + fn cci(&mut self, value: ValueType) -> ValueType { + let ma = self.sma.next(value); + let dev = self.dev(value, ma); + + (value - ma) / (dev * self.cfg.scale) + } +} + +impl IndicatorInstance for CommodityChannelIndexInstance { + type Config = CommodityChannelIndex; + + fn name(&self) -> &str { + "CommodityChannelIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let value = candle.source(self.cfg.source); + + let cci = self.cci(value); + + // let mut t_signal = 0; + // let mut signal = 0; + + // if cci > self.cfg.zone && self.last_cci <= self.cfg.zone { + // t_signal += 1; + // } + + // if cci < -self.cfg.zone && self.last_cci >= -self.cfg.zone { + // t_signal -= 1; + // } + + let t_signal = (cci > self.cfg.zone && self.last_cci <= self.cfg.zone) as i8 + - (cci < -self.cfg.zone && self.last_cci >= -self.cfg.zone) as i8; + + // if t_signal != 0 && self.last_signal != t_signal { + // signal = t_signal; + // } + + let signal = (t_signal != 0 && self.last_signal != t_signal) as i8 * t_signal; + + self.last_cci = cci; + self.last_signal = signal; + + IndicatorResult::new(&[cci], &[Action::from(signal)]) + } +} diff --git a/src/indicators/coppock_curve.rs b/src/indicators/coppock_curve.rs new file mode 100644 index 0000000..06896b3 --- /dev/null +++ b/src/indicators/coppock_curve.rs @@ -0,0 +1,134 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, OHLC}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Cross, PivotSignal, RateOfChange}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CoppockCurve { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub s2_left: PeriodType, + pub s2_right: PeriodType, + pub s3_period: PeriodType, + pub source: Source, + pub method1: RegularMethods, + pub method2: RegularMethods, +} + +impl IndicatorConfig for CoppockCurve { + fn validate(&self) -> bool { + true + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "s2_left" => self.s2_left = value.parse().unwrap(), + "s2_right" => self.s2_right = value.parse().unwrap(), + "s3_period" => self.s3_period = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + "method1" => self.method1 = value.parse().unwrap(), + "method2" => self.method2 = value.parse().unwrap(), + // "zone" => self.zone = value.parse().unwrap(), + // "source" => self.source = value.parse().unwrap(), + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 3) + } +} + +impl IndicatorInitializer for CoppockCurve { + type Instance = CoppockCurveInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + roc1: RateOfChange::new(cfg.period2, src), + roc2: RateOfChange::new(cfg.period3, src), + ma1: method(cfg.method1, cfg.period1, 0.), + ma2: method(cfg.method2, cfg.s3_period, 0.), + cross_over1: Cross::default(), + pivot: PivotSignal::new(cfg.s2_left, cfg.s2_right, 0.), + cross_over2: Cross::default(), + + cfg, + } + } +} + +impl Default for CoppockCurve { + fn default() -> Self { + Self { + period1: 10, + period2: 14, + period3: 11, + s2_left: 4, + s2_right: 2, + s3_period: 5, + method1: RegularMethods::WMA, + method2: RegularMethods::EMA, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct CoppockCurveInstance { + cfg: CoppockCurve, + + roc1: RateOfChange, + roc2: RateOfChange, + ma1: RegularMethod, + ma2: RegularMethod, + cross_over1: Cross, + pivot: PivotSignal, + cross_over2: Cross, +} + +impl IndicatorInstance for CoppockCurveInstance { + type Config = CoppockCurve; + + fn name(&self) -> &str { + "CoppockCurve" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + let roc1 = self.roc1.next(src); + let roc2 = self.roc2.next(src); + let value1 = self.ma1.next(roc1 + roc2); + let value2 = self.ma2.next(value1); + + let signal1 = self.cross_over1.next((value1, 0.)); + let signal2 = self.pivot.next(value1); + let signal3 = self.cross_over2.next((value1, value2)); + + IndicatorResult::new(&[value1, value2], &[signal1, signal2, signal3]) + } +} diff --git a/src/indicators/detrended_price_oscillator.rs b/src/indicators/detrended_price_oscillator.rs new file mode 100644 index 0000000..e614dd3 --- /dev/null +++ b/src/indicators/detrended_price_oscillator.rs @@ -0,0 +1,137 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, ValueType, Window, OHLC}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::Cross; +// https://www.investopedia.com/terms/d/detrended-price-oscillator-dpo.asp +// The Formula for the Detrended Price Oscillator (DPO) is + +//
DPO=Price from X2+1 periods ago−X period SMA +//
where: +//
X = Number of periods used for the look-back period +//
SMA = Simple Moving Average
\begin{aligned} +//
&DPO=Price~from~\frac{X}{2}+1~periods~ago-X~period~SMA\\ +//
&\textbf{where:}\\ +//
&\text{X = Number of periods used for the look-back period}\\ +//
&\text{SMA = Simple Moving Average}\\ +//
\end{aligned} +//




​DPO=Price from 2 +// X​+1 periods ago−X period SMAwhere:X = Number of periods used for the look-back periodSMA = Simple Moving Average​ + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DetrendedPriceOscillator { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub method1: RegularMethods, + pub method2: RegularMethods, + pub method3: RegularMethods, + pub source: Source, +} + +impl IndicatorConfig for DetrendedPriceOscillator { + fn validate(&self) -> bool { self.period1 > 1 && self.period2 >= 1 && self.period3 >= 1 } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "method1" => self.method1 = value.parse().unwrap(), + "method2" => self.method2 = value.parse().unwrap(), + "method3" => self.method2 = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { (2, 2) } +} + +impl IndicatorInitializer for DetrendedPriceOscillator { + type Instance = DetrendedPriceOscillatorInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + sma: method(cfg.method1, cfg.period1, src), + window: Window::new(cfg.period1 / 2 + 1, src), + ma2: method(cfg.method2, cfg.period2, 0.), + ma3: method(cfg.method3, cfg.period3, 0.), + cross_over1: Cross::default(), + cross_over2: Cross::default(), + + cfg, + } + } +} + +impl Default for DetrendedPriceOscillator { + fn default() -> Self { + Self { + period1: 21, + period2: 21, + period3: 21, + method1: RegularMethods::SMA, + method2: RegularMethods::SMA, + method3: RegularMethods::SMA, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct DetrendedPriceOscillatorInstance { + cfg: DetrendedPriceOscillator, + + sma: RegularMethod, + window: Window, + ma2: RegularMethod, + ma3: RegularMethod, + cross_over1: Cross, + cross_over2: Cross, +} + +impl IndicatorInstance for DetrendedPriceOscillatorInstance { + type Config = DetrendedPriceOscillator; + + fn name(&self) -> &str { "DetrendedPriceOscillator" } + + #[inline] + fn config(&self) -> &Self::Config { &self.cfg } + + #[allow(unreachable_code, unused_variables)] + fn next(&mut self, candle: T) -> IndicatorResult { + todo!("Проверить дефолтные значения"); + // Возможно стоит вернуть индикатор к оригинальному виду + + let src = candle.source(self.cfg.source); + + let sma = self.sma.next(src); + let left_src = self.window.push(src); + + let dpo = left_src - sma; + let q = self.ma2.next(dpo); + let ma_q = self.ma3.next(q); + + let signal1 = self.cross_over1.next((q, 0.)); + let signal2 = self.cross_over2.next((q, ma_q)); + + IndicatorResult::new(&[q, ma_q], &[signal1, signal2]) + } +} diff --git a/src/indicators/ease_of_movement.rs b/src/indicators/ease_of_movement.rs new file mode 100644 index 0000000..1af2c45 --- /dev/null +++ b/src/indicators/ease_of_movement.rs @@ -0,0 +1,118 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, PeriodType, Window, OHLCV}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::{method, signi, RegularMethod, RegularMethods}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct EaseOfMovement { + pub period1: PeriodType, + pub period2: PeriodType, + pub method: RegularMethods, +} + +impl IndicatorConfig for EaseOfMovement { + fn validate(&self) -> bool { + self.period1 > 1 && self.period2 >= 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn is_volume_based(&self) -> bool { + true + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for EaseOfMovement { + type Instance = EaseOfMovementInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + m1: method(cfg.method, cfg.period1, 0.), + w: Window::new(cfg.period2, candle), + + cfg, + } + } +} + +impl Default for EaseOfMovement { + fn default() -> Self { + Self { + period1: 13, + period2: 1, + method: RegularMethods::SMA, + } + } +} + +#[derive(Debug)] +pub struct EaseOfMovementInstance { + cfg: EaseOfMovement, + + m1: RegularMethod, + w: Window, +} + +impl IndicatorInstance for EaseOfMovementInstance { + type Config = EaseOfMovement; + + fn name(&self) -> &str { + "EaseOfMovement" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let prev_candle = self.w.push(candle); + + let d_high = candle.high() - prev_candle.high(); + let d_low = candle.low() - prev_candle.low(); + + let d = (d_high + d_low) * 0.5; + + let v = d * (candle.high() - candle.low()) / candle.volume(); + debug_assert!(v.is_finite() && !v.is_nan()); + + let value = self.m1.next(v); + + // let signal = if value > 0. { + // 1 + // } else if value < 0. { + // -1 + // } else { + // 0 + // }; + let signal = signi(value); + + IndicatorResult::new(&[value], &[Action::from(signal)]) + } +} diff --git a/src/indicators/elders_force_index.rs b/src/indicators/elders_force_index.rs new file mode 100644 index 0000000..f156d76 --- /dev/null +++ b/src/indicators/elders_force_index.rs @@ -0,0 +1,114 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, ValueType, Window, OHLC, OHLCV}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::Cross; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct EldersForceIndex { + pub period1: PeriodType, + pub period2: PeriodType, + pub method: RegularMethods, + pub source: Source, +} + +impl IndicatorConfig for EldersForceIndex { + fn validate(&self) -> bool { + self.period1 > 1 && self.period2 >= 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn is_volume_based(&self) -> bool { + true + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for EldersForceIndex { + type Instance = EldersForceIndexInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + ma: method(cfg.method, cfg.period1, 0.), + window: Window::new(cfg.period2, candle), + vol_sum: candle.volume() * cfg.period2 as ValueType, + cross_over: Cross::default(), + cfg, + } + } +} + +impl Default for EldersForceIndex { + fn default() -> Self { + Self { + period1: 13, + period2: 1, + method: RegularMethods::SMA, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct EldersForceIndexInstance { + cfg: EldersForceIndex, + + ma: RegularMethod, + window: Window, + vol_sum: ValueType, + cross_over: Cross, +} + +impl IndicatorInstance for EldersForceIndexInstance { + type Config = EldersForceIndex; + + fn name(&self) -> &str { + "EldersForceIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let left_candle = self.window.push(candle); + + self.vol_sum += candle.volume() - left_candle.volume(); + let r = (OHLC::source(&candle, self.cfg.source) + - OHLC::source(&left_candle, self.cfg.source)) + * self.vol_sum; + + let value = self.ma.next(r); + let signal = self.cross_over.next((value, 0.)); + + IndicatorResult::new(&[value], &[signal]) + } +} diff --git a/src/indicators/envelopes.rs b/src/indicators/envelopes.rs new file mode 100644 index 0000000..9b0709d --- /dev/null +++ b/src/indicators/envelopes.rs @@ -0,0 +1,117 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, PeriodType, Source, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::{method, RegularMethod, RegularMethods}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Envelopes { + pub period: PeriodType, + pub k: ValueType, + pub method: RegularMethods, + pub source: Source, + pub source2: Source, +} + +impl IndicatorConfig for Envelopes { + fn validate(&self) -> bool { + true + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "k" => self.k = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + "source2" => self.source2 = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 1) + } +} + +impl IndicatorInitializer for Envelopes { + type Instance = EnvelopesInstance; + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + + Self::Instance { + ma: method(cfg.method, cfg.period, src), + k_high: 1.0 + cfg.k, + k_low: 1.0 - cfg.k, + cfg, + } + } +} + +impl Default for Envelopes { + fn default() -> Self { + Self { + period: 20, + k: 0.1, + method: RegularMethods::SMA, + source: Source::Close, + source2: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct EnvelopesInstance { + cfg: Envelopes, + + ma: RegularMethod, + k_high: ValueType, + k_low: ValueType, +} + +impl IndicatorInstance for EnvelopesInstance { + type Config = Envelopes; + + fn name(&self) -> &str { + "Envelopes" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + let v = self.ma.next(src); + + let (value1, value2) = (v * self.k_high, v * self.k_low); + + let src2 = candle.source(self.cfg.source2); + // let signal = if src2 < value2 { + // 1 + // } else if src2 > value1 { + // -1 + // } else { + // 0 + // }; + + let signal = (src2 < value2) as i8 - (src2 > value1) as i8; + + IndicatorResult::new(&[value1, value2], &[Action::from(signal)]) + } +} diff --git a/src/indicators/example.rs b/src/indicators/example.rs new file mode 100644 index 0000000..0156ba0 --- /dev/null +++ b/src/indicators/example.rs @@ -0,0 +1,145 @@ +//! This is an example indicator +//! +//! It has a **Configuration** with parameters `price`, `period` and `source`. +//! +//! The idea is to find signals where price of timeseries crosses this config's `price` for the last `period` frames. + +// Some core structures and traits +use crate::core::{Action, IndicatorResult, PeriodType, Source, ValueType}; +use crate::prelude::*; + +// Cross method for searching crossover between price and our value +use crate::methods::Cross; + +// If you are using `serde`, then it might be useful for you +// If you don't, you can just skip these lines +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// # Example config for the indicator **Configuration** +/// +/// Must implement `Debug`, `Clone`, `Default`, [`IndicatorConfig`](crate::core::IndicatorConfig) and [`IndicatorInitializer`](crate::core::IndicatorInitializer) traits. +/// +/// Also it can implement `serde::{Serialize, Deserialize}` - it's up to you. +/// +/// See source code for the full example +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Example { + price: ValueType, + period: PeriodType, + source: Source, +} + +/// Implementing [`IndicatorConfig`](crate::core::IndicatorConfig) trait +impl IndicatorConfig for Example { + /// Validates config values to be consistent + fn validate(&self) -> bool { + self.price > 0.0 + } + + /// Sets attributes of config by given name and value by `String` + fn set(&mut self, name: &str, value: String) { + match name { + "price" => self.price = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + /// Our indicator will return single raw value and two signals + fn size(&self) -> (u8, u8) { + (1, 2) + } +} + +/// Implementing IndicatorInitializer to create **State** from the **Configration** +impl IndicatorInitializer for Example { + type Instance = ExampleInstance; + + fn init(self, _candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + cross: Cross::default(), + last_signal: Action::None, + last_signal_position: 0, + cfg, + } + } +} + +/// Implementing `Default` trait for default config +impl Default for Example { + fn default() -> Self { + Self { + price: 2.0, + period: 3, + source: Source::Close, + } + } +} + +/// # Example [`IndicatorInstance`](crate::core::IndicatorInstance) implementation +/// +/// Must implement `Debug` and [`IndicatorInstance`](crate::core::IndicatorInstance) traits +/// +/// See source code for the full example +#[derive(Debug, Clone, Copy)] +pub struct ExampleInstance { + cfg: Example, + + cross: Cross, + last_signal: Action, + last_signal_position: PeriodType, +} + +/// Implementing IndicatorInstance trait for Example +impl IndicatorInstance for ExampleInstance { + type Config = Example; + + fn name(&self) -> &str { + "Example" + } + + fn config(&self) -> &Self::Config { + &self.cfg + } + + /// Calculates next value by giving [`OHLC`](crate::core::OHLC)-object + fn next(&mut self, candle: T) -> IndicatorResult { + let new_signal = self.cross.next((candle.close(), self.cfg.price)); + + let signal = match new_signal { + Action::None => { + self.last_signal = new_signal; + self.last_signal_position = 0; + new_signal + } + _ => match self.last_signal { + Action::None => self.last_signal, + _ => { + self.last_signal_position += 1; + if self.last_signal_position > self.cfg.period { + self.last_signal = Action::None; + } + + self.last_signal + } + }, + }; + + let some_other_signal = Action::from(0.5); + + IndicatorResult::new(&[candle.close()], &[signal, some_other_signal]) + } +} diff --git a/src/indicators/fisher_transform.rs b/src/indicators/fisher_transform.rs new file mode 100644 index 0000000..f48e4bf --- /dev/null +++ b/src/indicators/fisher_transform.rs @@ -0,0 +1,189 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, Source, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Cross, Highest, Lowest}; + +// https://www.investopedia.com/terms/f/fisher-transform.asp +// FT = 1/2 * ln((1+x)/(1-x)) = arctanh(x) +// x - transformation of price to a level between -1 and 1 for N periods + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct FisherTransform { + pub period1: PeriodType, + pub period2: PeriodType, + pub zone: ValueType, + pub delta: PeriodType, + pub method: RegularMethods, + pub source: Source, +} + +impl IndicatorConfig for FisherTransform { + fn validate(&self) -> bool { + self.period1 >= 3 && self.delta >= 1 && self.period2 >= 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + "delta" => self.delta = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 2) + } +} + +impl IndicatorInitializer for FisherTransform { + type Instance = FisherTransformInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + + Self::Instance { + ma1: method(cfg.method, cfg.period2, 0.), + highest: Highest::new(cfg.period1, src), + lowest: Lowest::new(cfg.period1, src), + cross_over: Cross::default(), + extreme: 0, + prev_value: 0., + cfg, + } + } +} + +impl Default for FisherTransform { + fn default() -> Self { + Self { + period1: 9, + period2: 1, + zone: 1.5, + delta: 1, + method: RegularMethods::SMA, + source: Source::TP, + } + } +} + +#[derive(Debug)] +pub struct FisherTransformInstance { + cfg: FisherTransform, + + ma1: RegularMethod, + highest: Highest, + lowest: Lowest, + cross_over: Cross, + extreme: i8, + prev_value: ValueType, +} + +const BOUND: ValueType = 0.999; + +#[inline] +fn bound_value(value: ValueType) -> ValueType { + value.min(BOUND).max(-BOUND) +} + +impl IndicatorInstance for FisherTransformInstance { + type Config = FisherTransform; + + fn name(&self) -> &str { + "FisherTransform" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + + // converting original value to between -1.0 and 1.0 over period1 + let h = self.highest.next(src); + let l = self.lowest.next(src); + let is_different = (h != l) as i8 as ValueType; + let v1 = is_different * (src * 2. - (h + l)) / (h - l + 1. - is_different); + + // bound value + let bound_val = bound_value(v1); + + // calculating fisher transform value + let fisher_transform: ValueType = bound_val.atanh(); //((1.0 + v2)/(1.0-v2)).ln(); + + // if fisher_transform > self.cfg.zone { + // self.extreme = -1; + // } else if fisher_transform < self.cfg.zone { + // self.extreme = 1; + // } + self.extreme = + (fisher_transform < self.cfg.zone) as i8 - (fisher_transform > self.cfg.zone) as i8; + + let s1; + { + // We’ll take trade signals based on the following rules: + // Long trades + + // Fisher Transform must be negative (i.e., the more negative the indicator is, the more “stretched” or excessively bearish price is) + // Taken after a reversal of the Fisher Transform from negatively sloped to positively sloped (i.e., rate of change from negative to positive) + + // Short trades + + // Fisher Transform must be positive (i.e., price perceived to be excessively bullish) + // Taken after a reversal in the direction of the Fisher Transform + // s1 = if self.extreme == 1 && fisher_transform - self.prev_value < 0. { + // -1 + // } else if self.extreme == -1 && fisher_transform - self.prev_value > 0. { + // 1 + // } else { + // 0 + // }; + s1 = (self.extreme == -1 && fisher_transform - self.prev_value > 0.) as i8 + - (self.extreme == 1 && fisher_transform - self.prev_value < 0.) as i8; + } + + self.prev_value = fisher_transform; + + let s2; + let fisher_transform_ma: ValueType; + { + // The Fisher Transform frequently has a signal line attached to it. This is a moving average of the Fisher Transform value, + // so it moves slightly slower than the Fisher Transform line. When the Fisher Transform crosses the trigger line it is used + // by some traders as a trade signal. For example, when the Fisher Transform drops below the signal line after hitting an + // extreme high, that could be used as a signal to sell a current long position. + fisher_transform_ma = self.ma1.next(fisher_transform); + let cross = self + .cross_over + .next((fisher_transform, fisher_transform_ma)) + .analog(); + // s2 = if cross * self.extreme == 1 { cross } else { 0 }; + s2 = ((cross * self.extreme) == 1) as i8 * cross; + } + + IndicatorResult::new( + &[fisher_transform, fisher_transform_ma], + &[Action::from(s1), Action::from(s2)], + ) + } +} diff --git a/src/indicators/hull_moving_average.rs b/src/indicators/hull_moving_average.rs new file mode 100644 index 0000000..723cda5 --- /dev/null +++ b/src/indicators/hull_moving_average.rs @@ -0,0 +1,100 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, OHLC}; +use crate::methods::{PivotSignal, HMA}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct HullMovingAverage { + pub period: PeriodType, + pub left: PeriodType, + pub right: PeriodType, + pub source: Source, +} + +impl IndicatorConfig for HullMovingAverage { + fn validate(&self) -> bool { + self.period > 1 && self.left >= 1 && self.right >= 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "left" => self.left = value.parse().unwrap(), + "right" => self.right = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for HullMovingAverage { + type Instance = HullMovingAverageInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + + Self::Instance { + hma: HMA::new(cfg.period, src), + pivot: PivotSignal::new(cfg.left, cfg.right, src), + cfg, + } + } +} + +impl Default for HullMovingAverage { + fn default() -> Self { + Self { + period: 9, + left: 3, + right: 2, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct HullMovingAverageInstance { + cfg: HullMovingAverage, + + hma: HMA, + pivot: PivotSignal, +} + +impl IndicatorInstance for HullMovingAverageInstance { + type Config = HullMovingAverage; + + fn name(&self) -> &str { + "HullMovingAverage" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let value = self.hma.next(candle.source(self.cfg.source)); + let signal = self.pivot.next(value); + + IndicatorResult::new(&[value], &[signal]) + } +} diff --git a/src/indicators/ichimoku_cloud.rs b/src/indicators/ichimoku_cloud.rs new file mode 100644 index 0000000..28e43c6 --- /dev/null +++ b/src/indicators/ichimoku_cloud.rs @@ -0,0 +1,162 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, Source, ValueType, Window, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::{Cross, Highest, Lowest}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct IchimokuCloud { + pub l1: PeriodType, + pub l2: PeriodType, + pub l3: PeriodType, + pub m: PeriodType, + pub source: Source, +} + +impl IndicatorConfig for IchimokuCloud { + fn validate(&self) -> bool { + self.l1 < self.l2 && self.l2 < self.l3 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "l1" => self.l1 = value.parse().unwrap(), + "l2" => self.l2 = value.parse().unwrap(), + "l3" => self.l3 = value.parse().unwrap(), + "m" => self.m = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (4, 2) + } +} + +impl IndicatorInitializer for IchimokuCloud { + type Instance = IchimokuCloudInstance; + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + highest1: Highest::new(cfg.l1, candle.high()), + highest2: Highest::new(cfg.l2, candle.high()), + highest3: Highest::new(cfg.l3, candle.high()), + lowest1: Lowest::new(cfg.l1, candle.low()), + lowest2: Lowest::new(cfg.l2, candle.low()), + lowest3: Lowest::new(cfg.l3, candle.low()), + window1: Window::new(cfg.m, candle.hl2()), + window2: Window::new(cfg.m, candle.hl2()), + cross1: Cross::default(), + cross2: Cross::default(), + cfg, + } + } +} + +impl Default for IchimokuCloud { + fn default() -> Self { + Self { + l1: 9, + l2: 26, + l3: 52, + m: 26, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct IchimokuCloudInstance { + cfg: IchimokuCloud, + + highest1: Highest, + highest2: Highest, + highest3: Highest, + lowest1: Lowest, + lowest2: Lowest, + lowest3: Lowest, + window1: Window, + window2: Window, + cross1: Cross, + cross2: Cross, +} + +impl IndicatorInstance for IchimokuCloudInstance { + type Config = IchimokuCloud; + + fn name(&self) -> &str { + "IchimokuCloud" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + let (high, low) = (candle.high(), candle.low()); + let (highest1, lowest1) = (self.highest1.next(high), self.lowest1.next(low)); + let (highest2, lowest2) = (self.highest2.next(high), self.lowest2.next(low)); + let (highest3, lowest3) = (self.highest3.next(high), self.lowest3.next(low)); + + let tenkan_sen = (highest1 + lowest1) * 0.5; + let kijun_sen = (highest2 + lowest2) * 0.5; + + let senkou_span_a = self.window1.push((tenkan_sen + kijun_sen) * 0.5); + let senkou_span_b = self.window2.push((highest3 + lowest3) * 0.5); + + let s1_cross = self.cross1.next((tenkan_sen, kijun_sen)); + let s2_cross = self.cross2.next((src, kijun_sen)); + + // let mut s1 = 0; + // let mut s2 = 0; + + let green: bool = senkou_span_a > senkou_span_b; + let red: bool = senkou_span_a < senkou_span_b; + + // if src > senkou_span_a && src > senkou_span_b && green && s1_cross == Action::BUY_ALL { + // s1 += 1; + // } else if src < senkou_span_a && src < senkou_span_b && red && s1_cross == Action::SELL_ALL + // { + // s1 -= 1; + // } + + // if src > senkou_span_a && src > senkou_span_b && green && s2_cross == Action::BUY_ALL { + // s2 += 1; + // } else if src < senkou_span_a && src < senkou_span_b && red && s2_cross == Action::SELL_ALL + // { + // s2 -= 1; + // } + + let s1 = (src > senkou_span_a + && src > senkou_span_b + && green && s1_cross == Action::BUY_ALL) as i8 + - (src < senkou_span_a && src < senkou_span_b && red && s1_cross == Action::SELL_ALL) + as i8; + let s2 = (src > senkou_span_a + && src > senkou_span_b + && green && s2_cross == Action::BUY_ALL) as i8 + - (src < senkou_span_a && src < senkou_span_b && red && s2_cross == Action::SELL_ALL) + as i8; + + IndicatorResult::new( + &[tenkan_sen, kijun_sen, senkou_span_a, senkou_span_b], + &[Action::from(s1), Action::from(s2)], + ) + } +} diff --git a/src/indicators/kaufman.rs b/src/indicators/kaufman.rs new file mode 100644 index 0000000..81e1c90 --- /dev/null +++ b/src/indicators/kaufman.rs @@ -0,0 +1,162 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, Source, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::{Change, Cross, LinearVolatility, StDev}; + +// https://ru.wikipedia.org/wiki/%D0%90%D0%B4%D0%B0%D0%BF%D1%82%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D0%BA%D0%BE%D0%BB%D1%8C%D0%B7%D1%8F%D1%89%D0%B0%D1%8F_%D1%81%D1%80%D0%B5%D0%B4%D0%BD%D1%8F%D1%8F_%D0%9A%D0%B0%D1%83%D1%84%D0%BC%D0%B0%D0%BD%D0%B0 +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Kaufman { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub filter_period: PeriodType, + pub square_smooth: bool, + pub k: ValueType, + pub source: Source, +} + +impl IndicatorConfig for Kaufman { + fn validate(&self) -> bool { + self.period3 > self.period2 + && self.period2 > 0 + && self.period1 > 0 + && (self.k > 0.0 || self.filter_period < 2) + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "filter_period" => self.filter_period = value.parse().unwrap(), + "square_smooth" => self.square_smooth = value.parse().unwrap(), + "k" => self.k = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for Kaufman { + type Instance = KaufmanInstance; + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + volatility: LinearVolatility::new(cfg.period1, src), + change: Change::new(cfg.period1, src), + fastest: 2. / (cfg.period2 + 1) as ValueType, + slowest: 2. / (cfg.period3 + 1) as ValueType, + st_dev: StDev::new(cfg.filter_period, src), + cross: Cross::default(), + last_signal: Action::None, + last_signal_value: src, + prev_value: src, + cfg: cfg, + } + } +} + +impl Default for Kaufman { + fn default() -> Self { + Self { + period1: 10, + period2: 2, + period3: 30, + k: 0.3, + square_smooth: true, + filter_period: 10, + source: Source::Close, + } + } +} +#[derive(Debug)] +pub struct KaufmanInstance { + cfg: Kaufman, + + volatility: LinearVolatility, + change: Change, + fastest: ValueType, + slowest: ValueType, + st_dev: StDev, + cross: Cross, + last_signal: Action, + last_signal_value: ValueType, + prev_value: ValueType, +} + +impl IndicatorInstance for KaufmanInstance { + type Config = Kaufman; + + fn name(&self) -> &str { + "Kaufman" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + + let direction = self.change.next(src).abs(); + let volatility = self.volatility.next(src); + + let er = if volatility != 0. { + direction / volatility + } else { + 0. + }; + let mut smooth = er.mul_add(self.fastest - self.slowest, self.slowest); + + if self.cfg.square_smooth { + smooth = smooth * smooth; + } + + let value = self.prev_value + smooth * (src - self.prev_value); + self.prev_value = value; + + let cross = self.cross.next((src, value)); + + let signal; + if self.cfg.filter_period > 1 { + let st_dev = self.st_dev.next(value); + let filter = st_dev * self.cfg.k; + + if cross.is_some() { + self.last_signal = cross; + self.last_signal_value = value; + signal = Action::None; + } else if self.last_signal.is_some() && (value - self.last_signal_value).abs() > filter + { + signal = self.last_signal; + self.last_signal = Action::None; + } else { + signal = Action::None; + } + } else { + signal = cross; + } + + IndicatorResult::new(&[value], &[signal]) + } +} diff --git a/src/indicators/keltner_channels.rs b/src/indicators/keltner_channels.rs new file mode 100644 index 0000000..871e02f --- /dev/null +++ b/src/indicators/keltner_channels.rs @@ -0,0 +1,115 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, ValueType, OHLC}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{CrossAbove, CrossUnder, SMA}; + +// https://en.wikipedia.org/wiki/Keltner_channel +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct KeltnerChannels { + pub period: PeriodType, + pub method: RegularMethods, + pub sigma: ValueType, + pub source: Source, +} + +impl IndicatorConfig for KeltnerChannels { + fn validate(&self) -> bool { + self.period > 1 && self.sigma > 1e-4 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + "sigma" => self.sigma = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (3, 1) + } +} + +impl IndicatorInitializer for KeltnerChannels { + type Instance = KeltnerChannelsInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + prev_candle: candle, + ma: method(cfg.method, cfg.period, src), + sma: SMA::new(cfg.period, candle.high() - candle.low()), + cross_above: CrossAbove::default(), + cross_under: CrossUnder::default(), + cfg, + } + } +} + +impl Default for KeltnerChannels { + fn default() -> Self { + Self { + period: 20, + sigma: 1.0, + source: Source::Close, + method: RegularMethods::EMA, + } + } +} + +#[derive(Debug)] +pub struct KeltnerChannelsInstance { + cfg: KeltnerChannels, + + prev_candle: T, + ma: RegularMethod, + sma: SMA, + cross_above: CrossAbove, + cross_under: CrossUnder, +} + +impl IndicatorInstance for KeltnerChannelsInstance { + type Config = KeltnerChannels; + + fn name(&self) -> &str { + "KeltnerChannels" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let source = candle.source(self.cfg.source); + let tr = candle.tr(&self.prev_candle); + let ma: ValueType = self.ma.next(source); + let atr = self.sma.next(tr); + + let upper = ma + atr * self.cfg.sigma; + let lower = ma - atr * self.cfg.sigma; + + let signal = + self.cross_under.next((source, lower)) - self.cross_above.next((source, upper)); + + IndicatorResult::new(&[source, upper, lower], &[signal]) + } +} diff --git a/src/indicators/klinger_volume_oscillator.rs b/src/indicators/klinger_volume_oscillator.rs new file mode 100644 index 0000000..d5e8f3a --- /dev/null +++ b/src/indicators/klinger_volume_oscillator.rs @@ -0,0 +1,135 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, ValueType, OHLCV}; +use crate::helpers::{method, sign, RegularMethod, RegularMethods}; +use crate::methods::Cross; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct KlingerVolumeOscillator { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub method1: RegularMethods, + pub method2: RegularMethods, +} + +impl IndicatorConfig for KlingerVolumeOscillator { + fn validate(&self) -> bool { + self.period1 < self.period2 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "method1" => self.method1 = value.parse().unwrap(), + "method2" => self.method2 = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn is_volume_based(&self) -> bool { + true + } + + fn size(&self) -> (u8, u8) { + (2, 2) + } +} + +impl IndicatorInitializer for KlingerVolumeOscillator { + type Instance = KlingerVolumeOscillatorInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + ma1: method(cfg.method1, cfg.period1, 0.), + ma2: method(cfg.method1, cfg.period2, 0.), + ma3: method(cfg.method2, cfg.period3, 0.), + cross1: Cross::default(), + cross2: Cross::default(), + last_tp: candle.tp(), + cfg, + } + } +} + +impl Default for KlingerVolumeOscillator { + fn default() -> Self { + Self { + period1: 34, + period2: 55, + period3: 13, + method1: RegularMethods::EMA, + method2: RegularMethods::EMA, + } + } +} + +#[derive(Debug)] +pub struct KlingerVolumeOscillatorInstance { + cfg: KlingerVolumeOscillator, + + ma1: RegularMethod, + ma2: RegularMethod, + ma3: RegularMethod, + cross1: Cross, + cross2: Cross, + last_tp: ValueType, +} + +impl IndicatorInstance for KlingerVolumeOscillatorInstance { + type Config = KlingerVolumeOscillator; + + fn name(&self) -> &str { + "KlingerVolumeOscillator" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let tp = candle.tp(); + + let d = tp - self.last_tp; + self.last_tp = tp; + + // let vol = if d > 0. { + // candle.volume() + // } else if d < 0. { + // -candle.volume() + // } else { + // 0. + // }; + + let vol = sign(d) * candle.volume(); + + let ma1: ValueType = self.ma1.next(vol); + let ma2: ValueType = self.ma2.next(vol); + let ko = ma1 - ma2; + + let ma3: ValueType = self.ma3.next(ko); + + let s1 = self.cross1.next((ko, 0.)); + let s2 = self.cross2.next((ko, ma3)); + + IndicatorResult::new(&[ko, ma3], &[s1, s2]) + } +} diff --git a/src/indicators/know_sure_thing.rs b/src/indicators/know_sure_thing.rs new file mode 100644 index 0000000..bb847e5 --- /dev/null +++ b/src/indicators/know_sure_thing.rs @@ -0,0 +1,152 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, ValueType, OHLC}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Cross, RateOfChange}; + +// https://en.wikipedia.org/wiki/KST_oscillator +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct KnowSureThing { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub period4: PeriodType, + pub sma1: PeriodType, + pub sma2: PeriodType, + pub sma3: PeriodType, + pub sma4: PeriodType, + pub method1: RegularMethods, + + pub sma5: PeriodType, + pub method2: RegularMethods, +} + +impl IndicatorConfig for KnowSureThing { + fn validate(&self) -> bool { + self.period1 < self.period2 && self.period2 < self.period3 && self.period3 < self.period4 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "period4" => self.period4 = value.parse().unwrap(), + "sma1" => self.sma1 = value.parse().unwrap(), + "sma2" => self.sma2 = value.parse().unwrap(), + "sma3" => self.sma3 = value.parse().unwrap(), + "sma4" => self.sma4 = value.parse().unwrap(), + "sma5" => self.sma5 = value.parse().unwrap(), + "method1" => self.method1 = value.parse().unwrap(), + "method2" => self.method2 = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 1) + } +} + +impl IndicatorInitializer for KnowSureThing { + type Instance = KnowSureThingInstance; + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let close = candle.close(); + Self::Instance { + roc1v: RateOfChange::new(cfg.period1, close), + roc2v: RateOfChange::new(cfg.period2, close), + roc3v: RateOfChange::new(cfg.period3, close), + roc4v: RateOfChange::new(cfg.period4, close), + ma1: method(cfg.method1, cfg.sma1, 0.), + ma2: method(cfg.method1, cfg.sma2, 0.), + ma3: method(cfg.method1, cfg.sma3, 0.), + ma4: method(cfg.method1, cfg.sma4, 0.), + ma5: method(cfg.method2, cfg.sma5, 0.), + cross: Cross::default(), + cfg, + } + } +} + +impl Default for KnowSureThing { + fn default() -> Self { + Self { + period1: 10, + period2: 15, + period3: 20, + period4: 30, + sma1: 10, + sma2: 10, + sma3: 10, + sma4: 15, + sma5: 9, + method1: RegularMethods::SMA, + method2: RegularMethods::SMA, + } + } +} + +#[derive(Debug)] +pub struct KnowSureThingInstance { + cfg: KnowSureThing, + + roc1v: RateOfChange, + roc2v: RateOfChange, + roc3v: RateOfChange, + roc4v: RateOfChange, + ma1: RegularMethod, + ma2: RegularMethod, + ma3: RegularMethod, + ma4: RegularMethod, + ma5: RegularMethod, + cross: Cross, +} + +impl IndicatorInstance for KnowSureThingInstance { + type Config = KnowSureThing; + + fn name(&self) -> &str { + "KnowSureThing" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let close = candle.close(); + + let roc1: ValueType = self.roc1v.next(close); + let roc2: ValueType = self.roc2v.next(close); + let roc3: ValueType = self.roc3v.next(close); + let roc4: ValueType = self.roc4v.next(close); + + let rcma1: ValueType = self.ma1.next(roc1); + let rcma2: ValueType = self.ma2.next(roc2); + let rcma3: ValueType = self.ma3.next(roc3); + let rcma4: ValueType = self.ma4.next(roc4); + + let kst = rcma1 + rcma2 * 2. + rcma3 * 3. + rcma4 * 4.; + let sl: ValueType = self.ma5.next(kst); + + let signal = self.cross.next((kst, sl)); + + IndicatorResult::new(&[kst, sl], &[signal]) + } +} diff --git a/src/indicators/macd.rs b/src/indicators/macd.rs new file mode 100644 index 0000000..c36ed6f --- /dev/null +++ b/src/indicators/macd.rs @@ -0,0 +1,128 @@ +#![allow(unused_imports)] + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Candle, Method, PeriodType, Source, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::Cross; + +// https://en.wikipedia.org/wiki/MACD +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct MACD { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub method1: RegularMethods, + pub method2: RegularMethods, + pub method3: RegularMethods, + pub source: Source, +} + +impl IndicatorConfig for MACD { + fn validate(&self) -> bool { + self.period1 < self.period2 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "method1" => self.method1 = value.parse().unwrap(), + "method2" => self.method2 = value.parse().unwrap(), + "method3" => self.method3 = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 1) + } +} + +impl IndicatorInitializer for MACD { + type Instance = MACDInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + ma1: method(cfg.method1, cfg.period1, src), + ma2: method(cfg.method2, cfg.period2, src), + ma3: method(cfg.method3, cfg.period3, src), + cross: Cross::new((), (0.0, 0.0)), + cfg, + } + } +} + +impl Default for MACD { + fn default() -> Self { + Self { + period1: 12, + period2: 26, + period3: 9, + method1: RegularMethods::EMA, + method2: RegularMethods::EMA, + method3: RegularMethods::EMA, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct MACDInstance { + cfg: MACD, + + ma1: RegularMethod, + ma2: RegularMethod, + ma3: RegularMethod, + cross: Cross, +} + +/// Just an alias for MACD +pub type MovingAverageConvergenceDivergence = MACD; + +impl IndicatorInstance for MACDInstance { + type Config = MACD; + + fn config(&self) -> &Self::Config + where + Self: Sized, + { + &self.cfg + } + + #[inline] + fn next(&mut self, candle: T) -> IndicatorResult + where + Self: Sized, + { + let src = candle.source(self.cfg.source); + + let ema1 = self.ma1.next(src); + let ema2 = self.ma2.next(src); + + let macd = ema1 - ema2; + let sigline = self.ma3.next(macd); + + let signal = self.cross.next((macd, sigline)); + + IndicatorResult::new(&[macd, sigline], &[signal]) + } +} diff --git a/src/indicators/mod.rs b/src/indicators/mod.rs new file mode 100644 index 0000000..8db7c26 --- /dev/null +++ b/src/indicators/mod.rs @@ -0,0 +1,109 @@ +#![allow(missing_docs)] +pub mod example; + +// // --------------------------------------------- + +// mod aroon; +// pub use aroon::*; + +mod average_directional_index; +pub use average_directional_index::*; + +mod awesome_oscillator; +pub use awesome_oscillator::*; + +mod bollinger_bands; +pub use bollinger_bands::*; + +mod chaikin_money_flow; +pub use chaikin_money_flow::*; + +mod chaikin_oscillator; +pub use chaikin_oscillator::*; + +// mod chande_kroll_stop; +// pub use chande_kroll_stop::*; + +mod chande_momentum_oscillator; +pub use chande_momentum_oscillator::*; + +mod commodity_channel_index; +pub use commodity_channel_index::*; + +mod coppock_curve; +pub use coppock_curve::*; + +// mod detrended_price_oscillator; +// pub use detrended_price_oscillator::*; + +mod ease_of_movement; +pub use ease_of_movement::*; + +mod elders_force_index; +pub use elders_force_index::*; + +mod envelopes; +pub use envelopes::*; + +mod fisher_transform; +pub use fisher_transform::*; + +mod hull_moving_average; +pub use hull_moving_average::*; + +mod ichimoku_cloud; +pub use ichimoku_cloud::*; + +mod kaufman; +pub use kaufman::*; + +mod keltner_channels; +pub use keltner_channels::*; + +mod klinger_volume_oscillator; +pub use klinger_volume_oscillator::*; + +mod know_sure_thing; +pub use know_sure_thing::*; + +mod macd; +pub use macd::*; + +mod momentum_index; +pub use momentum_index::*; + +mod money_flow_index; +pub use money_flow_index::*; + +mod parabolic_sar; +pub use parabolic_sar::*; + +mod pivot_reversal_strategy; +pub use pivot_reversal_strategy::*; + +mod price_channel_strategy; +pub use price_channel_strategy::*; + +mod relative_strength_index; +pub use relative_strength_index::*; + +mod relative_vigor_index; +pub use relative_vigor_index::*; + +mod smi_ergodic_indicator; +pub use smi_ergodic_indicator::*; + +mod stochastic_oscillator; +pub use stochastic_oscillator::*; + +mod trix; +pub use trix::*; + +mod true_strength_index; +pub use true_strength_index::*; + +mod vidya; +pub use vidya::*; + +mod woodies_cci; +pub use woodies_cci::*; diff --git a/src/indicators/momentum_index.rs b/src/indicators/momentum_index.rs new file mode 100644 index 0000000..71736b2 --- /dev/null +++ b/src/indicators/momentum_index.rs @@ -0,0 +1,109 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, Source, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::Momentum; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct MomentumIndex { + pub period1: PeriodType, + pub period2: PeriodType, + pub source: Source, +} + +impl IndicatorConfig for MomentumIndex { + fn validate(&self) -> bool { + self.period1 > self.period2 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 1) + } +} + +impl IndicatorInitializer for MomentumIndex { + type Instance = MomentumIndexInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + momentum1: Momentum::new(cfg.period1, src), + momentum2: Momentum::new(cfg.period2, src), + cfg, + } + } +} + +impl Default for MomentumIndex { + fn default() -> Self { + Self { + period1: 10, + period2: 1, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct MomentumIndexInstance { + cfg: MomentumIndex, + + momentum1: Momentum, + momentum2: Momentum, +} + +impl IndicatorInstance for MomentumIndexInstance { + type Config = MomentumIndex; + + fn name(&self) -> &str { + "MomentumIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + + let v = self.momentum1.next(src); + let s = self.momentum2.next(src); + + // let signal; + // if v > 0. && s > 0. { + // signal = 1; + // } else if v < 0. && s < 0. { + // signal = -1; + // } else { + // signal = 0; + // } + + let signal = (v > 0. && s > 0.) as i8 - (v < 0. && s < 0.) as i8; + + IndicatorResult::new(&[v, s], &[Action::from(signal)]) + } +} diff --git a/src/indicators/money_flow_index.rs b/src/indicators/money_flow_index.rs new file mode 100644 index 0000000..35107b8 --- /dev/null +++ b/src/indicators/money_flow_index.rs @@ -0,0 +1,150 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, ValueType, Window, OHLCV}; +use crate::methods::{CrossAbove, CrossUnder}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct MoneyFlowIndex { + pub period: PeriodType, + pub zone: ValueType, +} + +impl IndicatorConfig for MoneyFlowIndex { + fn validate(&self) -> bool { + self.zone >= 0. && self.zone <= 0.5 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn is_volume_based(&self) -> bool { + true + } + + fn size(&self) -> (u8, u8) { + (3, 1) + } +} + +impl IndicatorInitializer for MoneyFlowIndex { + type Instance = MoneyFlowIndexInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + window: Window::new(cfg.period, candle), + prev_candle: candle, + last_prev_candle: candle, + pmf: 0., + nmf: 0., + cross_under: CrossUnder::default(), + cross_above: CrossAbove::default(), + cfg, + } + } +} + +impl Default for MoneyFlowIndex { + fn default() -> Self { + Self { + period: 14, + zone: 0.2, + } + } +} + +#[derive(Debug)] +pub struct MoneyFlowIndexInstance { + cfg: MoneyFlowIndex, + + window: Window, + prev_candle: T, + last_prev_candle: T, + pmf: ValueType, + nmf: ValueType, + cross_under: CrossUnder, + cross_above: CrossAbove, +} + +#[inline] +fn tfunc(candle: &T, last_candle: &T) -> (ValueType, ValueType) { + let tp1 = candle.tp(); + let tp2 = last_candle.tp(); + + // if tp1 < tp2 { + // (0., tp1 * candle.volume()) + // } else if tp1 > tp2 { + // (tp1 * candle.volume(), 0.) + // } else { + // (0., 0.) + // } + + ( + (tp1 > tp2) as i8 as ValueType * candle.volume(), + (tp1 < tp2) as i8 as ValueType * candle.volume(), + ) +} + +impl IndicatorInstance for MoneyFlowIndexInstance { + type Config = MoneyFlowIndex; + + fn name(&self) -> &str { + "MoneyFlowIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let (pos, neg) = tfunc(&candle, &self.prev_candle); + + let last_candle = self.window.push(candle); + let (left_pos, left_neg) = tfunc(&last_candle, &self.last_prev_candle); + + self.last_prev_candle = last_candle; + self.prev_candle = candle; + + self.pmf += pos - left_pos; + self.nmf += neg - left_neg; + + let mfr; + if self.nmf != 0.0 { + mfr = self.pmf / self.nmf; + } else { + mfr = 1.; + } + + let value = 1. - (1. + mfr).recip(); + + let upper = self.cfg.zone; + let lower = 1. - self.cfg.zone; + + let cross_under = self.cross_under.next((value, self.cfg.zone)); + let cross_above = self.cross_above.next((value, 1. - self.cfg.zone)); + + let signal = cross_under - cross_above; + + IndicatorResult::new(&[upper, value, lower], &[signal]) + } +} diff --git a/src/indicators/parabolic_sar.rs b/src/indicators/parabolic_sar.rs new file mode 100644 index 0000000..257f23d --- /dev/null +++ b/src/indicators/parabolic_sar.rs @@ -0,0 +1,157 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; + +// https://en.wikipedia.org/wiki/Parabolic_SAR +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ParabolicSAR { + pub af_step: ValueType, + pub af_max: ValueType, +} + +impl IndicatorConfig for ParabolicSAR { + fn validate(&self) -> bool { + self.af_step < self.af_max + } + + fn set(&mut self, name: &str, value: String) { + match name { + "af_step" => self.af_step = value.parse().unwrap(), + "af_max" => self.af_max = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 1) + } +} + +impl IndicatorInitializer for ParabolicSAR { + type Instance = ParabolicSARInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + trend: 1, + trend_inc: 1, + low: candle.low(), + high: candle.high(), + sar: candle.low(), + prev_candle: candle, + prev_trend: 0, + cfg, + } + } +} + +impl Default for ParabolicSAR { + fn default() -> Self { + Self { + af_max: 0.2, + af_step: 0.02, + } + } +} + +#[derive(Debug)] +pub struct ParabolicSARInstance { + cfg: ParabolicSAR, + + trend: i8, + trend_inc: u32, + low: ValueType, + high: ValueType, + sar: ValueType, + prev_candle: T, + prev_trend: i8, +} + +/// Just an alias for ParabolicSAR +pub type ParabolicStopAndReverse = ParabolicSAR; + +impl IndicatorInstance for ParabolicSARInstance { + type Config = ParabolicSAR; + + fn name(&self) -> &str { + "ParabolicSAR" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + if self.trend > 0 { + if self.high < candle.high() { + self.high = candle.high(); + self.trend_inc += 1; + } + + if candle.low() < self.sar { + self.trend *= -1; + self.low = candle.low(); + self.trend_inc = 1; + self.sar = self.high; + } + } else if self.trend < 0 { + if self.low > candle.low() { + self.low = candle.low(); + self.trend_inc += 1; + } + + if candle.high() > self.sar { + self.trend *= -1; + self.high = candle.high(); + self.trend_inc = 1; + self.sar = self.low; + } + } + + let trend = self.trend; + let sar = self.sar; + + // af := math.Min(a.AfMax, a.AfStep*float64(trendI)) + let af = self + .cfg + .af_max + .min(self.cfg.af_step * (self.trend_inc as ValueType)); + + if self.trend > 0 { + self.sar = self.sar + af * (self.high - self.sar); + self.sar = self.sar.min(candle.low()).min(self.prev_candle.low()); + } else if self.trend < 0 { + self.sar = self.sar + af * (self.low - self.sar); + self.sar = self.sar.max(candle.high()).max(self.prev_candle.high()); + } + + self.prev_candle = candle; + + // let signal; + // if self.prev_trend != trend { + // signal = trend; + // } else { + // signal = 0; + // } + let signal = (self.prev_trend != trend) as i8 * trend; + + self.prev_trend = trend; + + IndicatorResult::new(&[sar, trend as ValueType], &[Action::from(signal)]) + } +} diff --git a/src/indicators/pivot_reversal_strategy.rs b/src/indicators/pivot_reversal_strategy.rs new file mode 100644 index 0000000..0ab46b1 --- /dev/null +++ b/src/indicators/pivot_reversal_strategy.rs @@ -0,0 +1,117 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, ValueType, Window, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::{PivotHighSignal, PivotLowSignal}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PivotReversalStrategy { + pub left: PeriodType, + pub right: PeriodType, +} + +impl IndicatorConfig for PivotReversalStrategy { + fn validate(&self) -> bool { + true + } + + fn set(&mut self, name: &str, value: String) { + match name { + "left" => self.left = value.parse().unwrap(), + "right" => self.right = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for PivotReversalStrategy { + type Instance = PivotReversalStrategyInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + ph: PivotHighSignal::new(cfg.left, cfg.right, candle.high()), + pl: PivotLowSignal::new(cfg.left, cfg.right, candle.low()), + window: Window::new(cfg.right, candle), + hprice: 0., + lprice: 0., + cfg, + } + } +} + +impl Default for PivotReversalStrategy { + fn default() -> Self { + Self { left: 4, right: 2 } + } +} + +#[derive(Debug)] +pub struct PivotReversalStrategyInstance { + cfg: PivotReversalStrategy, + + ph: PivotHighSignal, + pl: PivotLowSignal, + window: Window, + hprice: ValueType, + lprice: ValueType, +} + +impl IndicatorInstance for PivotReversalStrategyInstance { + type Config = PivotReversalStrategy; + fn name(&self) -> &str { + "PivotReversalStrategy" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + fn next(&mut self, candle: T) -> IndicatorResult { + let (high, low) = (candle.high(), candle.low()); + let past_candle = self.window.push(candle); + + let swh = self.ph.next(high); + let swl = self.pl.next(low); + + let mut le = 0; + let mut se = 0; + + if swh.analog() > 0 { + self.hprice = past_candle.high(); + } + + if swh.analog() > 0 || candle.high() <= self.hprice { + le = 1; + } + + if swl.analog() > 0 { + self.lprice = past_candle.low(); + } + + if swl.analog() > 0 || low >= self.lprice { + se = 1; + } + + let r = se - le; + + IndicatorResult::new(&[r as ValueType], &[Action::from(r)]) + } +} diff --git a/src/indicators/price_channel_strategy.rs b/src/indicators/price_channel_strategy.rs new file mode 100644 index 0000000..d454436 --- /dev/null +++ b/src/indicators/price_channel_strategy.rs @@ -0,0 +1,104 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::{Highest, Lowest}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PriceChannelStrategy { + pub period: PeriodType, + pub sigma: ValueType, +} + +impl IndicatorConfig for PriceChannelStrategy { + fn validate(&self) -> bool { + self.period > 1 && self.sigma > 0. + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "sigma" => self.sigma = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 1) + } +} + +impl IndicatorInitializer for PriceChannelStrategy { + type Instance = PriceChannelStrategyInstance; + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + Self::Instance { + highest: Highest::new(cfg.period, candle.high()), + lowest: Lowest::new(cfg.period, candle.low()), + cfg, + } + } +} + +impl Default for PriceChannelStrategy { + fn default() -> Self { + Self { + period: 20, + sigma: 1.0, + } + } +} + +#[derive(Debug)] +pub struct PriceChannelStrategyInstance { + cfg: PriceChannelStrategy, + + highest: Highest, + lowest: Lowest, +} + +impl IndicatorInstance for PriceChannelStrategyInstance { + type Config = PriceChannelStrategy; + + fn name(&self) -> &str { + "PriceChannelStrategy" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + fn next(&mut self, candle: T) -> IndicatorResult { + let (high, low) = (candle.high(), candle.low()); + let highest = self.highest.next(high); + let lowest = self.lowest.next(low); + + let middle = (highest + lowest) * 0.5; + let delta = highest - middle; + + let upper = middle + delta * self.cfg.sigma; + let lower = middle - delta * self.cfg.sigma; + + // let signal_up = if candle.high() >= upper { 1 } else { 0 }; + // let signal_down = if candle.low() <= lower { 1 } else { 0 }; + let signal_up = (candle.high() >= upper) as i8; + let signal_down = (candle.low() <= lower) as i8; + + let signal = signal_up - signal_down; + + IndicatorResult::new(&[upper, lower], &[Action::from(signal)]) + } +} diff --git a/src/indicators/relative_strength_index.rs b/src/indicators/relative_strength_index.rs new file mode 100644 index 0000000..a0ddf71 --- /dev/null +++ b/src/indicators/relative_strength_index.rs @@ -0,0 +1,124 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, ValueType, OHLC}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Change, CrossAbove, CrossUnder}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RelativeStrengthIndex { + pub period: PeriodType, + pub zone: ValueType, + pub source: Source, + pub method: RegularMethods, +} + +impl IndicatorConfig for RelativeStrengthIndex { + fn validate(&self) -> bool { + self.period > 2 && self.zone > 0. && self.zone <= 0.5 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for RelativeStrengthIndex { + type Instance = RelativeStrengthIndexInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + change: Change::new(1, src), + posma: method(cfg.method, cfg.period, 0.), + negma: method(cfg.method, cfg.period, 0.), + cross_above: CrossAbove::default(), + cross_under: CrossUnder::default(), + cfg, + } + } +} + +impl Default for RelativeStrengthIndex { + fn default() -> Self { + Self { + period: 14, + zone: 0.3, + method: RegularMethods::RMA, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct RelativeStrengthIndexInstance { + cfg: RelativeStrengthIndex, + + change: Change, + posma: RegularMethod, + negma: RegularMethod, + cross_above: CrossAbove, + cross_under: CrossUnder, +} + +/// Just an alias for RelativeStrengthIndex +pub type RSI = RelativeStrengthIndex; + +impl IndicatorInstance for RelativeStrengthIndexInstance { + type Config = RelativeStrengthIndex; + + fn name(&self) -> &str { + "RelativeStrengthIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + + let change = self.change.next(src); + let pos: ValueType = self.posma.next(change.max(0.)); + let neg: ValueType = self.negma.next(change.min(0.)) * -1.; + + let value; + if pos != 0. || neg != 0. { + debug_assert!(pos + neg != 0.); + value = pos / (pos + neg) + } else { + value = 0.; + } + + let oversold = self.cross_under.next((value, self.cfg.zone)); + let overbought = self.cross_above.next((value, 1. - self.cfg.zone)); + let signal = oversold - overbought; + + IndicatorResult::new(&[value], &[signal]) + } +} diff --git a/src/indicators/relative_vigor_index.rs b/src/indicators/relative_vigor_index.rs new file mode 100644 index 0000000..2a93ad2 --- /dev/null +++ b/src/indicators/relative_vigor_index.rs @@ -0,0 +1,142 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, ValueType, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Cross, SMA, SWMA}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RelativeVigorIndex { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub method: RegularMethods, + pub zone: ValueType, +} + +impl IndicatorConfig for RelativeVigorIndex { + fn validate(&self) -> bool { + self.period1 >= 2 && self.zone >= 0. && self.zone <= 1. && self.period3 > 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 2) + } +} + +impl IndicatorInitializer for RelativeVigorIndex { + type Instance = RelativeVigorIndexInstance; + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let d_close = candle.close() - candle.open(); + let d_hl = candle.high() - candle.low(); + let rvi = if d_hl != 0. { d_close / d_hl } else { 0. }; + Self::Instance { + prev_close: candle.open(), + swma1: SWMA::new(cfg.period2, d_close), + sma1: SMA::new(cfg.period1, d_close), + swma2: SWMA::new(cfg.period2, d_hl), + sma2: SMA::new(cfg.period1, d_hl), + ma: method(cfg.method, cfg.period3, rvi), + cross: Cross::default(), + cfg, + } + } +} + +impl Default for RelativeVigorIndex { + fn default() -> Self { + Self { + period1: 10, + period2: 4, + period3: 4, + method: RegularMethods::SWMA, + zone: 0.25, + } + } +} + +#[derive(Debug)] +pub struct RelativeVigorIndexInstance { + cfg: RelativeVigorIndex, + + prev_close: ValueType, + swma1: SWMA, + sma1: SMA, + swma2: SWMA, + sma2: SMA, + ma: RegularMethod, + cross: Cross, +} + +impl IndicatorInstance for RelativeVigorIndexInstance { + type Config = RelativeVigorIndex; + + fn name(&self) -> &str { + "RelativeVigorIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let close_open = candle.close() - self.prev_close; + let high_low = candle.high() - candle.low(); + + self.prev_close = candle.close(); + + let swma1 = self.swma1.next(close_open); + let sma1 = self.sma1.next(swma1); + let swma2 = self.swma2.next(high_low); + let sma2 = self.sma2.next(swma2); + + let rvi = if sma2 != 0. { sma1 / sma2 } else { 0. }; + let sig: ValueType = self.ma.next(rvi); + + // let s2; + + let s1 = self.cross.next((rvi, sig)); + + // if s1.sign().unwrap_or_default() < 0 && rvi > self.cfg.zone && sig > self.cfg.zone { + // s2 = 1; + // } else if s1.sign().unwrap_or_default() > 0 && rvi < -self.cfg.zone && sig < -self.cfg.zone + // { + // s2 = -1; + // } else { + // s2 = 0; + // } + + let s2 = (s1.sign().unwrap_or_default() < 0 && rvi > self.cfg.zone && sig > self.cfg.zone) + as i8 - (s1.sign().unwrap_or_default() > 0 + && rvi < -self.cfg.zone + && sig < -self.cfg.zone) as i8; + + IndicatorResult::new(&[rvi, sig], &[s1, Action::from(s2)]) + } +} diff --git a/src/indicators/smi_ergodic_indicator.rs b/src/indicators/smi_ergodic_indicator.rs new file mode 100644 index 0000000..71e5bab --- /dev/null +++ b/src/indicators/smi_ergodic_indicator.rs @@ -0,0 +1,126 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, ValueType, OHLC}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Change, Cross, EMA}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SMIErgodicIndicator { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub method: RegularMethods, + pub source: Source, +} + +impl IndicatorConfig for SMIErgodicIndicator { + fn validate(&self) -> bool { + self.period1 > 1 && self.period2 > 1 && self.period3 > 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 1) + } +} + +impl IndicatorInitializer for SMIErgodicIndicator { + type Instance = SMIErgodicIndicatorInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + change: Change::new(1, src), + ema11: EMA::new(cfg.period1, 0.), + ema12: EMA::new(cfg.period2, 0.), + ema21: EMA::new(cfg.period1, 0.), + ema22: EMA::new(cfg.period2, 0.), + ma: method(cfg.method, cfg.period3, 0.), + cross: Cross::default(), + cfg, + } + } +} + +impl Default for SMIErgodicIndicator { + fn default() -> Self { + Self { + period1: 5, + period2: 20, + period3: 5, + method: RegularMethods::EMA, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct SMIErgodicIndicatorInstance { + cfg: SMIErgodicIndicator, + + change: Change, + ema11: EMA, + ema12: EMA, + ema21: EMA, + ema22: EMA, + ma: RegularMethod, + cross: Cross, +} + +impl IndicatorInstance for SMIErgodicIndicatorInstance { + type Config = SMIErgodicIndicator; + + fn name(&self) -> &str { + "SMIErgodicIndicator" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + let change = self.change.next(src); + + let temp_change = self.ema12.next(self.ema11.next(change)); + + let temp_abs_change = self.ema22.next(self.ema21.next(change.abs())); + + let smi = if temp_abs_change > 0. { + temp_change / temp_abs_change + } else { + 0. + }; + let sig: ValueType = self.ma.next(smi); + + let signal = self.cross.next((smi, sig)); + + IndicatorResult::new(&[smi, sig], &[signal]) + } +} diff --git a/src/indicators/stochastic_oscillator.rs b/src/indicators/stochastic_oscillator.rs new file mode 100644 index 0000000..268a2de --- /dev/null +++ b/src/indicators/stochastic_oscillator.rs @@ -0,0 +1,143 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, ValueType, OHLC}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Cross, CrossAbove, CrossUnder, Highest, Lowest}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct StochasticOscillator { + pub period: PeriodType, + pub smooth_k: PeriodType, + pub smooth_d: PeriodType, + pub zone: ValueType, + pub method: RegularMethods, +} + +impl IndicatorConfig for StochasticOscillator { + fn validate(&self) -> bool { + self.period > 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "smooth_k" => self.smooth_k = value.parse().unwrap(), + "smooth_d" => self.smooth_d = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + "method" => self.method = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 3) + } +} + +impl IndicatorInitializer for StochasticOscillator { + type Instance = StochasticOscillatorInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let k_rows = if candle.high() != candle.low() { + (candle.close() - candle.low()) / (candle.high() - candle.low()) + } else { + 0. + }; + + Self::Instance { + upper_zone: 1. - cfg.zone, + highest: Highest::new(cfg.period, candle.high()), + lowest: Lowest::new(cfg.period, candle.low()), + ma1: method(cfg.method, cfg.smooth_k, k_rows), + ma2: method(cfg.method, cfg.smooth_d, k_rows), + cross_over: Cross::default(), + cross_above1: CrossAbove::default(), + cross_under1: CrossUnder::default(), + cross_above2: CrossAbove::default(), + cross_under2: CrossUnder::default(), + cfg, + } + } +} + +impl Default for StochasticOscillator { + fn default() -> Self { + Self { + period: 14, + smooth_k: 1, + smooth_d: 3, + method: RegularMethods::SMA, + zone: 0.2, + } + } +} + +#[derive(Debug)] +pub struct StochasticOscillatorInstance { + cfg: StochasticOscillator, + + upper_zone: ValueType, + highest: Highest, + lowest: Lowest, + ma1: RegularMethod, + ma2: RegularMethod, + cross_over: Cross, + cross_above1: CrossAbove, + cross_under1: CrossUnder, + cross_above2: CrossAbove, + cross_under2: CrossUnder, +} + +impl IndicatorInstance for StochasticOscillatorInstance { + type Config = StochasticOscillator; + + fn name(&self) -> &str { + "StochasticOscillator" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let (close, high, low) = (candle.close(), candle.high(), candle.low()); + + let highest = self.highest.next(high); + let lowest = self.lowest.next(low); + + let k_rows = if highest != lowest { + (close - lowest) / (highest - lowest) + } else { + 0. + }; + + let f1 = self.ma1.next(k_rows); + let f2 = self.ma2.next(f1); + + let s1 = self.cross_above1.next((f1, self.cfg.zone)) + - self.cross_under1.next((f1, self.upper_zone)); + + let s2 = self.cross_above2.next((f2, self.cfg.zone)) + - self.cross_under2.next((f2, self.upper_zone)); + + let s3 = self.cross_over.next((f1, f2)); + + IndicatorResult::new(&[f1, f2], &[s1, s2, s3]) + } +} diff --git a/src/indicators/trix.rs b/src/indicators/trix.rs new file mode 100644 index 0000000..89e303c --- /dev/null +++ b/src/indicators/trix.rs @@ -0,0 +1,131 @@ +use crate::core::{ + IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult, Method, PeriodType, + Source, ValueType, OHLC, +}; +use crate::helpers::{method, RegularMethod, RegularMethods}; +use crate::methods::{Change, Cross, TMA}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +// Как идея: сигнал на покупку/продажу при пересекании графиком определённой зоны +// Такой сигнал не может служить как основной, но может служить как усиливающий +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Trix { + pub period1: PeriodType, + pub period2: PeriodType, + pub method2: RegularMethods, + pub source: Source, +} + +impl IndicatorConfig for Trix { + fn validate(&self) -> bool { + self.period1 > 2 && self.period2 > 1 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 3) + } +} + +impl IndicatorInitializer for Trix { + type Instance = TRIXInstance; + + fn init(self, candle: T) -> Self::Instance { + let src = candle.source(self.source); + + Self::Instance { + tma: TMA::new(self.period1, src), + sig: method(self.method2, self.period2, src), + change: Change::new(1, src), + cross1: Cross::new((), (src, src)), + cross2: Cross::new((), (src, src)), + prev_value: 0.0, + + cfg: self, + // phantom: PhantomData::default(), + } + } +} + +impl Default for Trix { + fn default() -> Self { + Self { + period1: 18, + period2: 6, // TODO: find recommended value here + method2: RegularMethods::EMA, + source: Source::Close, + } + } +} + +// https://en.wikipedia.org/wiki/Trix_(technical_analysis) +#[derive(Debug)] +pub struct TRIXInstance { + // { + cfg: Trix, + + tma: TMA, + sig: RegularMethod, + change: Change, + // pivot: Option, + cross1: Cross, + cross2: Cross, + prev_value: ValueType, + // phantom: PhantomData, +} + +impl IndicatorInstance for TRIXInstance { + type Config = Trix; + + fn config(&self) -> &Self::Config + where + Self: Sized, + { + &self.cfg + } + + #[inline] + fn next(&mut self, candle: T) -> IndicatorResult + where + Self: Sized, + { + let src = candle.source(self.cfg.source); + let tma = self.tma.next(src); + let value = self.change.next(tma); + // let signal1; + // if value > self.prev_value { + // signal1 = Action::BUY_ALL; + // } else if value < self.prev_value { + // signal1 = Action::SELL_ALL; + // } else { + // signal1 = Action::None; + // } + let signal1 = (value > self.prev_value) as i8 - (value < self.prev_value) as i8; + + let sigline = self.sig.next(value); + + let signal2 = self.cross1.next((value, sigline)); + let signal3 = self.cross2.next((value, 0.)); + + IndicatorResult::new(&[value], &[signal1.into(), signal2, signal3]) + } +} diff --git a/src/indicators/true_strength_index.rs b/src/indicators/true_strength_index.rs new file mode 100644 index 0000000..d6536c2 --- /dev/null +++ b/src/indicators/true_strength_index.rs @@ -0,0 +1,133 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::core::{Method, PeriodType, Source, ValueType, OHLC}; +use crate::methods::{Change, Cross, CrossAbove, CrossUnder, EMA}; + +// https://en.wikipedia.org/wiki/Trix_(technical_analysis) +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TrueStrengthIndex { + pub period1: PeriodType, + pub period2: PeriodType, + pub period3: PeriodType, + pub zone: ValueType, + pub source: Source, +} + +impl IndicatorConfig for TrueStrengthIndex { + fn validate(&self) -> bool { + self.period1 > 2 && self.zone >= 0. && self.zone <= 1. + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "period3" => self.period3 = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 3) + } +} + +impl IndicatorInitializer for TrueStrengthIndex { + type Instance = TrueStrengthIndexInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + change: Change::new(1, src), + ema11: EMA::new(cfg.period1, 0.), + ema12: EMA::new(cfg.period2, 0.), + ema21: EMA::new(cfg.period1, 0.), + ema22: EMA::new(cfg.period2, 0.), + ema: EMA::new(cfg.period3, 0.), + cross_under: CrossUnder::default(), + cross_above: CrossAbove::default(), + cross_over1: Cross::default(), + cross_over2: Cross::default(), + cfg, + } + } +} + +impl Default for TrueStrengthIndex { + fn default() -> Self { + Self { + period1: 25, + period2: 13, + period3: 13, + zone: 0.25, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct TrueStrengthIndexInstance { + cfg: TrueStrengthIndex, + + change: Change, + ema11: EMA, + ema12: EMA, + ema21: EMA, + ema22: EMA, + ema: EMA, + cross_under: CrossUnder, + cross_above: CrossAbove, + cross_over1: Cross, + cross_over2: Cross, +} + +impl IndicatorInstance for TrueStrengthIndexInstance { + type Config = TrueStrengthIndex; + + fn name(&self) -> &str { + "TrueStrengthIndex" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + let m1 = self.change.next(src); + let m2 = m1.abs(); + let ema11 = self.ema11.next(m1); + let ema12 = self.ema12.next(ema11); + let ema21 = self.ema21.next(m2); + let ema22 = self.ema22.next(ema21); + + let value = if ema22 != 0. { ema12 / ema22 } else { 0. }; + + let sig = self.ema.next(value); + + let s1 = self.cross_under.next((value, -self.cfg.zone)) + - self.cross_above.next((value, self.cfg.zone)); + let s2 = self.cross_over1.next((value, 0.)); + let s3 = self.cross_over2.next((value, sig)); + + IndicatorResult::new(&[value, sig], &[s1, s2, s3]) + } +} diff --git a/src/indicators/vidya.rs b/src/indicators/vidya.rs new file mode 100644 index 0000000..27448e9 --- /dev/null +++ b/src/indicators/vidya.rs @@ -0,0 +1,162 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::core::{Action, Method, PeriodType, Source, ValueType, Window, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::methods::Change; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Vidya { + pub period: PeriodType, + pub zone: ValueType, + pub source: Source, +} + +impl IndicatorConfig for Vidya { + fn validate(&self) -> bool { + self.period > 1 && self.zone >= 0. && self.zone <= 5. + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period" => self.period = value.parse().unwrap(), + "zone" => self.zone = value.parse().unwrap(), + "source" => self.source = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (1, 1) + } +} + +impl IndicatorInitializer for Vidya { + type Instance = VidyaInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + let src = candle.source(cfg.source); + Self::Instance { + f: 2. / (1 + cfg.period) as ValueType, + up_sum: 0., + dn_sum: 0., + last_value: src, + last_result: src, + window: Window::new(cfg.period, 0.), + change: Change::new(1, src), + last_signal: 0, + cfg, + } + } +} + +impl Default for Vidya { + fn default() -> Self { + Self { + period: 10, + zone: 0.01, + source: Source::Close, + } + } +} + +#[derive(Debug)] +pub struct VidyaInstance { + cfg: Vidya, + + f: ValueType, + up_sum: ValueType, + dn_sum: ValueType, + last_value: ValueType, + last_result: ValueType, + window: Window, + change: Change, + last_signal: i8, +} + +impl IndicatorInstance for VidyaInstance { + type Config = Vidya; + + fn name(&self) -> &str { + "Vidya" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let src = candle.source(self.cfg.source); + + let change = self.change.next(src); + + let left_change = self.window.push(change); + + if left_change > 0. { + self.up_sum -= left_change; + } else if left_change < 0. { + self.dn_sum -= left_change.abs(); + } + + if change > 0. { + self.up_sum += change; + } else if change < 0. { + self.dn_sum += change.abs(); + } + + let value; + + if self.up_sum == 0. && self.dn_sum == 0. { + value = self.last_result; + } else { + let cmo = ((self.up_sum - self.dn_sum) / (self.up_sum + self.dn_sum)).abs(); + let f_cmo = self.f * cmo; + let result = src * f_cmo + (1.0 - f_cmo) * self.last_result; + value = result; + self.last_result = result; + } + + self.last_value = src; + + let upper_zone = 1.0 + self.cfg.zone; + let lower_zone = 1.0 - self.cfg.zone; + + let signal; + + if value * upper_zone > src { + if self.last_signal != -1 { + signal = -1; + } else { + signal = 0; + } + } else if value * lower_zone < src { + if self.last_signal != 1 { + signal = 1; + } else { + signal = 0; + } + } else { + signal = 0; + } + + if signal != 0 { + self.last_signal = signal; + } + + IndicatorResult::new(&[value], &[Action::from(signal)]) + } +} diff --git a/src/indicators/woodies_cci.rs b/src/indicators/woodies_cci.rs new file mode 100644 index 0000000..c501a1b --- /dev/null +++ b/src/indicators/woodies_cci.rs @@ -0,0 +1,162 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::{CommodityChannelIndex, CommodityChannelIndexInstance}; +use crate::core::{Action, Method, PeriodType, ValueType, Window, OHLC}; +use crate::core::{IndicatorConfig, IndicatorInitializer, IndicatorInstance, IndicatorResult}; +use crate::helpers::signi; +use crate::methods::{Cross, CrossAbove, CrossUnder, SMA}; + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct WoodiesCCI { + pub period1: PeriodType, + pub period2: PeriodType, + pub signal1_period: PeriodType, + pub signal2_bars_count: isize, + pub signal3_zone: ValueType, +} + +impl IndicatorConfig for WoodiesCCI { + fn validate(&self) -> bool { + self.period1 > self.period2 + } + + fn set(&mut self, name: &str, value: String) { + match name { + "period1" => self.period1 = value.parse().unwrap(), + "period2" => self.period2 = value.parse().unwrap(), + "signal1_period" => self.signal1_period = value.parse().unwrap(), + "signal1_bars_count" => self.signal2_bars_count = value.parse().unwrap(), + "signal3_zone" => self.signal3_zone = value.parse().unwrap(), + + _ => { + dbg!(format!( + "Unknown attribute `{:}` with value `{:}` for `{:}`", + name, + value, + std::any::type_name::(), + )); + } + }; + } + + fn size(&self) -> (u8, u8) { + (2, 3) + } +} + +impl IndicatorInitializer for WoodiesCCI { + type Instance = WoodiesCCIInstance; + + fn init(self, candle: T) -> Self::Instance + where + Self: Sized, + { + let cfg = self; + + let mut cci1 = CommodityChannelIndex::default(); + cci1.period = cfg.period1; + let mut cci2 = CommodityChannelIndex::default(); + cci2.period = cfg.period2; + + Self::Instance { + cci1: cci1.init(candle), + cci2: cci2.init(candle), + sma: SMA::new(cfg.signal1_period, 0.), + cross1: Cross::default(), + cross2: Cross::default(), + s2_sum: 0, + s3_sum: 0., + s3_count: 0, + window: Window::new(cfg.signal2_bars_count as PeriodType, 0), + cross_above: CrossAbove::default(), + cross_under: CrossUnder::default(), + cfg, + } + } +} + +impl Default for WoodiesCCI { + fn default() -> Self { + Self { + period1: 14, + period2: 6, + signal1_period: 9, + signal2_bars_count: 6, + signal3_zone: 0.2, + } + } +} + +#[derive(Debug)] +pub struct WoodiesCCIInstance { + cfg: WoodiesCCI, + + cci1: CommodityChannelIndexInstance, + cci2: CommodityChannelIndexInstance, + sma: SMA, + cross1: Cross, + cross2: Cross, + s2_sum: isize, + s3_sum: ValueType, + s3_count: PeriodType, + window: Window, + cross_above: CrossAbove, + cross_under: CrossUnder, +} + +impl IndicatorInstance for WoodiesCCIInstance { + type Config = WoodiesCCI; + + fn name(&self) -> &str { + "WoodiesCCI" + } + + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + fn next(&mut self, candle: T) -> IndicatorResult { + let cci1 = self.cci1.next(candle).value(0); + let cci2 = self.cci2.next(candle).value(0); + + let cci1_sign = signi(cci1); + let d_cci = cci1 - cci2; + let sma = self.sma.next(d_cci); + let s1 = self.cross1.next((sma, 0.)); + + let s0 = self.cross2.next((cci1, 0.)); + self.s2_sum += (cci1_sign - self.window.push(cci1_sign)) as isize; + + // let s2; + // if self.s2_sum >= self.cfg.signal2_bars_count { + // s2 = 1; + // } else if self.s2_sum <= -self.cfg.signal2_bars_count { + // s2 = -1; + // } else { + // s2 = 0; + // } + let s2 = (self.s2_sum >= self.cfg.signal2_bars_count) as i8 + - (self.s2_sum <= -self.cfg.signal2_bars_count) as i8; + + // if s0.is_some() { + // self.s3_sum = 0.; + // self.s3_count = 0; + // } + + let is_none = s0.is_none(); + self.s3_sum *= is_none as i8 as ValueType; + self.s3_count *= is_none as PeriodType; + + self.s3_sum += cci1; + self.s3_count += 1; + + let s3v = self.s3_sum / self.s3_count as ValueType; + let s3 = self.cross_above.next((s3v, self.cfg.signal3_zone)) + - self.cross_under.next((s3v, -self.cfg.signal3_zone)); + + IndicatorResult::new(&[cci1, cci2], &[s1, Action::from(s2), s3]) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8c83652 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,50 @@ +#![warn( + missing_docs, + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces, + unused_qualifications +)] + +//! Yet Another Technical Analysis library +//! +//! YaTA implements most common technical analysis [methods](crate::methods) and [indicators](crate::indicators) +//! +//! It also provides you an iterface to create your own indicators. +//! +//! Some commonly used methods: +//! * [ADI](crate::methods::ADI) Accumulation-distribution index; +//! * [Cross](crate::methods::Cross) / [CrossAbove](crate::methods::CrossAbove) / [CrossUnder](crate::methods::CrossUnder); +//! * [Derivative](crate::methods::Derivative) (differential); +//! * [Highest](crate::methods::Highest) / [Lowest](crate::methods::Lowest) / [Highest - Lowest Delta](crate::methods::HighestLowestDelta); +//! * [HMA](crate::methods::HMA) Hull moving average; +//! * [Integral](crate::methods::Integral) (sum); +//! * [LinReg](crate::methods::LinReg) Linear regression moving average; +//! * [Momentum](crate::methods::Momentum); +//! * [Pivot points](crate::methods::PivotSignal); +//! * [SMA](crate::methods::SMA) Simple moving average; +//! * [WMA](crate::methods::WMA) Weighted moving average; +//! * [VWMA](crate::methods::VWMA) Volume weighted moving average; +//! * [EMA](crate::methods::EMA), [DMA](crate::methods::DMA), [TMA](crate::methods::TMA), [DEMA](crate::methods::DEMA), [TEMA](crate::methods::TEMA) Exponential moving average family; +//! * [SWMA](crate::methods::SWMA) Symmetrically weighted moving average. +//! +//! And many others: [See Full list](crate::methods#structs) +//! +//! # Current usafe status +//! Currently there is no `unsafe` code in the crate. + +pub mod core; +pub mod helpers; +pub mod indicators; +pub mod methods; + +/// Contains main traits you need to start using this library +pub mod prelude { + pub use super::core::{ + IndicatorConfig, IndicatorInitializer, IndicatorInstance, Method, OHLC, OHLCV, + }; +} diff --git a/src/methods/adi.rs b/src/methods/adi.rs new file mode 100644 index 0000000..d0f4a64 --- /dev/null +++ b/src/methods/adi.rs @@ -0,0 +1,228 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window, OHLCV}; +use std::marker::PhantomData; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Accumulation Distribution Index](https://en.wikipedia.org/wiki/Accumulation/distribution_index) of specified `length` for timeseries of [`OHLCV`] +/// +/// [`CLV`] ranges from -1 when the close is the low of the day, to +1 when it's the high. +/// For instance if the close is 3/4 the way up the range then [`CLV`] is +0.5. +/// The accumulation/distribution index adds up volume multiplied by the [`CLV`] factor, i.e. +/// +/// ADI = ADI_prev + [`CLV`] * [`volume`] +/// +/// The name accumulation/distribution comes from the idea that during accumulation buyers are in control +/// and the price will be bid up through the day, or will make a recovery if sold down, in either case +/// more often finishing near the day's high than the low. The opposite applies during distribution. +/// +/// The accumulation/distribution index is similar to on balance volume, but acc/dist is based on the close +/// within the day's range, instead of the close-to-close up or down that the latter uses. +/// +/// Can be used by a shortcut [ADI] +/// +/// Used in indicators: [Chaikin Money Flow](crate::indicators::ChaikinMoneyFlow), [Chaikin Oscillator](crate::indicators::ChaikinOscillator) +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// When `length` == 0, ADI becomes windowless. That means full ADI value accumulation over time. +/// +/// When `length` > 0, ADI will be calculated over the last `length` values. +/// +/// # Input type +/// Input type is [`OHLCV`] +/// +/// # Output type +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::ADI; +/// use yata::helpers::RandomCandles; +/// +/// let mut candles = RandomCandles::default(); +/// let mut windowless = ADI::new(0, candles.first()); +/// let mut windowed = ADI::new(3, candles.first()); // <----- Window size 3 +/// +/// let candle = candles.next().unwrap(); +/// assert_ne!(windowless.next(candle), windowed.next(candle)); +/// +/// let candle = candles.next().unwrap(); +/// assert_ne!(windowless.next(candle), windowed.next(candle)); +/// +/// let candle = candles.next().unwrap(); +/// assert!((windowless.next(candle)-windowed.next(candle)).abs() < 1e-10); // Must be equal here +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [ADI] +/// +/// [`OHLC`]: crate::core::OHLC +/// [`OHLCV`]: crate::core::OHLCV +/// [`volume`]: crate::core::OHLCV::volume +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// [`CLV`]: crate::core::OHLC::clv +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ADI { + length: PeriodType, + cmf_sum: ValueType, + window: Window, + phantom: PhantomData, +} + +impl ADI { + /// Returns last calculated value + pub fn get_value(&self) -> ValueType { + self.cmf_sum + } +} + +impl Method for ADI { + type Params = PeriodType; + type Input = T; + type Output = ValueType; + + fn new(length: Self::Params, candle: Self::Input) -> Self { + let mut cmf_sum = 0.0; + let window; + + if length > 0 { + let clvv = candle.clv() * candle.volume(); + cmf_sum = clvv * length as ValueType; + window = Window::new(length, clvv); + } else { + window = Window::empty(); + } + + Self { + length, + + cmf_sum, + window, + phantom: PhantomData::default(), + } + } + + #[inline] + fn next(&mut self, candle: Self::Input) -> Self::Output { + let clvv = candle.clv() * candle.volume(); + self.cmf_sum += clvv; + + if self.length > 0 { + self.cmf_sum -= self.window.push(clvv); + } + + self.cmf_sum + } +} + +#[cfg(test)] +mod tests { + use super::ValueType; + #[allow(dead_code)] + const SIGMA: ValueType = 1e-4; + + #[test] + fn test_adi_const() { + use super::ADI; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + let candle = Candle { + open: 121.0, + high: 133.0, + low: 49.0, + close: 70.0, + volume: 531.0, + }; + + for i in 1..30 { + let mut adi = ADI::new(i, candle); + let output = adi.next(candle); + + test_const(&mut adi, candle, output); + } + } + + #[test] + #[should_panic] + fn test_adi_windowless_const() { + use super::ADI; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + let candle = Candle { + open: 121.0, + high: 133.0, + low: 49.0, + close: 70.0, + volume: 531.0, + }; + + let mut adi = ADI::new(0, candle); + let output = adi.next(candle); + + test_const(&mut adi, candle, output); + } + + #[test] + fn test_adi() { + use crate::core::Method as _; + use crate::core::{OHLC, OHLCV}; + use crate::helpers::RandomCandles; + use crate::methods::ADI; + + let mut candles = RandomCandles::default(); + let mut adi = ADI::new(0, candles.first()); + + candles.take(100).fold(0., |s, candle| { + assert_eq!(adi.next(candle), s + candle.clv() * candle.volume()); + s + candle.clv() * candle.volume() + }); + } + + #[test] + fn test_adi_windowed() { + use crate::core::Method as _; + use crate::helpers::RandomCandles; + use crate::methods::ADI; + + let mut candles = RandomCandles::default(); + let mut adi = ADI::new(0, candles.first()); + let mut adiw = [ + ADI::new(1, candles.first()), + ADI::new(2, candles.first()), + ADI::new(3, candles.first()), + ADI::new(4, candles.first()), + ADI::new(5, candles.first()), + ]; + + candles + .take(adiw.len()) + .enumerate() + .for_each(|(i, candle)| { + let v1 = adi.next(candle); + + adiw.iter_mut().enumerate().for_each(|(j, adiw)| { + let v2 = adiw.next(candle); + if i == j { + assert!((v1 - v2).abs() < SIGMA, "{}, {}", v1, v2); + } else { + assert!((v1 - v2).abs() >= SIGMA, "{}, {}", v1, v2); + } + }); + }); + } +} diff --git a/src/methods/conv.rs b/src/methods/conv.rs new file mode 100644 index 0000000..4506c2f --- /dev/null +++ b/src/methods/conv.rs @@ -0,0 +1,154 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Convolution Moving Average with specified `weights` for timeseries of [`ValueType`]. +/// +/// # Parameters +/// +/// Has a single parameter `weights`: Vec<[`ValueType`]> +/// +/// `weights` vector's length must be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Perfomance +/// +/// O(length(`weights`)) +/// +/// This method is relatively slow compare to the other methods. +/// +/// # See also +/// +/// [`WMA`](crate::methods::WMA), [`SWMA`](crate::methods::SWMA) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Conv { + weights: Vec, + window: Window, + wsum_invert: ValueType, + + initialized: bool, +} + +impl Method for Conv { + type Params = Vec; + type Input = ValueType; + type Output = Self::Input; + + fn new(weights: Self::Params, value: Self::Input) -> Self { + debug_assert!(weights.len() > 0, "Conv: weights length must be > 0"); + + let wsum_invert = weights.iter().sum::().recip(); + + Self { + window: Window::new(weights.len() as PeriodType, value), + weights, + wsum_invert, + + initialized: false, + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.window.push(value); + self.window + .iter() + .zip(self.weights.iter()) + .fold(0., |s, (value, &weight)| value.mul_add(weight, s)) + * self.wsum_invert + } +} + +#[cfg(test)] +mod tests { + // #![allow(unused_imports)] + use super::{Conv as TestingMethod, Method}; + use crate::core::{PeriodType, ValueType}; + use crate::helpers::RandomCandles; + + // #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + fn get_weights(length: PeriodType) -> Vec { + (0..length) + .map(|i| { + let i_f = i as ValueType; + i_f.sin().abs() * i_f + 1.0 + }) + .collect() + } + + #[test] + fn test_conv_const() { + use super::*; + use crate::core::Method; + use crate::methods::tests::test_const_float; + + for i in 1..30 { + let weights = get_weights(i); + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(weights, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_conv1() { + let mut candles = RandomCandles::default(); + + let weights = get_weights(1); + let mut ma = TestingMethod::new(weights, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_conv() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..30).for_each(|weights_count| { + let mut weights = get_weights(weights_count); + let wsum: ValueType = weights.iter().sum(); + + let mut ma = TestingMethod::new(weights.clone(), src[0]); + weights.reverse(); + + src.iter().enumerate().for_each(|(i, &x)| { + let wcv = weights + .iter() + .enumerate() + .fold(0.0, |sum, (j, &w)| sum + w * src[i.saturating_sub(j)]); + + let value = ma.next(x); + let value2 = wcv / wsum; + assert!( + (value2 - value).abs() < SIGMA, + "{}, {}, {:?}", + value, + value2, + &weights + ); + }); + }); + } +} diff --git a/src/methods/cross.rs b/src/methods/cross.rs new file mode 100644 index 0000000..7038792 --- /dev/null +++ b/src/methods/cross.rs @@ -0,0 +1,392 @@ +use crate::core::Method; +use crate::core::{Action, ValueType}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Searches for two timeseries lines of type [`ValueType`] cross each other. +/// +/// If `value` crossed `base` upwards, then returns [Action::BUY_ALL](crate::core::Action::BUY_ALL) +/// +/// If `value` crossed `base` downwards, then returns [Action::SELL_ALL](crate::core::Action::SELL_ALL) +/// +/// Else (if series did not cross each other) returns [Action::None](crate::core::Action::None) +/// +/// # Parameters +/// +/// Has no parameters +/// +/// # Input type +/// +/// Input type is (`value`: [`ValueType`], `base`: [`ValueType`]) +/// +/// # Output type +/// +/// Output type is [`Action`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::Cross; +/// +/// let mut cross = Cross::default(); +/// +/// let t1 = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; +/// let t2 = vec![5.0, 3.0, 1.8, 2.9, 4.1, 5.6]; +/// let r = vec![ 0, 0, 1, 0, -1, 0 ]; +/// +/// (0..t1.len()).for_each(|i| { +/// let value = t1[i]; +/// let base = t2[i]; +/// let cross_value = cross.next((value, base)).analog(); +/// assert_eq!(cross_value, r[i]); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [CrossAbove], [CrossUnder] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// [`Action`]: crate::core::Action +#[derive(Debug, Default, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Cross { + up: CrossAbove, + down: CrossUnder, +} + +impl Method for Cross { + type Params = (); + type Input = (ValueType, ValueType); + type Output = Action; + + fn new(_: Self::Params, value: Self::Input) -> Self + where + Self: Sized, + { + Self { + up: CrossAbove::new((), value), + down: CrossUnder::new((), value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let up = self.up.binary(value.0, value.1); + let down = self.down.binary(value.0, value.1); + + ((up as i8) - (down as i8)).into() + } +} + +/// Searches for `value` timeseries line crosses `base` line upwards +/// +/// If `value` crossed `base` upwards, then returns [Action::BUY_ALL](crate::core::Action::BUY_ALL) +/// +/// Else returns [Action::None](crate::core::Action::None) +/// +/// # Parameters +/// +/// Has no parameters +/// +/// # Input type +/// +/// Input type is (`value`: [`ValueType`], `base`: [`ValueType`]) +/// +/// # Output type +/// +/// Output type is [`Action`] +/// +/// # Examples +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::CrossAbove; +/// +/// let mut cross_above = CrossAbove::new((), (0.0, 5.0)); +/// +/// let t1 = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; +/// let t2 = vec![5.0, 3.0, 1.8, 2.9, 4.1, 5.6]; +/// let r = vec![ 0, 0, 1, 0, 0, 0 ]; +/// +/// (0..t1.len()).for_each(|i| { +/// let value = t1[i]; +/// let base = t2[i]; +/// let cross_value = cross_above.next((value, base)).analog(); +/// assert_eq!(cross_value, r[i]); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [Cross], [CrossUnder] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// [`DigitalSignal`]: crate::core::DigitalSignal +#[derive(Debug, Default, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CrossAbove { + last_delta: ValueType, +} + +impl CrossAbove { + /// Returns `true` when value1 crosses `value2` timeseries upwards + /// Otherwise returns `false` + #[inline] + pub fn binary(&mut self, value1: ValueType, value2: ValueType) -> bool { + let last_delta = self.last_delta; + let current_delta = value1 - value2; + + self.last_delta = current_delta; + + last_delta < 0. && current_delta >= 0. + } +} + +impl Method for CrossAbove { + type Params = (); + type Input = (ValueType, ValueType); + type Output = Action; + + fn new(_: Self::Params, value: Self::Input) -> Self + where + Self: Sized, + { + Self { + last_delta: value.0 - value.1, + ..Self::default() + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + Action::from(self.binary(value.0, value.1) as i8) + } +} + +/// Searches for `value` timeseries line crosses `base` line downwards +/// +/// If `value` crossed `base` downwards, then returns [Action::BUY_ALL](crate::core::Action::BUY_ALL) +/// +/// Else returns [Action::None](crate::core::Action::None) +/// +/// # Parameters +/// +/// Has no parameters +/// +/// # Input type +/// +/// Input type is (`value`: [`ValueType`], `base`: [`ValueType`]) +/// +/// # Output type +/// +/// Output type is [`Action`] +/// +/// # Examples +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::CrossUnder; +/// +/// let mut cross_under = CrossUnder::new((), (0.0, 5.0)); +/// +/// let t1 = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; +/// let t2 = vec![5.0, 3.0, 1.8, 2.9, 4.1, 5.6]; +/// let r = vec![ 0, 0, 0, 0, 1, 0 ]; +/// +/// (0..t1.len()).for_each(|i| { +/// let value = t1[i]; +/// let base = t2[i]; +/// let cross_value = cross_under.next((value, base)).analog(); +/// assert_eq!(cross_value, r[i]); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [Cross], [CrossAbove] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// [`DigitalSignal`]: crate::core::DigitalSignal +#[derive(Debug, Default, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CrossUnder { + last_delta: ValueType, +} + +impl CrossUnder { + /// Returns `true` when value1 crosses `value2` timeseries downwards + /// Otherwise returns `false` + #[inline] + pub fn binary(&mut self, value1: ValueType, value2: ValueType) -> bool { + let last_delta = self.last_delta; + let current_delta = value1 - value2; + + self.last_delta = current_delta; + + last_delta > 0. && current_delta <= 0. + } +} + +impl Method for CrossUnder { + type Params = (); + type Input = (ValueType, ValueType); + type Output = Action; + + fn new(_: Self::Params, value: Self::Input) -> Self + where + Self: Sized, + { + Self { + last_delta: value.0 - value.1, + ..Self::default() + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + Action::from(self.binary(value.0, value.1) as i8) + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[test] + fn test_cross_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + let input = (7.0, 1.0); + let mut cross = Cross::new((), input); + let output = cross.next(input); + + test_const(&mut cross, input, output); + } + + #[test] + fn test_cross() { + use super::Cross as TestingMethod; + use crate::core::Method; + + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + let avg = src.iter().sum::() / src.len() as ValueType; + + let mut ma = TestingMethod::new((), (src[0], avg)); + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next((x, avg)).analog(); + + let value2; + if x > avg && src[i.saturating_sub(1)] < avg { + value2 = 1; + } else if x < avg && src[i.saturating_sub(1)] > avg { + value2 = -1; + } else { + value2 = 0; + } + + assert_eq!(value1, value2, "{}, {} at index {}", value2, value1, i); + }); + } + #[test] + fn test_cross_above_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + let input = (7.0, 1.0); + let mut cross = CrossAbove::new((), input); + let output = cross.next(input); + + test_const(&mut cross, input, output); + } + + #[test] + fn test_cross_above() { + use super::CrossAbove as TestingMethod; + use crate::core::Method; + + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + let avg = src.iter().sum::() / src.len() as ValueType; + + let mut ma = TestingMethod::new((), (src[0], avg)); + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next((x, avg)).analog(); + + let value2; + if x > avg && src[i.saturating_sub(1)] < avg { + value2 = 1; + } else { + value2 = 0; + } + + assert_eq!(value1, value2, "{}, {} at index {}", value2, value1, i); + }); + } + + #[test] + fn test_cross_under_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + let input = (7.0, 1.0); + let mut cross = CrossUnder::new((), input); + let output = cross.next(input); + + test_const(&mut cross, input, output); + } + + #[test] + fn test_cross_under() { + use super::CrossUnder as TestingMethod; + use crate::core::Method; + + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + let avg = src.iter().sum::() / src.len() as ValueType; + + let mut ma = TestingMethod::new((), (src[0], avg)); + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next((x, avg)).analog(); + + let value2; + if x < avg && src[i.saturating_sub(1)] > avg { + value2 = 1; + } else { + value2 = 0; + } + + assert_eq!(value1, value2, "{}, {} at index {}", value2, value1, i); + }); + } +} diff --git a/src/methods/derivative.rs b/src/methods/derivative.rs new file mode 100644 index 0000000..49db776 --- /dev/null +++ b/src/methods/derivative.rs @@ -0,0 +1,145 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Derivative](https://en.wikipedia.org/wiki/Derivative) of specified window `length` for timeseries of [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// Default is 1 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::Derivative; +/// +/// let s = vec![0.0, 1.0, 3.0, 0.5, 2.0, -10.0]; +/// let r = vec![0.0, 1.0, 2.0,-2.5, 1.5, -12.0]; +/// +/// let mut derivative = Derivative::new(1, s[0]); +/// +/// (0..s.len()).for_each(|i| { +/// let der = derivative.next(s[i]); +/// assert_eq!(der, r[i]); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [`Integral`](crate::methods::Integral), [`Rate of Change`](crate::methods::RateOfChange), [`Momentum`](crate::methods::Momentum) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Derivative { + divider: ValueType, + window: Window, + initialized: bool, +} + +/// Just an alias for Derivative +pub type Differential = Derivative; + +impl Method for Derivative { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + Self { + divider: (length as ValueType).recip(), + window: Window::new(length, value), + initialized: false, + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let prev_value = self.window.push(value); + (value - prev_value) * self.divider + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Derivative as TestingMethod, Method}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_derivative_const() { + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + test_const(&mut method, input, 0.0); + } + } + + #[test] + fn test_derivative1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + let mut prev = None; + + candles.take(100).map(|x| x.close).for_each(|x| { + assert!(((x - prev.unwrap_or(x)) - ma.next(x)).abs() < SIGMA); + prev = Some(x); + }); + } + + #[test] + fn test_derivative() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + let mut value2 = src[0]; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + + value2 = (x - src[i.saturating_sub(length as usize)]) / (length as ValueType); + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value, + i, + length + ); + }); + }); + } +} diff --git a/src/methods/ema.rs b/src/methods/ema.rs new file mode 100644 index 0000000..1970686 --- /dev/null +++ b/src/methods/ema.rs @@ -0,0 +1,618 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Exponential Moving Average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) of specified `length` for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::EMA; +/// +/// // EMA of length=3 +/// let mut ema = EMA::new(3, 3.0); +/// +/// ema.next(3.0); +/// ema.next(6.0); +/// +/// assert_eq!(ema.next(9.0), 6.75); +/// assert_eq!(ema.next(12.0), 9.375); +/// ``` +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [DMA], [DEMA], [TMA], [TEMA], [RMA](crate::methods::RMA) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct EMA { + alpha: ValueType, + value: ValueType, +} + +impl Method for EMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + #[inline] + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "EMA: length should be > 0"); + + let alpha = 2. / ((length + 1) as ValueType); + Self { alpha, value } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.value = (value - self.value).mul_add(self.alpha, self.value); + + self.value + } +} + +/// Simple shortcut for [EMA] over [EMA] +/// +/// # See also +/// +/// [EMA], [DEMA], [TMA], [TEMA], [RMA](crate::methods::RMA) +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DMA { + ema: EMA, + dma: EMA, +} + +impl Method for DMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "DMA: length should be > 0"); + + Self { + ema: EMA::new(length, value), + dma: EMA::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.dma.next(self.ema.next(value)) + } +} + +/// Simple shortcut for [EMA] over [EMA] over [EMA] (or [EMA] over [DMA], or [DMA] over [EMA]) +/// +/// # See also +/// +/// [EMA], [DMA], [DEMA], [TEMA], [RMA](crate::methods::RMA) +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TMA { + dma: DMA, + tma: EMA, +} + +impl Method for TMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "TMA: length should be > 0"); + + Self { + dma: DMA::new(length, value), + tma: EMA::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.tma.next(self.dma.next(value)) + } +} + +/// [Double Exponential Moving Average](https://en.wikipedia.org/wiki/Double_exponential_moving_average) of specified `length` for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::DEMA; +/// +/// // DEMA of length=3 +/// let mut dema = DEMA::new(3, 1.0); +/// +/// dema.next(1.0); +/// dema.next(2.0); +/// +/// assert_eq!(dema.next(3.0), 2.75); +/// assert_eq!(dema.next(4.0), 3.8125); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// +/// # See also +/// +/// [EMA], [DMA], [TMA], [TEMA], [RMA](crate::methods::RMA) +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DEMA { + ema: EMA, + dma: EMA, +} + +impl Method for DEMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "DEMA: length should be > 0"); + + Self { + ema: EMA::new(length, value), + dma: EMA::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let ema = self.ema.next(value); + let dma = self.dma.next(ema); + + // 2. * ema - dma + ema.mul_add(2., -dma) + } +} + +/// [Triple Exponential Moving Average](https://en.wikipedia.org/wiki/Triple_exponential_moving_average) of specified `length` for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::TEMA; +/// +/// // TEMA of length=3 +/// let mut tema = TEMA::new(3, 1.0); +/// +/// tema.next(1.0); +/// tema.next(2.0); +/// +/// assert_eq!(tema.next(3.0), 2.9375); +/// assert_eq!(tema.next(4.0), 4.0); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// +/// # See also +/// +/// [EMA], [DMA], [DEMA], [TMA], [RMA](crate::methods::RMA) +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TEMA { + ema: EMA, + dma: EMA, + tma: EMA, +} + +impl Method for TEMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "TEMA: length should be > 0"); + + Self { + ema: EMA::new(length, value), + dma: EMA::new(length, value), + tma: EMA::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let ema = self.ema.next(value); + let dma = self.dma.next(ema); + let tma = self.tma.next(dma); + + // 3. * (ema - dma) + tma + (ema - dma).mul_add(3., tma) + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_ema_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = EMA::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_ema1() { + use super::{Method, EMA as TestingMethod}; + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_ema() { + use super::{Method, EMA as TestingMethod}; + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + let alpha = 2. / (length + 1) as ValueType; + + let mut prev_value = src[0]; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + + let value2 = alpha * x + (1. - alpha) * prev_value; + + prev_value = value2; + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value, + i, + length + ); + }); + }); + } + + #[test] + fn test_dma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = DMA::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_dma1() { + use super::{Method, DMA as TestingMethod}; + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_dma() { + use super::{Method, DMA as TestingMethod}; + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + let alpha = 2. / (length + 1) as ValueType; + + let mut prev_value1 = src[0]; + let mut prev_value2 = src[0]; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + + let ema1 = alpha * x + (1. - alpha) * prev_value1; + let ema2 = alpha * ema1 + (1. - alpha) * prev_value2; + + prev_value1 = ema1; + prev_value2 = ema2; + + let value2 = ema2; + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value, + i, + length + ); + }); + }); + } + + #[test] + fn test_dema_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = DEMA::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_dema1() { + use super::{Method, DEMA as TestingMethod}; + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_dema() { + use super::{Method, DEMA as TestingMethod}; + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + let alpha = 2. / (length + 1) as ValueType; + + let mut prev_value1 = src[0]; + let mut prev_value2 = src[0]; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + + let ema1 = alpha * x + (1. - alpha) * prev_value1; + let ema2 = alpha * ema1 + (1. - alpha) * prev_value2; + + prev_value1 = ema1; + prev_value2 = ema2; + + let value2 = 2. * ema1 - ema2; + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value, + i, + length + ); + }); + }); + } + + #[test] + fn test_tma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TMA::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_tma1() { + use super::{Method, TMA as TestingMethod}; + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_tma() { + use super::{Method, TMA as TestingMethod}; + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + let alpha = 2. / (length + 1) as ValueType; + + let mut prev_value1 = src[0]; + let mut prev_value2 = src[0]; + let mut prev_value3 = src[0]; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + + let ema1 = alpha * x + (1. - alpha) * prev_value1; + let ema2 = alpha * ema1 + (1. - alpha) * prev_value2; + let ema3 = alpha * ema2 + (1. - alpha) * prev_value3; + + prev_value1 = ema1; + prev_value2 = ema2; + prev_value3 = ema3; + + let value2 = ema3; + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value, + i, + length + ); + }); + }); + } + + #[test] + fn test_tema_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TEMA::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_tema1() { + use super::{Method, TEMA as TestingMethod}; + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_tema() { + use super::{Method, TEMA as TestingMethod}; + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + let alpha = 2. / (length + 1) as ValueType; + + let mut prev_value1 = src[0]; + let mut prev_value2 = src[0]; + let mut prev_value3 = src[0]; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + + let ema = alpha * x + (1. - alpha) * prev_value1; + let dma = alpha * ema + (1. - alpha) * prev_value2; + let tma = alpha * dma + (1. - alpha) * prev_value3; + + prev_value1 = ema; + prev_value2 = dma; + prev_value3 = tma; + + let value2 = 3. * ema - 3. * dma + tma; + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value, + i, + length + ); + }); + }); + } +} diff --git a/src/methods/highest_lowest.rs b/src/methods/highest_lowest.rs new file mode 100644 index 0000000..38e038c --- /dev/null +++ b/src/methods/highest_lowest.rs @@ -0,0 +1,393 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Calculates absolute difference between highest and lowest values over the last `length` values for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// Output value is always >= 0.0 +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::HighestLowestDelta; +/// +/// +/// let values = [1.0, 2.0, 3.0, 2.0, 1.0, 0.5, 2.0, 3.0]; +/// let r = [0.0, 1.0, 2.0, 1.0, 2.0, 1.5, 1.5, 2.5]; +/// let mut hld = HighestLowestDelta::new(3, values[0]); +/// +/// (0..values.len()).for_each(|i| { +/// let v = hld.next(values[i]); +/// assert_eq!(v, r[i]); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(`length`) +/// +/// This method is relatively very slow compare to the other methods. +/// +/// # See also +/// +/// [Highest], [Lowest] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct HighestLowestDelta { + highest: Highest, + lowest: Lowest, +} + +impl Method for HighestLowestDelta { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self + where + Self: Sized, + { + debug_assert!(length > 0, "HighestLowestDelta: length should be > 0"); + + Self { + highest: Highest::new(length, value), + lowest: Lowest::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: ValueType) -> ValueType { + self.highest.next(value) - self.lowest.next(value) + } +} + +/// Returns highest value over the last `length` values for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::Highest; +/// +/// let values = [1.0, 2.0, 3.0, 2.0, 1.0, 0.5, 2.0, 3.0]; +/// let r = [1.0, 2.0, 3.0, 3.0, 3.0, 2.0, 2.0, 3.0]; +/// +/// let mut highest = Highest::new(3, values[0]); +/// +/// (0..values.len()).for_each(|i| { +/// let v = highest.next(values[i]); +/// assert_eq!(v, r[i]); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(`length`) +/// +/// This method is relatively slow compare to the other methods. +/// +/// # See also +/// +/// [HighestLowestDelta], [Lowest] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Highest { + value: ValueType, + window: Window, +} + +impl Method for Highest { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "Highest: length should be > 0"); + + Self { + window: Window::new(length, value), + value, + } + } + + #[inline] + fn next(&mut self, value: ValueType) -> ValueType { + self.window.push(value); + + if value > self.value { + self.value = value; + } else { + self.value = self.window.iter().fold(value, |a, b| a.max(b)); + } + + self.value + } +} + +/// Returns lowest value over the last `length` values for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::Lowest; +/// +/// let values = [1.0, 2.0, 3.0, 2.0, 1.0, 0.5, 2.0, 3.0]; +/// let r = [1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 0.5, 0.5]; +/// +/// let mut lowest = Lowest::new(3, values[0]); +/// +/// (0..values.len()).for_each(|i| { +/// let v = lowest.next(values[i]); +/// assert_eq!(v, r[i]); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(`length`) +/// +/// This method is relatively slow compare to the other methods. +/// +/// # See also +/// +/// [HighestLowestDelta], [Highest] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Lowest { + value: ValueType, + window: Window, +} + +impl Method for Lowest { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "Lowest: length should be > 0"); + + Self { + window: Window::new(length, value), + value, + } + } + + #[inline] + fn next(&mut self, value: ValueType) -> ValueType { + self.window.push(value); + + if value < self.value { + self.value = value; + } else { + self.value = self.window.iter().fold(value, |a, b| a.min(b)); + } + + self.value + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use crate::core::{PeriodType, ValueType}; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_highest_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = Highest::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_highest1() { + use super::{Highest as TestingMethod, Method}; + + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert_eq!(x.close, ma.next(x.close)); + }); + } + + #[test] + fn test_highest() { + use super::{Highest as TestingMethod, Method}; + + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (2..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + let length = length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next(x); + let value2 = (0..length).fold(src[i], |m, j| m.max(src[i.saturating_sub(j)])); + assert_eq!(value2, value1); + }); + }); + } + + #[test] + fn test_lowest_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = Lowest::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_lowest1() { + use super::{Lowest as TestingMethod, Method}; + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert_eq!(x.close, ma.next(x.close)); + }); + } + + #[test] + fn test_lowest() { + use super::{Lowest as TestingMethod, Method}; + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (2..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + let length = length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next(x); + let value2 = (0..length).fold(src[i], |m, j| m.min(src[i.saturating_sub(j)])); + assert_eq!(value2, value1); + }); + }); + } + + #[test] + fn test_highest_lowest_delta_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = HighestLowestDelta::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_highes_lowest_delta1() { + use super::{HighestLowestDelta as TestingMethod, Method}; + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert_eq!(0.0, ma.next(x.close)); + }); + } + + #[test] + fn test_highes_lowest_delta() { + use super::{HighestLowestDelta as TestingMethod, Method}; + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (2..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + let length = length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next(x); + let min = (0..length).fold(src[i], |m, j| m.min(src[i.saturating_sub(j)])); + let max = (0..length).fold(src[i], |m, j| m.max(src[i.saturating_sub(j)])); + assert_eq!(max - min, value1); + }); + }); + } +} diff --git a/src/methods/hma.rs b/src/methods/hma.rs new file mode 100644 index 0000000..c0e996a --- /dev/null +++ b/src/methods/hma.rs @@ -0,0 +1,133 @@ +use super::WMA; +use crate::core::{Method, PeriodType, ValueType}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Hull Moving Average](https://www.tradingview.com/scripts/hullma/) for last `length` values for timeseries of type [`ValueType`] +/// +/// HMA = [`WMA`] from (2*[`WMA`] over `length`/2 − [`WMA`] over `length`) over sqrt(`length`)) +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 1 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::HMA; +/// use yata::helpers::RandomCandles; +/// +/// let mut candles = RandomCandles::default(); +/// +/// let mut hma = HMA::new(5, candles.first().close); +/// +/// candles.take(5).enumerate().for_each(|(index, candle)| { +/// println!("HMA at #{} is {}", index, hma.next(candle.close)); +/// }); +/// +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [Weighted Moving Average][`WMA`] +/// +/// [`WMA`]: crate::methods::WMA +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct HMA { + wma1: WMA, + wma2: WMA, + wma3: WMA, +} + +// TODO: +// Rewrite algorithm using signle Window instead of 3 Windows inside WMAs +impl Method for HMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 1, "HMA: length should be > 1"); + + Self { + wma1: WMA::new(length / 2, value), + wma2: WMA::new(length, value), + wma3: WMA::new((length as ValueType).sqrt() as PeriodType, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let w1 = self.wma1.next(value); + let w2 = self.wma2.next(value); + + self.wma3.next(w1.mul_add(2., -w2)) + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{HMA as TestingMethod, WMA}; + use crate::core::Method; + use crate::core::{PeriodType, ValueType}; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_hma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 2..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_hma() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (2..20).for_each(|length| { + let mut wma1 = WMA::new(length, src[0]); + let mut wma2 = WMA::new(length / 2, src[0]); + let mut wma3 = WMA::new((length as ValueType).sqrt() as PeriodType, src[0]); + + let mut ma = TestingMethod::new(length, src[0]); + + src.iter().for_each(|&x| { + let value1 = ma.next(x); + let value2 = wma3.next(2. * wma2.next(x) - wma1.next(x)); + assert!((value2 - value1).abs() < SIGMA); + }); + }); + } +} diff --git a/src/methods/integral.rs b/src/methods/integral.rs new file mode 100644 index 0000000..94c29df --- /dev/null +++ b/src/methods/integral.rs @@ -0,0 +1,216 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Integrates (summarizes) [`ValueType`] values for the given window size `length` +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// If `length` == 0, then integrates since the beginning of timeseries +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::Integral; +/// +/// // Integrates over last 3 values +/// let mut integral = Integral::new(3, 1.0); +/// +/// integral.next(1.0); +/// integral.next(2.0); +/// assert_eq!(integral.next(3.0), 6.0); // 1 + 2 + 3 +/// assert_eq!(integral.next(4.0), 9.0); // 2 + 3 + 4 +/// assert_eq!(integral.next(5.0), 12.0); // 3 + 4 + 5 +/// ``` +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::Integral; +/// +/// // Integrates since the beginning +/// let mut integral = Integral::new(0, 1.0); // same as Integral::default() +/// +/// integral.next(1.0); +/// integral.next(2.0); +/// assert_eq!(integral.next(3.0), 6.0); // 1 + 2 + 3 +/// assert_eq!(integral.next(4.0), 10.0); // 1 + 2 + 3 + 4 +/// assert_eq!(integral.next(5.0), 15.0); // 1 + 2 + 3 + 4 + 5 +/// ``` +/// +/// ### Intergal is opposite method for Derivative +/// ``` +/// use yata::prelude::*; +/// use yata::methods::{Integral, Derivative}; +/// +/// let s = [1.0, 2.0, 3.0, 3.0, 2.5, 3.5, 5.0]; +/// let mut integral = Integral::default(); +/// let mut derivative = Derivative::new(1, s[0]); +/// +/// (&s).iter().for_each(|&v| { +/// let integration_constant = s[0]; +/// let der = derivative.next(v); +/// let integr = integral.next(der) + integration_constant; +/// +/// assert_eq!(integr, v); +/// }); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [Derivative](crate::methods::Derivative) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Integral { + value: ValueType, + window: Window, +} + +/// Just an alias for Integral +pub type Sum = Integral; + +impl Method for Integral { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + Self { + window: Window::new(length, value), + value: value * length as ValueType, + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.value += value; + + if !self.window.is_empty() { + self.value -= self.window.push(value); + } + + self.value + } +} + +impl Default for Integral { + fn default() -> Self { + Self::new(0, 0.0) + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Integral as TestingMethod, Method}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_integral_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + #[should_panic] + fn test_integral0_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + let input = (5.0 + 56.0) / 16.3251; + let mut method = TestingMethod::new(0, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + + #[test] + fn test_integral0() { + let src: Vec = RandomCandles::default() + .take(100) + .map(|x| x.close) + .collect(); + + let mut ma = TestingMethod::new(0, src[0]); + let mut q = Vec::new(); + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next(x); + let value2 = src.iter().take(i + 1).fold(0.0, |s, &c| s + c); + q.push(x); + + assert_eq!(value1, value2, "at index {} with value {}: {:?}", i, x, q); + }); + } + + #[test] + fn test_integral1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_integral() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + let length = length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let value1 = ma.next(x); + + let value2 = (0..length).fold(0.0, |s, j| s + src[i.saturating_sub(j)]); + + assert!( + (value2 - value1).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value1, + i, + length + ); + }); + }); + } +} diff --git a/src/methods/lin_reg.rs b/src/methods/lin_reg.rs new file mode 100644 index 0000000..4e6701c --- /dev/null +++ b/src/methods/lin_reg.rs @@ -0,0 +1,163 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Linear regression](https://en.wikipedia.org/wiki/Linear_regression) moving average for last `length` values of timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 1 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Perfomance +/// +/// O(1) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LinReg { + length: PeriodType, + s_xy: ValueType, + s_y: ValueType, + s_x: ValueType, + float_length: ValueType, + length_invert: ValueType, + divider: ValueType, + window: Window, +} + +impl Method for LinReg { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 1, "LinReg: length should be > 1"); + + let l64 = length as usize; + let float_length = length as ValueType; + let length_invert = -float_length.recip(); + + let n_1 = l64 - 1; + let s_x = l64 * n_1 / 2; + let s_x2 = s_x * (2 * n_1 + 1) / 3; + + let divider = ((l64 * s_x2 - s_x * s_x) as ValueType).recip(); + + let s_x = -(s_x as ValueType); + Self { + length, + float_length, + length_invert, + divider, + s_x: s_x, + s_y: -value * float_length, + s_xy: value * s_x, + window: Window::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let past_value = self.window.push(value); + + self.s_xy += past_value.mul_add(self.float_length, self.s_y); + self.s_y += past_value - value; + + // y = kx + b, x=0 + let k = self.s_xy.mul_add(self.float_length, self.s_x * self.s_y) * self.divider; + let b = self.s_x.mul_add(k, self.s_y) * self.length_invert; + + b + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{LinReg as TestingMethod, Method}; + use crate::core::{Candle, ValueType}; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-5; + + // #[test] + // fn test_lin_reg1() { + // let candles = RandomCandles::default(); + + // let mut ma = TestingMethod::new(1); + + // candles.take(100).map(|x| x.close).for_each(|x| { + // let v = ma.next(x); + // assert!((x - v).abs() < SIGMA, "{}, {}", x, v); + // }); + // } + + #[test] + fn test_lin_reg_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 2..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_lin_reg() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (2..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + let length = length as usize; + + let n = length as ValueType; + let s_x: usize = (0..length).sum(); + let s_x2: usize = (0..length).map(|x| x * x).sum(); + + let s_x = s_x as ValueType; + let s_x2 = s_x2 as ValueType; + + src.iter().enumerate().for_each(|(i, &x)| { + let ma_value = ma.next(x); + + let s_xy = + (0..length).fold(0.0, |s, j| s + j as ValueType * src[i.saturating_sub(j)]); + let s_y: ValueType = (0..length).map(|j| src[i.saturating_sub(j)]).sum(); + + let a = (n * s_xy - s_x * s_y) / (n * s_x2 - s_x * s_x); + let b = (s_y - a * s_x) / n; + + assert!( + (ma_value - b).abs() < SIGMA, + "{}, {} at index {} with length {}", + ma_value, + b, + i, + length + ); + }); + }); + } +} diff --git a/src/methods/mod.rs b/src/methods/mod.rs new file mode 100644 index 0000000..413780e --- /dev/null +++ b/src/methods/mod.rs @@ -0,0 +1,104 @@ +#![warn(missing_docs, missing_debug_implementations)] + +//! Commonly used methods for manipulating timeseries. +//! Every method implements [`Method`](crate::core::Method) trait. +//! +//! To create a method instance use [`Method::new`](crate::core::Method::new). +//! To get new output value over the input value use [`Method::next`](crate::core::Method::next). +//! +//! ``` +//! // creating Weighted Moving Average of length 5 +//! use yata::prelude::*; +//! use yata::methods::WMA; +//! +//! let mut wma = WMA::new(5, 20.0); +//! +//! let input_value = 34.51; +//! let output_value = wma.next(input_value); +//! ``` +//! +//! # Examples +//! +//! ``` +//! use yata::prelude::*; +//! use yata::methods::SMA; +//! +//! let mut sma = SMA::new(3, 5.0); +//! sma.next(5.0); +//! sma.next(4.0); +//! assert_eq!(sma.next(6.0), 5.0); +//! assert_eq!(sma.next(2.0), 4.0); +//! assert_eq!(sma.next(-2.0), 2.0); +//! ``` + +mod sma; +pub use sma::*; +mod wma; +pub use wma::*; +mod ema; +pub use ema::*; +mod rma; +pub use rma::*; +mod smm; +pub use smm::*; +mod hma; +pub use hma::*; +mod lin_reg; +pub use lin_reg::*; +mod swma; +pub use swma::*; +mod conv; +pub use conv::*; +mod vwma; +pub use vwma::*; +// +mod derivative; +pub use derivative::*; +mod integral; +pub use integral::*; +mod momentum; +pub use momentum::*; +mod rate_of_change; +pub use rate_of_change::*; +mod st_dev; +pub use st_dev::*; +mod volatility; +pub use volatility::*; + +mod cross; +pub use cross::*; +mod pivot; +pub use pivot::*; +mod highest_lowest; +pub use highest_lowest::*; +mod adi; +pub use adi::*; +mod past; +pub use past::*; + +#[cfg(test)] +mod tests { + use crate::core::{Method, ValueType}; + use std::fmt::Debug; + + pub(super) fn test_const( + method: &mut dyn Method, + input: I, + output: O, + ) { + for _ in 0..100 { + assert_eq!(method.next(input), output); + } + } + + const SIGMA: ValueType = 1e-7; + pub(super) fn test_const_float( + method: &mut dyn Method, + input: I, + output: ValueType, + ) { + for _ in 0..100 { + assert!((method.next(input) - output).abs() < SIGMA); + } + } +} diff --git a/src/methods/momentum.rs b/src/methods/momentum.rs new file mode 100644 index 0000000..d3eed4d --- /dev/null +++ b/src/methods/momentum.rs @@ -0,0 +1,152 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Momentum](https://en.wikipedia.org/wiki/Momentum_(technical_analysis)) calculates difference between current +/// value and n-th value back, where n = `length` +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::Momentum; +/// +/// let mut change = Momentum::new(3, 1.0); // a.k.a. Change => let mut change = Change::new(3); +/// change.next(1.0); +/// change.next(2.0); +/// assert_eq!(change.next(3.0), 2.0); +/// assert_eq!(change.next(4.0), 3.0); +/// assert_eq!(change.next(2.0), 0.0); +/// ``` +/// +/// ### At `length`=1 Momentum is the same as Derivative with `length`=1 +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::Momentum; +/// use yata::methods::Derivative; +/// +/// let mut change = Momentum::new(1, 1.0); +/// let mut derivative = Derivative::new(1, 1.0); +/// change.next(1.0); derivative.next(1.0); +/// change.next(2.0); derivative.next(2.0); +/// assert_eq!(change.next(3.0), derivative.next(3.0)); +/// assert_eq!(change.next(4.0), derivative.next(4.0)); +/// assert_eq!(change.next(2.0), derivative.next(2.0)); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See Also +/// +/// [`Rate of Change`](crate::methods::RateOfChange), [`Derivative`](crate::methods::Derivative) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Momentum { + window: Window, + last_value: ValueType, +} + +/// Just an alias for [Momentum] method +pub type Change = Momentum; + +/// Just an alias for [Momentum] method +pub type MTM = Momentum; + +impl Method for Momentum { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "Momentum: length should be > 0"); + + Self { + window: Window::new(length, value), + last_value: value, + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + value - self.window.push(value) + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, Momentum as TestingMethod}; + use crate::core::{Candle, ValueType}; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_momentum_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_momentum1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + let mut prev = None; + candles.take(100).map(|x| x.close).for_each(|x| { + let q = ma.next(x); + let p = prev.unwrap_or(x); + assert_eq!(q, x - p); + prev = Some(x); + }); + } + + #[test] + fn test_momentum() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + src.iter().enumerate().for_each(|(i, &x)| { + assert_eq!(x - src[i.saturating_sub(length as usize)], ma.next(x)) + }); + }); + } +} diff --git a/src/methods/past.rs b/src/methods/past.rs new file mode 100644 index 0000000..ca7f1b1 --- /dev/null +++ b/src/methods/past.rs @@ -0,0 +1,137 @@ +use crate::core::Method; +use crate::core::{PeriodType, Window}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Moves timeseries by `length` items forward +/// +/// It's just a simple method-like wrapper for [`Window`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::Past; +/// +/// // Move of length=3 +/// let mut past = Past::new(3, 1.0); +/// +/// past.next(1.0); +/// past.next(2.0); +/// past.next(3.0); +/// +/// assert_eq!(past.next(4.0), 1.0); +/// assert_eq!(past.next(5.0), 2.0); +/// assert_eq!(past.next(6.0), 3.0); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [`Window`] +/// +/// [`Window`]: crate::core::Window +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Past(Window) +where + T: Sized + Copy + Default + fmt::Debug; + +impl Method for Past +where + T: Sized + Copy + Default + fmt::Debug, +{ + type Params = PeriodType; + type Input = T; + type Output = T; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "Past: length should be > 0"); + + Self(Window::new(length, value)) + } + + #[inline] + fn next(&mut self, value: T) -> T { + self.0.push(value) + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, Past as TestingMethod}; + use crate::core::{Candle, ValueType}; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_past_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_past1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first()); + + let mut prev = None; + candles.take(100).for_each(|x| { + let q = ma.next(x); + let p = prev.unwrap_or(x); + assert_eq!(p.close, q.close); + assert_eq!(p.volume, q.volume); + assert_eq!(p.high, q.high); + prev = Some(x); + }); + } + + #[test] + fn test_past() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + src.iter() + .enumerate() + .for_each(|(i, &x)| assert_eq!(src[i.saturating_sub(length as usize)], ma.next(x))); + }); + } +} diff --git a/src/methods/pivot.rs b/src/methods/pivot.rs new file mode 100644 index 0000000..9003b56 --- /dev/null +++ b/src/methods/pivot.rs @@ -0,0 +1,428 @@ +use crate::core::Method; +use crate::core::{Action, PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Searches for [Pivot Points](https://en.wikipedia.org/wiki/Pivot_point_(technical_analysis)) over last `left`+`right`+1 values of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a tuple of 2 parameters (`left`: [`PeriodType`], `right`: [`PeriodType`]) +/// +/// `left` should be > 0 and `right` should be > 0 +/// +/// There is an additional restriction on parameters: `left`+`right`+1 should be <= [`PeriodType`]::MAX. +/// So if your [`PeriodType`] is default `u8`, then `left`+`right`+1 should be <= 255 +/// +/// [Read more about `PeriodType`][`PeriodType`] +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`Action`] +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::PivotSignal; +/// +/// let s = [1.0, 2.0, 3.0, 2.0, 1.0, 1.0, 2.0]; +/// let r = [ 0, 0, 1, 0, -1, 0, 1 ]; +/// +/// let mut pivot = PivotSignal::new(2, 2, s[0]); +/// let r2: Vec = s.iter().map(|&v| pivot.next(v).analog()).collect(); +/// +/// assert_eq!(r2, r2); +/// ``` +/// +/// # Perfomance +/// +/// O(`left`+`right`) +/// +/// # See also +/// +/// [PivotHighSignal], [PivotLowSignal] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// [`Action`]: crate::core::Action +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PivotSignal { + high: PivotHighSignal, + low: PivotLowSignal, +} + +impl PivotSignal { + /// Constructs new instanceof PivotSignal + /// It's just an alias for `Method::new((left, right), value)` but without parentheses of `Input` touple + pub fn new(left: PeriodType, right: PeriodType, value: ValueType) -> Self { + debug_assert!( + left > 0 && right > 0, + "PivotSignal: left and right should be >= 1" + ); + + Self { + high: Method::new((left, right), value), + low: Method::new((left, right), value), + } + } +} + +impl Method for PivotSignal { + type Params = (PeriodType, PeriodType); + type Input = ValueType; + type Output = Action; + + fn new(params: Self::Params, value: Self::Input) -> Self + where + Self: Sized, + { + let (left, right) = params; + + debug_assert!( + left >= 1 && right >= 1, + "PivotSignal: left and right should be >= 1" + ); + + Self { + high: Method::new((left, right), value), + low: Method::new((left, right), value), + } + } + + #[inline] + fn next(&mut self, value: ValueType) -> Self::Output { + self.low.next(value) - self.high.next(value) + } +} + +/// Searches for high [Pivot Points](https://en.wikipedia.org/wiki/Pivot_point_(technical_analysis)) over last `left`+`right`+1 values of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a tuple of 2 parameters (`left`: [`PeriodType`], `right`: [`PeriodType`]) +/// +/// `left` should be > 0 and `right` should be > 0 +/// +/// There is an additional restriction on parameters: `left`+`right`+1 should be <= [`PeriodType`]::MAX. +/// So if your [`PeriodType`] is default `u8`, then `left`+`right`+1 should be <= 255 +/// +/// [Read more about `PeriodType`][`PeriodType`] +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`Action`] +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::PivotHighSignal; +/// +/// let s = [1.0, 2.0, 3.0, 2.0, 1.0, 1.0, 2.0]; +/// let r = [ 0, 0, 0, 0, 1, 0, 0 ]; +/// +/// let mut pivot = PivotHighSignal::new(2, 2, s[0]); +/// let r2: Vec = s.iter().map(|&v| pivot.next(v).analog()).collect(); +/// +/// assert_eq!(r2, r2); +/// ``` +/// +/// # Perfomance +/// +/// O(`left`+`right`) +/// +/// # See also +/// +/// [PivotSignal], [PivotLowSignal] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// [`Action`]: crate::core::Action +/// +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PivotHighSignal { + left: PeriodType, + right: PeriodType, + + max_value: ValueType, + max_index: PeriodType, + index: PeriodType, + window: Window, +} + +impl PivotHighSignal { + /// Constructs new instanceof PivotHighSignal + /// It's just an alias for `Method::new((left, right), value)` but without parentheses of `Input` touple + pub fn new(left: PeriodType, right: PeriodType, value: ValueType) -> Self { + Method::new((left, right), value) + } +} + +impl Method for PivotHighSignal { + type Params = (PeriodType, PeriodType); + type Input = ValueType; + type Output = Action; + + fn new(params: Self::Params, value: Self::Input) -> Self { + debug_assert!( + params.0 >= 1 && params.1 >= 1, + "PivotHighSignal: left and right should be >= 1" + ); + + let (left, right) = params; + + Self { + left, + right, + max_value: value, + max_index: 0, + index: 0, + window: Window::new(left + right + 1, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.window.push(value); + + let first_index = (self.index + 1).saturating_sub(self.window.len()); + + if self.max_index < first_index { + let mut max_index = first_index; + let mut max_value = self.window.first(); + + self.window + .iter() + .zip(first_index..) + .skip(1) + .for_each(|(x, i)| { + if x >= max_value { + max_value = x; + max_index = i; + } + }); + self.max_value = max_value; + self.max_index = max_index; + } else { + if value >= self.max_value { + self.max_value = value; + self.max_index = self.index; + } + } + + let s; + if self.index >= self.right && self.max_index == self.index.saturating_sub(self.right) { + s = Action::BUY_ALL; + } else { + s = Action::None; + } + + self.index += 1; + s + } +} + +/// Searches for low [Pivot Points](https://en.wikipedia.org/wiki/Pivot_point_(technical_analysis)) over last `left`+`right`+1 values of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a tuple of 2 parameters (`left`: [`PeriodType`], `right`: [`PeriodType`]) +/// +/// `left` should be > 0 and `right` should be > 0 +/// +/// There is an additional restriction on parameters: `left`+`right`+1 should be <= [`PeriodType`]::MAX. +/// So if your [`PeriodType`] is default `u8`, then `left`+`right`+1 should be <= 255 +/// +/// [Read more about `PeriodType`][`PeriodType`] +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`Action`] +/// +/// ``` +/// use yata::core::Method; +/// use yata::methods::PivotHighSignal; +/// +/// let s = [1.0, 2.0, 3.0, 2.0, 1.0, 1.0, 2.0]; +/// let r = [ 0, 0, 1, 0, 0, 0, 1 ]; +/// +/// let mut pivot = PivotHighSignal::new(2, 2, s[0]); +/// let r2: Vec = s.iter().map(|&v| pivot.next(v).analog()).collect(); +/// +/// assert_eq!(r2, r2); +/// ``` +/// +/// # Perfomance +/// +/// O(`left`+`right`) +/// +/// # See also +/// +/// [PivotSignal], [PivotHighSignal] +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// [`Action`]: crate::core::Action +/// +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PivotLowSignal { + left: PeriodType, + right: PeriodType, + + // value: ValueType, + // before: usize, + // after: usize, + min_value: ValueType, + min_index: PeriodType, + index: PeriodType, + window: Window, +} + +impl PivotLowSignal { + /// Constructs new instanceof PivotLowSignal + /// It's just an alias for `Method::new((left, right), value)` but without parentheses of `Input` touple + pub fn new(left: PeriodType, right: PeriodType, value: ValueType) -> Self { + Method::new((left, right), value) + } +} + +impl Method for PivotLowSignal { + type Params = (PeriodType, PeriodType); + type Input = ValueType; + type Output = Action; + + fn new(params: Self::Params, value: Self::Input) -> Self { + debug_assert!( + params.0 >= 1 && params.1 >= 1, + "PivotLowSignal: left and right should be >= 1" + ); + + let (left, right) = params; + + Self { + left, + right, + min_value: value, + min_index: 0, + index: 0, + window: Window::new(left + right + 1, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.window.push(value); + + let first_index = (self.index + 1).saturating_sub(self.window.len()); + + if self.min_index < first_index { + let mut min_index = first_index; + let mut min_value = self.window.first(); + + self.window + .iter() + .zip(first_index..) + .skip(1) + .for_each(|(x, i)| { + if x <= min_value { + min_value = x; + min_index = i; + } + }); + self.min_value = min_value; + self.min_index = min_index; + } else { + if value <= self.min_value { + self.min_value = value; + self.min_index = self.index; + } + } + + let s; + if self.index >= self.right && self.min_index == self.index.saturating_sub(self.right) { + s = Action::BUY_ALL; + } else { + s = Action::None; + } + + self.index += 1; + s + } +} + +#[cfg(test)] +mod tests { + #[allow(unused_imports)] + use super::*; + + #[test] + fn test_pivot_low_const() { + use super::*; + use crate::core::Method; + use crate::methods::tests::test_const; + + for i in 1..10 { + for j in 1..10 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = PivotLowSignal::new(i, j, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + } + + #[test] + #[rustfmt::skip] + fn test_pivot_low() { + let v: Vec = vec![2.0, 1.0, 2.0, 2.0, 3.0, 2.0, 1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 1.0, 2.0, 1.0, 2.0, 3.0]; + let r: Vec = vec![ 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1 ]; + + let mut pivot = PivotLowSignal::new(2, 2, v[0]); + + let r2: Vec = v.iter().map(|&x| pivot.next(x).analog()).collect(); + assert_eq!(r, r2); + } + + #[test] + fn test_pivot_high_const() { + use super::*; + use crate::core::Method; + use crate::methods::tests::test_const; + + for i in 1..10 { + for j in 1..10 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = PivotHighSignal::new(i, j, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + } + + #[test] + #[rustfmt::skip] + fn test_pivot_high() { + let v: Vec = vec![2.0, 1.0, 2.0, 2.0, 3.0, 2.0, 1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 1.0, 2.0, 1.0, 2.0, 3.0]; + let r: Vec = vec![ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ]; + + let mut pivot = PivotHighSignal::new(2, 2, v[0]); + + let r2: Vec = v.iter().map(|&x| pivot.next(x).analog()).collect(); + assert_eq!(r, r2); + } +} diff --git a/src/methods/rate_of_change.rs b/src/methods/rate_of_change.rs new file mode 100644 index 0000000..c295877 --- /dev/null +++ b/src/methods/rate_of_change.rs @@ -0,0 +1,133 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Just an alias for [RateOfChange](RateOfChange) method +pub type ROC = RateOfChange; + +/// [Rate of change](https://en.wikipedia.org/wiki/Momentum_(technical_analysis)) calculates relative difference between current +/// value and n-th value back, where n = `length` +/// +/// ROC = (`value` - `n_th_value`) / `n_th_value` +/// +/// ROC = [`Momentum`] / `n_th_value` +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// Input value should always be greater than 0.0. (`value` > 0.0) +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See Also +/// +/// [`Momentum`], [`Derivative`](crate::methods::Derivative) +/// +/// [`Momentum`]: crate::methods::Momentum +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RateOfChange(Window); + +impl Method for RateOfChange { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "RateOfChange: length should be > 0"); + + Self(Window::new(length, value)) + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let prev_value = self.0.push(value); + + (value - prev_value) / prev_value + } +} + +mod tests { + #![allow(unused_imports)] + use super::{Method, ROC as TestingMethod}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + use crate::methods::{Derivative, Past}; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_rate_of_change_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_rate_of_change1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + let mut der = Derivative::new(1, candles.first().close); + let mut mv = Past::new(1, candles.first().close); + + candles.take(100).map(|x| x.close).for_each(|x| { + let value = ma.next(x); + let value2 = der.next(x) / mv.next(x); + assert!((value - value2).abs() < SIGMA); + }); + } + + #[test] + fn test_rate_of_change() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + let left_value = src[i.saturating_sub(length as usize)]; + + let value2 = (x - left_value) / left_value; + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value, + value2, + i, + length + ); + }); + }); + } +} diff --git a/src/methods/rma.rs b/src/methods/rma.rs new file mode 100644 index 0000000..e132232 --- /dev/null +++ b/src/methods/rma.rs @@ -0,0 +1,151 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Running Moving Average](https://en.wikipedia.org/wiki/Moving_average#Modified_moving_average) of specified `length` for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::RMA; +/// +/// // RMA of length=3 +/// let mut rma = RMA::new(3, 1.0); +/// +/// rma.next(1.0); +/// rma.next(2.0); +/// +/// assert!((rma.next(3.0)-1.8888888).abs() < 1e-5); +/// assert!((rma.next(4.0)-2.5925925925).abs() < 1e-5); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [`EMA`](crate::methods::EMA) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RMA { + alpha: ValueType, + alpha_rev: ValueType, + prev_value: ValueType, +} + +/// Just an alias for RMA +pub type MMA = RMA; + +/// Just an alias for RMA +pub type SMMA = RMA; + +impl Method for RMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "RMA: length should be > 0"); + + let alpha = (length as ValueType).recip(); + Self { + alpha, + alpha_rev: 1. - alpha, + prev_value: value, + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let value = self.alpha.mul_add(value, self.alpha_rev * self.prev_value); + self.prev_value = value; + + value + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, RMA as TestingMethod}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_rma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const_float(&mut method, input, output); + } + } + + #[test] + fn test_rma1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_rma() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|length| { + let mut ma = TestingMethod::new(length, src[0]); + + let mut value2 = src[0]; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + + value2 = (x + (length - 1) as ValueType * value2) / (length as ValueType); + + assert!( + (value2 - value).abs() < SIGMA, + "{}, {} at index {} with length {}", + value2, + value, + i, + length + ); + }); + }); + } +} diff --git a/src/methods/sma.rs b/src/methods/sma.rs new file mode 100644 index 0000000..1e29555 --- /dev/null +++ b/src/methods/sma.rs @@ -0,0 +1,137 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Simple Moving Average](https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average) of specified `length` for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::SMA; +/// +/// // SMA of length=3 +/// let mut sma = SMA::new(3, 1.0); +/// +/// sma.next(1.0); +/// sma.next(2.0); +/// +/// assert_eq!(sma.next(3.0), 2.0); +/// assert_eq!(sma.next(4.0), 3.0); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SMA { + divider: ValueType, + value: ValueType, + window: Window, +} + +impl Method for SMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "SMA: length should be > 0"); + + Self { + divider: (length as ValueType).recip(), + value, + window: Window::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let prev_value = self.window.push(value); + self.value += (value - prev_value) * self.divider; + + self.value + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, SMA as TestingMethod}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_sma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_sma1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_sma() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|sma_length| { + let mut sma = TestingMethod::new(sma_length, src[0]); + + src.iter().enumerate().for_each(|(i, &x)| { + let value = sma.next(x); + let slice_from = i.saturating_sub((sma_length - 1) as usize); + let slice_to = i; + let slice = &src[slice_from..=slice_to]; + let mut sum: ValueType = slice.iter().sum(); + if slice.len() < sma_length as usize { + sum += (sma_length as usize - slice.len()) as ValueType * src.first().unwrap(); + } + + assert!((sum / sma_length as ValueType - value).abs() < SIGMA); + }); + }); + } +} diff --git a/src/methods/smm.rs b/src/methods/smm.rs new file mode 100644 index 0000000..503947a --- /dev/null +++ b/src/methods/smm.rs @@ -0,0 +1,167 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// +/// [Simle Moving Median](https://en.wikipedia.org/wiki/Moving_average#Moving_median) of specified `length` for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::SMM; +/// +/// // SMM of length=3 +/// let mut smm = SMM::new(3, 1.0); +/// +/// smm.next(1.0); +/// smm.next(2.0); +/// +/// assert_eq!(smm.next(3.0), 2.0); +/// assert_eq!(smm.next(100.0), 3.0); +/// ``` +/// +/// # Perfomance +/// +/// O(`length` x log(`length`)) +/// +/// This method is relatively very slow compare to the other methods. +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SMM { + is_even: bool, + half: PeriodType, + half_m1: PeriodType, + window: Window, + slice: Vec, +} + +impl Method for SMM { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "SMM: length should be > 0"); + + let half = length / 2; + + Self { + is_even: length % 2 == 0, + half, + half_m1: half.saturating_sub(1), + window: Window::new(length, value), + slice: vec![0.0; length as usize], + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.window.push(value); + + self.slice.copy_from_slice(self.window.as_slice()); + + self.slice + .sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); + + if self.is_even { + (self.slice[self.half as usize] + self.slice[self.half_m1 as usize]) * 0.5 + } else { + self.slice[self.half as usize] + } + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, SMM as TestingMethod}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_smm_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_smm1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_smm() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|ma_length| { + let mut ma = TestingMethod::new(ma_length, src[0]); + let ma_length = ma_length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + let slice_from = i.saturating_sub(ma_length - 1); + let slice_to = i; + let mut slice = Vec::with_capacity(ma_length); + + src.iter() + .skip(slice_from) + .take(slice_to - slice_from + 1) + .for_each(|&x| slice.push(x)); + while slice.len() < ma_length { + slice.push(src[0]); + } + + slice.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + let value2; + + if ma_length % 2 == 0 { + value2 = (slice[ma_length / 2] + slice[ma_length / 2 - 1]) / 2.0; + } else { + value2 = slice[ma_length / 2]; + } + assert!((value2 - value).abs() < SIGMA); + }); + }); + } +} diff --git a/src/methods/st_dev.rs b/src/methods/st_dev.rs new file mode 100644 index 0000000..3cd1f77 --- /dev/null +++ b/src/methods/st_dev.rs @@ -0,0 +1,162 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; +use crate::methods::SMA; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Moving [Standart Deviation](https://en.wikipedia.org/wiki/Standard_deviation) over the window of size `length` for timeseries of type [`ValueType`] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::StDev; +/// +/// // StDev over the window with length=3 +/// let mut stdev = StDev::new(3, 1.0); +/// +/// stdev.next(1.0); +/// stdev.next(2.0); +/// +/// assert_eq!(stdev.next(3.0), 1.0); +/// assert_eq!(stdev.next(4.0), 1.0); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct StDev { + float_length: ValueType, + val_sum: ValueType, + sq_val_sum: ValueType, + k: ValueType, + window: Window, + ma: SMA, +} + +impl Method for StDev { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 1, "StDev: length should be > 1"); + + let k = ((length - 1) as ValueType).recip(); + + let float_length = length as ValueType; + let val_sum = value * float_length; + Self { + float_length, + val_sum, + sq_val_sum: value * val_sum, + k, + window: Window::new(length, value), + ma: SMA::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let prev_value = self.window.push(value); + self.sq_val_sum += value * value - prev_value * prev_value; + self.val_sum += value - prev_value; + let ma_value = self.ma.next(value); + + // let sum = self.sq_val_sum + ma_value * (ma_value * self.float_length - 2. * self.val_sum); + let sum = ma_value + .mul_add(self.float_length, -2. * self.val_sum) + .mul_add(ma_value, self.sq_val_sum); + (sum.abs() * self.k).sqrt() + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, StDev as TestingMethod}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-7; + + #[test] + fn test_st_dev_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const_float; + + for i in 2..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + test_const_float(&mut method, input, 0.0); + } + } + + // #[test] + // fn test_st_dev1() { + // let mut candles = RandomCandles::default(); + + // let mut ma = TestingMethod::new(1, candles.first().close); + + // candles.take(100).for_each(|x| { + // assert!((0.0 - ma.next(x.close)).abs() < SIGMA, "{:?}", ma); + // }); + // } + + #[test] + fn test_st_dev() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (2..20).for_each(|ma_length| { + let mut ma = TestingMethod::new(ma_length, src[0]); + let ma_length = ma_length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let mut avg = 0.; + for j in 0..ma_length { + avg += src[i.saturating_sub(j)] / ma_length as ValueType; + } + + let mut diff_sq_sum = 0.; + for j in 0..ma_length { + diff_sq_sum += (src[i.saturating_sub(j)] - avg).powi(2); + } + + let value = ma.next(x); + let value2 = (diff_sq_sum / (ma_length - 1) as ValueType).sqrt(); + assert!( + (value2 - value).abs() < SIGMA, + "{}, {}, index: {}", + value2, + value, + i + ); + }); + }); + } +} diff --git a/src/methods/swma.rs b/src/methods/swma.rs new file mode 100644 index 0000000..56d9ed7 --- /dev/null +++ b/src/methods/swma.rs @@ -0,0 +1,151 @@ +use super::Conv; +use crate::core::Method; +use crate::core::{PeriodType, ValueType}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Symmetrically Weighted Moving Average of specified `length` for timeseries of [`ValueType`]. +/// +/// F.e. if `length` = 4, then weights are: [ 1.0, 2.0, 2.0, 1.0 ]. +/// +/// If `length` = 5, then weights are: [ 1.0, 2.0, 3.0, 2.0, 1.0 ]. +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Perfomance +/// +/// O(`length`) +/// +/// This method is relatively slow compare to the other methods. +/// +/// # See also +/// +/// [`WMA`](crate::methods::WMA) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +/// +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SWMA(Conv); + +impl Method for SWMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "SWMA: length must be > 0"); + + let ln2 = length / 2; + + let k_sum = + (ln2 * (ln2 + 1) + if length % 2 == 1 { (length + 1) / 2 } else { 0 }) as ValueType; + + let mut weights = vec![0.; length as usize]; + (0..(length + 1) / 2).for_each(|i| { + let q = (i + 1) as ValueType / k_sum; + weights[i as usize] = q; + weights[(length - i - 1) as usize] = q; + }); + + Self(Conv::new(weights, value)) + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + self.0.next(value) + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Conv, Method, SWMA as TestingMethod}; + use crate::core::{PeriodType, ValueType}; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_swma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_swma1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_swma() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + let weights: Vec> = vec![ + vec![1.0], + vec![1.0, 1.0], + vec![1.0, 2.0, 1.0], + vec![1.0, 2.0, 2.0, 1.0], + vec![1.0, 2.0, 3.0, 2.0, 1.0], + vec![1.0, 2.0, 3.0, 3.0, 2.0, 1.0], + vec![1.0, 2.0, 3.0, 4.0, 3.0, 2.0, 1.0], + vec![1.0, 2.0, 3.0, 4.0, 4.0, 3.0, 2.0, 1.0], + vec![1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0], + vec![1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0], + ]; + + weights.iter().for_each(|weights| { + let wsum: ValueType = weights.iter().sum(); + let length = weights.len(); + + let mut ma = TestingMethod::new(length as PeriodType, src[0]); + let mut conv = Conv::new(weights.clone(), src[0]); + + src.iter().enumerate().for_each(|(i, &x)| { + let wcv = weights + .iter() + .enumerate() + .fold(0.0, |sum, (j, &w)| sum + w * src[i.saturating_sub(j)]); + + let value = ma.next(x); + let value2 = wcv / wsum; + let value3 = conv.next(x); + + assert!((value2 - value).abs() < SIGMA); + assert!((value3 - value).abs() < SIGMA); + }); + }); + } +} diff --git a/src/methods/volatility.rs b/src/methods/volatility.rs new file mode 100644 index 0000000..7ebe40d --- /dev/null +++ b/src/methods/volatility.rs @@ -0,0 +1,146 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Calculate moving linear volatility for last `length` values of type [`ValueType`] +/// +/// LV = Σ\[abs([`Derivative`]\(1\))\] +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// Output is always positive or 0.0 +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::LinearVolatility; +/// +/// // volatility over 3 periods +/// let mut vol = LinearVolatility::new(3, 1.0); +/// vol.next(1.0); +/// vol.next(2.0); +/// assert_eq!(vol.next(3.0), 2.0); +/// assert_eq!(vol.next(1.0), 4.0); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// [`Derivative`]: crate::methods::Derivative +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LinearVolatility { + window: Window, + prev_value: ValueType, + volatility: ValueType, +} + +impl Method for LinearVolatility { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "LinearVolatility: length should be > 0"); + + Self { + window: Window::new(length, 0.), + prev_value: value, + volatility: 0., + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let derivative = (value - self.prev_value).abs(); + self.prev_value = value; + + let past_derivative = self.window.push(derivative); + + self.volatility += derivative - past_derivative; + + self.volatility + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{LinearVolatility as TestingMethod, Method}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + use crate::methods::Derivative; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_volatility_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + test_const(&mut method, input, 0.0); + } + } + + #[test] + fn test_volatility1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + let mut der = Derivative::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + let v1 = der.next(x.close).abs(); + let v2 = ma.next(x.close); + assert!((v1 - v2).abs() < SIGMA, "{}, {}", v1, v2); + }); + } + + #[test] + fn test_linear_volatility() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|ma_length| { + let mut ma = TestingMethod::new(ma_length, src[0]); + let ma_length = ma_length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let mut s = 0.; + for j in 0..ma_length { + let d = src[i.saturating_sub(j)] - src[i.saturating_sub(j + 1)]; + s += d.abs(); + } + + let value = ma.next(x); + let value2 = s; + assert!((value2 - value).abs() < SIGMA); + }); + }); + } +} diff --git a/src/methods/vwma.rs b/src/methods/vwma.rs new file mode 100644 index 0000000..a55cc6e --- /dev/null +++ b/src/methods/vwma.rs @@ -0,0 +1,144 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Volume Weighed Moving Average](https://en.wikipedia.org/wiki/Moving_average#Weighted_moving_average) of specified `length` +/// for timeseries of type ([`ValueType`], [`ValueType`]) which represents pair of values (`value`, `weight`) +/// +/// # Parameters +/// +/// `length` should be > 0 +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::VWMA; +/// +/// // VWMA of length=3 +/// let mut vwma = VWMA::new(3, (3.0, 1.0)); +/// +/// // input value is a pair of f64 (value, weight) +/// vwma.next((3.0, 1.0)); +/// vwma.next((6.0, 1.0)); +/// +/// assert_eq!(vwma.next((9.0, 2.0)), 6.75); +/// assert!((vwma.next((12.0, 0.5))- 8.571428571428571).abs() < 1e-10); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct VWMA { + sum: ValueType, + vol_sum: ValueType, + window: Window<(ValueType, ValueType)>, +} + +impl Method for VWMA { + type Params = PeriodType; + type Input = (ValueType, ValueType); + type Output = ValueType; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "VWMA: length should be > 0"); + + Self { + sum: value.0 * value.1 * length as ValueType, + vol_sum: value.1 * length as ValueType, + window: Window::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let past_value = self.window.push(value); + + self.vol_sum += value.1 - past_value.1; + self.sum += value.0.mul_add(value.1, -past_value.0 * past_value.1); + + self.sum / self.vol_sum + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, VWMA as TestingMethod}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_vwma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = ((i as ValueType + 56.0) / 16.3251, 3.55); + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_vwma1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, (candles.first().close, candles.first().volume)); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next((x.close, x.volume))).abs() < SIGMA); + }); + } + + #[test] + fn test_vwma() { + let candles = RandomCandles::default(); + + let src: Vec<(ValueType, ValueType)> = + candles.take(100).map(|x| (x.close, x.volume)).collect(); + + (1..20).for_each(|ma_length| { + let mut ma = TestingMethod::new(ma_length, src[0]); + let ma_length = ma_length as usize; + + src.iter().enumerate().for_each(|(i, &x)| { + let mut slice: Vec<(ValueType, ValueType)> = Vec::with_capacity(ma_length); + for x in 0..ma_length { + slice.push(src[i.saturating_sub(x)]); + } + + let sum = slice + .iter() + .fold(0.0, |s, &(close, volume)| s + close * volume); + let vol_sum = slice.iter().fold(0.0, |s, &(_close, vol)| s + vol); + + let value2 = sum / vol_sum; + assert!((value2 - ma.next(x)).abs() < SIGMA); + }); + }); + } +} diff --git a/src/methods/wma.rs b/src/methods/wma.rs new file mode 100644 index 0000000..6e1ed6c --- /dev/null +++ b/src/methods/wma.rs @@ -0,0 +1,154 @@ +use crate::core::Method; +use crate::core::{PeriodType, ValueType, Window}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// [Weighed Moving Average](https://en.wikipedia.org/wiki/Moving_average#Weighted_moving_average) of specified `length` for timeseries of type [`ValueType`]. +/// +/// # Parameters +/// +/// Has a single parameter `length`: [`PeriodType`] +/// +/// `length` should be > 0 +/// +/// # Input type +/// +/// Input type is [`ValueType`] +/// +/// # Output type +/// +/// Output type is [`ValueType`] +/// +/// # Examples +/// +/// ``` +/// use yata::prelude::*; +/// use yata::methods::WMA; +/// +/// // WMA of length=3 +/// let mut wma = WMA::new(3, 3.0); +/// +/// wma.next(3.0); +/// wma.next(6.0); +/// +/// assert_eq!(wma.next(9.0), 7.0); +/// assert_eq!(wma.next(12.0), 10.0); +/// ``` +/// +/// # Perfomance +/// +/// O(1) +/// +/// # See also +/// +/// [Volume Weighted Moving Average](crate::methods::VWMA) for computing weighted moving average with custom weights over every value +/// +/// [`ValueType`]: crate::core::ValueType +/// [`PeriodType`]: crate::core::PeriodType +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct WMA { + sum: ValueType, + invert_sum: ValueType, + float_length: ValueType, + total: ValueType, + numerator: ValueType, + window: Window, +} + +impl Method for WMA { + type Params = PeriodType; + type Input = ValueType; + type Output = Self::Input; + + fn new(length: Self::Params, value: Self::Input) -> Self { + debug_assert!(length > 0, "WMA: length should be > 0"); + + let length2 = length as usize; + let sum = ((length2 * (length2 + 1)) / 2) as ValueType; + let float_length = length as ValueType; + Self { + sum: sum, + invert_sum: sum.recip(), + float_length, + total: value * float_length, + numerator: value * sum, + window: Window::new(length, value), + } + } + + #[inline] + fn next(&mut self, value: Self::Input) -> Self::Output { + let prev_value = self.window.push(value); + + self.numerator += self.float_length.mul_add(value, -self.total); + self.total += value - prev_value; + + self.numerator * self.invert_sum + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + use super::{Method, WMA as TestingMethod}; + use crate::core::ValueType; + use crate::helpers::RandomCandles; + use crate::methods::Conv; + + #[allow(dead_code)] + const SIGMA: ValueType = 1e-8; + + #[test] + fn test_wma_const() { + use super::*; + use crate::core::{Candle, Method}; + use crate::methods::tests::test_const; + + for i in 1..30 { + let input = (i as ValueType + 56.0) / 16.3251; + let mut method = TestingMethod::new(i, input); + + let output = method.next(input); + test_const(&mut method, input, output); + } + } + + #[test] + fn test_wma1() { + let mut candles = RandomCandles::default(); + + let mut ma = TestingMethod::new(1, candles.first().close); + + candles.take(100).for_each(|x| { + assert!((x.close - ma.next(x.close)).abs() < SIGMA); + }); + } + + #[test] + fn test_wma() { + let candles = RandomCandles::default(); + + let src: Vec = candles.take(100).map(|x| x.close).collect(); + + (1..20).for_each(|ma_length| { + let mut ma = TestingMethod::new(ma_length, src[0]); + let mut conv = Conv::new((1..=ma_length).map(|x| x as ValueType).collect(), src[0]); + let ma_length = ma_length as usize; + + let div = (1..=ma_length).sum::() as ValueType; + src.iter().enumerate().for_each(|(i, &x)| { + let value = ma.next(x); + let value2 = (0..ma_length).fold(0.0, |s, v| { + let j = i.saturating_sub(v); + s + src[j] * (ma_length - v) as ValueType + }) / div; + let value3 = conv.next(x); + + assert!((value2 - value).abs() < SIGMA); + assert!((value3 - value).abs() < SIGMA); + }); + }); + } +}