use crate::bar_indicators::average::ema::Ema;
use crate::bar_indicators::indicator_value::IndicatorValue;
use super::super::ohlcv_field::OhlcvField;
#[derive(Debug, Clone, Copy)]
pub struct ZeroLagEmaResult {
pub zlema: f64, pub regular_ema: f64, pub lag_compensation: f64, pub trend_direction: i8, pub trend_strength: f64, pub responsiveness: f64, }
impl ZeroLagEmaResult {
pub fn empty() -> Self {
Self {
zlema: 0.0,
regular_ema: 0.0,
lag_compensation: 0.0,
trend_direction: 0,
trend_strength: 0.0,
responsiveness: 1.0,
}
}
pub fn trend_direction_name(&self) -> &'static str {
match self.trend_direction {
1 => "Восходящий",
-1 => "Нисходящий",
_ => "Боковой",
}
}
pub fn trend_strength_name(&self) -> &'static str {
match self.trend_strength {
x if x < 0.2 => "Очень слабый",
x if x < 0.4 => "Слабый",
x if x < 0.6 => "Умеренный",
x if x < 0.8 => "Сильный",
_ => "Очень сильный",
}
}
}
#[derive(Debug, Clone)]
pub struct EhlersZeroLagEma {
main_ema: Ema, lag_ema: Ema, trend_ema: Ema,
prices: Vec<f64>, zlema_values: Vec<f64>, ema_values: Vec<f64>,
period: usize,
source: OhlcvField, lag_period: usize,
current_result: ZeroLagEmaResult,
is_ready: bool,
update_count: usize,
}
impl EhlersZeroLagEma {
pub fn new() -> Self {
Self::with_period(21)
}
pub fn with_period(period: usize) -> Self {
Self::with_source(period, OhlcvField::Close)
}
pub fn with_source(period: usize, source: OhlcvField) -> Self {
assert!(period > 0, "Period must be greater than 0");
let lag_period = ((period - 1) / 2).max(1);
Self {
main_ema: Ema::with_source(period, source),
lag_ema: Ema::new(period),
trend_ema: Ema::new(5),
prices: Vec::with_capacity(period),
zlema_values: Vec::with_capacity(16),
ema_values: Vec::with_capacity(16),
period,
source,
lag_period,
current_result: ZeroLagEmaResult::empty(),
is_ready: false,
update_count: 0,
}
}
pub fn with_custom_lag(period: usize, lag_period: usize) -> Self {
Self::with_custom_lag_and_source(period, lag_period, OhlcvField::Close)
}
pub fn with_custom_lag_and_source(period: usize, lag_period: usize, source: OhlcvField) -> Self {
assert!(period > 0, "Period must be greater than 0");
assert!(lag_period > 0 && lag_period < period,
"Lag period must be positive and less than main period");
let mut zlema = Self::with_source(period, source);
zlema.lag_period = lag_period;
zlema
}
pub fn update_bar(&mut self, open: f64, high: f64, low: f64, close: f64, volume: f64) -> ZeroLagEmaResult {
let price = self.source.extract(open, high, low, close, volume);
self.update_price(price)
}
pub fn update_price(&mut self, price: f64) -> ZeroLagEmaResult {
if self.prices.len() >= 32 {
self.prices.remove(0);
}
self.prices.push(price);
let current_ema = self.main_ema.update_bar(0.0, 0.0, 0.0, price, 0.0);
if self.ema_values.len() >= 16 {
self.ema_values.remove(0);
}
self.ema_values.push(current_ema);
let lagged_ema = if self.ema_values.len() > self.lag_period {
self.ema_values[self.ema_values.len() - 1 - self.lag_period]
} else {
current_ema
};
let lag_compensation = current_ema - lagged_ema;
let zlema = current_ema + lag_compensation;
if self.zlema_values.len() >= 16 {
self.zlema_values.remove(0);
}
self.zlema_values.push(zlema);
self.analyze_trend_and_strength(price, zlema, current_ema);
self.calculate_responsiveness(price, zlema, current_ema);
self.current_result.zlema = zlema;
self.current_result.regular_ema = current_ema;
self.current_result.lag_compensation = lag_compensation;
if self.main_ema.is_ready() && self.ema_values.len() > self.lag_period {
self.is_ready = true;
}
self.update_count += 1;
self.current_result
}
fn analyze_trend_and_strength(&mut self, current_price: f64, zlema: f64, ema: f64) {
if self.zlema_values.len() < 3 {
return;
}
let len = self.zlema_values.len();
let current_zlema = self.zlema_values[len - 1];
let prev_zlema = self.zlema_values[len - 2];
let prev2_zlema = self.zlema_values[len - 3];
let trend_change = current_zlema - prev_zlema;
let prev_trend_change = prev_zlema - prev2_zlema;
self.current_result.trend_direction = if trend_change > 0.0 && prev_trend_change > 0.0 {
1 } else if trend_change < 0.0 && prev_trend_change < 0.0 {
-1 } else {
0 };
let price_distance = (current_price - zlema).abs();
let ema_distance = (zlema - ema).abs();
let trend_strength = if zlema != 0.0 {
((price_distance + ema_distance) / zlema.abs()).min(1.0)
} else {
0.0
};
self.current_result.trend_strength = self.trend_ema.update_bar(0.0, 0.0, 0.0, trend_strength, 0.0);
}
fn calculate_responsiveness(&mut self, current_price: f64, zlema: f64, ema: f64) {
if self.prices.len() < 2 {
self.current_result.responsiveness = 1.0;
return;
}
let len = self.prices.len();
let prev_price = self.prices[len - 2];
let price_change = (current_price - prev_price).abs();
if price_change > 0.0 {
let zlema_response = (zlema - prev_price).abs();
let ema_response = (ema - prev_price).abs();
if ema_response > 0.0 {
self.current_result.responsiveness = (zlema_response / ema_response).min(3.0);
} else {
self.current_result.responsiveness = 1.0;
}
} else {
self.current_result.responsiveness = 1.0;
}
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Single(self.current_result.zlema)
}
pub fn result(&self) -> ZeroLagEmaResult {
self.current_result
}
pub fn is_ready(&self) -> bool {
self.is_ready
}
pub fn reset(&mut self) {
self.main_ema.reset();
self.lag_ema.reset();
self.trend_ema.reset();
self.prices.clear();
self.zlema_values.clear();
self.ema_values.clear();
self.current_result = ZeroLagEmaResult::empty();
self.is_ready = false;
self.update_count = 0;
}
pub fn period(&self) -> usize {
self.period
}
pub fn trading_signal(&self, current_price: f64, prev_price: f64) -> i8 {
if !self.is_ready || self.zlema_values.len() < 2 {
return 0;
}
let current_zlema = self.current_result.zlema;
let prev_zlema = self.zlema_values[self.zlema_values.len() - 2];
if prev_price <= prev_zlema && current_price > current_zlema {
return 1; } else if prev_price >= prev_zlema && current_price < current_zlema {
return -1; }
0
}
pub fn trend_signal(&self) -> i8 {
if !self.is_ready {
return 0;
}
let result = self.current_result;
if result.trend_strength > 0.3 {
return result.trend_direction;
}
0
}
pub fn divergence_signal(&self) -> i8 {
if !self.is_ready || self.zlema_values.len() < 2 || self.ema_values.len() < 2 {
return 0;
}
let zlema_len = self.zlema_values.len();
let ema_len = self.ema_values.len();
let current_zlema = self.zlema_values[zlema_len - 1];
let prev_zlema = self.zlema_values[zlema_len - 2];
let current_ema = self.ema_values[ema_len - 1];
let prev_ema = self.ema_values[ema_len - 2];
let zlema_direction = (current_zlema - prev_zlema).signum() as i8;
let ema_direction = (current_ema - prev_ema).signum() as i8;
if zlema_direction != 0 && ema_direction != 0 && zlema_direction != ema_direction {
return zlema_direction; }
0
}
pub fn info(&self, current_price: f64) -> String {
let result = self.current_result;
let position = if current_price > result.zlema { "Выше" } else { "Ниже" };
format!(
"Zero Lag EMA: {:.4}, EMA: {:.4}, Цена {} ZLEMA, Тренд: {} ({}), Отзывчивость: {:.1}x",
result.zlema,
result.regular_ema,
position,
result.trend_direction_name(),
result.trend_strength_name(),
result.responsiveness
)
}
pub fn additional_values(&self) -> std::collections::HashMap<String, f64> {
let mut values = std::collections::HashMap::new();
values.insert("zlema".to_string(), self.current_result.zlema);
values.insert("regular_ema".to_string(), self.current_result.regular_ema);
values.insert("lag_compensation".to_string(), self.current_result.lag_compensation);
values.insert("trend_direction".to_string(), self.current_result.trend_direction as f64);
values.insert("trend_strength".to_string(), self.current_result.trend_strength);
values.insert("responsiveness".to_string(), self.current_result.responsiveness);
values
}
pub fn update_count(&self) -> usize {
self.update_count
}
pub fn parameters(&self) -> (usize, usize) {
(self.period, self.lag_period)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero_lag_ema_creation() {
let zlema = EhlersZeroLagEma::new();
assert!(!zlema.is_ready());
assert_eq!(zlema.period(), 21);
}
#[test]
fn test_zero_lag_ema_with_period() {
let zlema = EhlersZeroLagEma::with_period(14);
assert_eq!(zlema.period(), 14);
assert_eq!(zlema.parameters().1, 6);
}
#[test]
fn test_zero_lag_ema_with_custom_lag() {
let zlema = EhlersZeroLagEma::with_custom_lag(20, 5);
assert_eq!(zlema.parameters(), (20, 5));
}
#[test]
fn test_zero_lag_ema_update() {
let mut zlema = EhlersZeroLagEma::new();
for i in 0..30 {
let price = 100.0 + i as f64 * 0.5;
let result = zlema.update_price(price);
if i > 25 {
assert!(zlema.is_ready());
assert!(result.zlema.is_finite());
}
}
assert_eq!(zlema.result().trend_direction, 1);
}
#[test]
fn test_responsiveness() {
let mut zlema = EhlersZeroLagEma::with_period(5);
let prices = [100.0, 100.0, 100.0, 105.0, 110.0];
for &price in &prices {
let result = zlema.update_price(price);
if zlema.is_ready() {
assert!(result.responsiveness >= 1.0);
}
}
}
#[test]
fn test_trading_signals() {
let mut zlema = EhlersZeroLagEma::new();
for i in 0..25 {
let price = 100.0 + i as f64;
let _result = zlema.update_price(price);
}
if zlema.is_ready() {
let signal = zlema.trading_signal(125.0, 124.0);
assert!(signal >= -1 && signal <= 1);
}
}
}