Skip to main content

indicatorMath_ULTRA_Rust/
generator.rs

1use crate::structs::{AnalysisOptions, AnalysisResult, BBValues, Candle, CandleMasterCode};
2use serde::{Deserialize, Serialize};
3use std::collections::VecDeque;
4
5#[derive(Serialize, Deserialize, Debug, Clone)]
6pub struct GeneratorState {
7    pub last_ema_1: f64,
8    pub last_ema_2: f64,
9    pub last_ema_3: f64,
10    pub last_atr: f64,
11    pub rsi_avg_gain: f64,
12    pub rsi_avg_loss: f64,
13    pub up_con_medium_ema: usize,
14    pub down_con_medium_ema: usize,
15    pub up_con_long_ema: usize,
16    pub down_con_long_ema: usize,
17    pub last_ema_cut_index: Option<usize>,
18    pub prev_analysis: Option<AnalysisResult>,
19    pub last_analysis: Option<AnalysisResult>,
20    pub last_candle: Option<Candle>,
21
22    // RSI
23    pub rsi_period: usize,
24
25    // ATR
26    pub atr_period: usize,
27
28    // ADX
29    pub tr_sum: f64,
30    pub pdm_sum: f64,
31    pub mdm_sum: f64,
32    pub adx_val: f64,
33    pub dx_count: usize,
34    pub adx_period: usize,
35
36    // BB
37    pub bb_window: VecDeque<f64>,
38    pub bb_period: usize,
39
40    // CI
41    pub ci_window: VecDeque<Candle>,
42    pub ci_atr_window: VecDeque<f64>,
43    pub ci_period: usize,
44
45    // Config cache
46    pub ema_1_k: f64,
47    pub ema_2_k: f64,
48    pub ema_3_k: f64,
49
50    // HMA/EHMA State
51    // We need to store history for WMA/EMA calculations if we want to do incremental HMA/EHMA
52    // HMA(n) = WMA(2*WMA(n/2) - WMA(n), sqrt(n))
53    // This is complex for O(1).
54    // For now, let's implement effective buffering for these types.
55    // Ideally O(1) HMA is hard. We might need O(N) over window.
56    pub close_window: VecDeque<f64>, // For types needing window like HMA
57    pub max_ma_period: usize,
58}
59
60impl GeneratorState {
61    pub fn new(options: &AnalysisOptions) -> Self {
62        let max_period = options
63            .ema1_period
64            .max(options.ema2_period)
65            .max(options.ema3_period);
66        // HMA needs slightly more for safety or internal WMAs
67        let buffer_size = max_period * 2;
68
69        Self {
70            last_ema_1: 0.0,
71            last_ema_2: 0.0,
72            last_ema_3: 0.0,
73            last_atr: 0.0,
74            rsi_avg_gain: 0.0,
75            rsi_avg_loss: 0.0,
76            up_con_medium_ema: 0,
77            down_con_medium_ema: 0,
78            up_con_long_ema: 0,
79            down_con_long_ema: 0,
80            last_ema_cut_index: None,
81            prev_analysis: None,
82            last_analysis: None,
83            last_candle: None,
84            atr_period: options.atr_period,
85            rsi_period: options.rsi_period,
86            tr_sum: 0.0,
87            pdm_sum: 0.0,
88            mdm_sum: 0.0,
89            adx_val: 0.0,
90            dx_count: 0,
91            adx_period: options.adx_period,
92            bb_window: VecDeque::with_capacity(options.bb_period),
93            bb_period: options.bb_period,
94            ci_window: VecDeque::with_capacity(options.ci_period),
95            ci_atr_window: VecDeque::with_capacity(options.ci_period),
96            ci_period: options.ci_period,
97            ema_1_k: 2.0 / (options.ema1_period as f64 + 1.0),
98            ema_2_k: 2.0 / (options.ema2_period as f64 + 1.0),
99            ema_3_k: 2.0 / (options.ema3_period as f64 + 1.0),
100
101            close_window: VecDeque::with_capacity(buffer_size),
102            max_ma_period: buffer_size,
103        }
104    }
105}
106
107#[derive(Clone)]
108pub struct AnalysisGenerator {
109    options: AnalysisOptions,
110    pub state: GeneratorState,
111    pub analysis_array: Vec<AnalysisResult>,
112    pub candle_data: Vec<Candle>,
113    pub current_candle: Option<Candle>,
114    pub master_codes: std::sync::Arc<Vec<CandleMasterCode>>,
115}
116
117impl AnalysisGenerator {
118    pub fn new(
119        options: AnalysisOptions,
120        master_codes: std::sync::Arc<Vec<CandleMasterCode>>,
121    ) -> Self {
122        let state = GeneratorState::new(&options);
123        Self {
124            options,
125            state,
126            analysis_array: Vec::new(),
127            candle_data: Vec::new(),
128            current_candle: None,
129            master_codes,
130        }
131    }
132
133    // Helper: WMA Calculation
134    fn calculate_wma(data: &[f64], period: usize) -> f64 {
135        if data.len() < period {
136            return 0.0;
137        }
138        let mut num = 0.0;
139        let mut den = 0.0;
140        for j in 0..period {
141            let val = data[data.len() - 1 - j];
142            let w = (period - j) as f64;
143            num += val * w;
144            den += w;
145        }
146        num / den
147    }
148
149    // Helper: EMA Calculation on slice (simple)
150    fn calculate_ema_slice(data: &[f64], period: usize) -> f64 {
151        if data.is_empty() {
152            return 0.0;
153        }
154        let k = 2.0 / (period as f64 + 1.0);
155        let mut ema = data[0];
156        for i in 1..data.len() {
157            ema = data[i] * k + ema * (1.0 - k);
158        }
159        ema
160    }
161
162    // Helper: HMA Calculation
163    // HMA = WMA(2 * WMA(n/2) - WMA(n), sqrt(n))
164    fn calculate_ma(
165        &self,
166        ma_type: &str,
167        period: usize,
168        current_price: f64,
169        last_ema_state: f64,
170        ema_k: f64,
171    ) -> f64 {
172        match ma_type {
173            "EMA" => current_price * ema_k + last_ema_state * (1.0 - ema_k),
174            "HMA" | "EHMA" => {
175                // For HMA/EHMA in incremental, we need the window.
176                // We use state.close_window which is updated in append_candle
177                // Note: 'current_price' is new, not yet in window if we call this before pushing?
178                // Let's assume input 'data' includes everything or we pass window.
179
180                // Construct a temporary window including current price
181                // Only needed for calculation
182                // Performance warning: copying vector
183
184                // We can optimize by iterating strictly.
185
186                // Ensure we have enough data
187                if self.state.close_window.len() + 1 < period {
188                    return current_price;
189                }
190
191                // Combine window + current
192                let iter = self
193                    .state
194                    .close_window
195                    .iter()
196                    .chain(std::iter::once(&current_price));
197                // Collecting to vec is expensive. We should change design if perf critical.
198                // But for < 200 items it's microsecond scale.
199                let data: Vec<f64> = iter.copied().collect();
200
201                if ma_type == "HMA" {
202                    let half = (period / 2).max(1);
203                    let sqrt = (period as f64).sqrt() as usize;
204
205                    // We need WMA of last 'sqrt' points.
206                    // The inputs to this WMA are (2*WMA(half) - WMA(period)).
207                    // We need to generate a series of these "raw" values for the last 'sqrt' points.
208
209                    let mut raw_series = Vec::new();
210                    let needed = sqrt; // We need 'needed' points of Raw Val.
211                                       // To get 1 Raw Val at index T, we need WMA(half) and WMA(period) ending at T.
212                                       // So we need data up to T.
213
214                    // We need to calculate RawVal for i = len-sqrt to len-1
215                    let len = data.len();
216                    if len < period {
217                        return current_price;
218                    }
219
220                    for i in 0..needed {
221                        let end_idx = len - (needed - 1 - i); // ending index (exclusive of slice?) no, include data[end_idx-1]
222                                                              // slice 0..end_idx
223                        if end_idx < period {
224                            continue;
225                        }
226
227                        let slice = &data[0..end_idx];
228                        // Efficiency: This re-calculates WMAs many times.
229                        // For incremental tick: O(period * sqrt(period)).
230                        // With period=200, ~200*14 = 2800 ops. Very fast.
231
232                        let wma_half = Self::calculate_wma(slice, half);
233                        let wma_full = Self::calculate_wma(slice, period);
234                        let raw = 2.0 * wma_half - wma_full;
235                        raw_series.push(raw);
236                    }
237
238                    Self::calculate_wma(&raw_series, sqrt)
239                } else {
240                    // EHMA
241                    let half = (period / 2).max(1);
242                    let sqrt = (period as f64).sqrt() as usize;
243
244                    // Similar to HMA but using EMA
245                    // EMA is stateful. We need to recalculate EMAs from start of window or keep state?
246                    // Recalculating EMA from scratch for the window is safer for consistence.
247                    // Optimization: Use cached EMA if possible?
248                    // Incremental EHMA is tricky without full series re-calc.
249                    // We will do full re-calc on the window for correctness.
250
251                    let len = data.len();
252                    if len < period {
253                        return current_price;
254                    }
255
256                    // We need enough raw values for the final EMA(sqrt)
257                    // But EMA needs to settle.
258                    // Simple implementation: Calculate Raw Series for whole available window, then EMA it.
259
260                    let mut raw_series = Vec::new();
261                    // Optimization: Calculate EMA_Half and EMA_Full incrementally over the window
262
263                    let k_half = 2.0 / (half as f64 + 1.0);
264                    let k_full = 2.0 / (period as f64 + 1.0);
265
266                    let mut val_half = data[0];
267                    let mut val_full = data[0];
268
269                    // Spin up to period
270                    for i in 1..len {
271                        val_half = data[i] * k_half + val_half * (1.0 - k_half);
272                        val_full = data[i] * k_full + val_full * (1.0 - k_full);
273
274                        let raw = 2.0 * val_half - val_full;
275                        raw_series.push(raw);
276                    }
277
278                    // Now EMA(sqrt) on raw_series
279                    // We only care about the last value
280                    Self::calculate_ema_slice(&raw_series, sqrt)
281                }
282            }
283            _ => current_price * ema_k + last_ema_state * (1.0 - ema_k),
284        }
285    }
286
287    // Helper: EMA Direction
288    fn get_ema_direction(&self, prev: f64, curr: f64) -> String {
289        let diff = prev - curr;
290        if diff.abs() <= self.options.flat_threshold {
291            "Flat".to_string()
292        } else if prev < curr {
293            "Up".to_string()
294        } else {
295            "Down".to_string()
296        }
297    }
298
299    pub fn append_candle(&mut self, new_candle: Candle) -> AnalysisResult {
300        let i = self.analysis_array.len();
301        let prev_candle = self.state.last_candle; // Copy
302
303        // Push candle
304        self.candle_data.push(new_candle);
305
306        // 1. EMA/HMA/EHMA Logic
307        // Update history window first
308        self.state.close_window.push_back(new_candle.close);
309        if self.state.close_window.len() > self.state.max_ma_period {
310            self.state.close_window.pop_front();
311        }
312
313        let close = new_candle.close;
314
315        // Handle init (first candle) - if it's the very first, MA is just the price
316        let new_ema_1 = if i == 0 {
317            close
318        } else {
319            self.calculate_ma(
320                &self.options.ema1_type,
321                self.options.ema1_period,
322                close,
323                self.state.last_ema_1,
324                self.state.ema_1_k,
325            )
326        };
327        let new_ema_2 = if i == 0 {
328            close
329        } else {
330            self.calculate_ma(
331                &self.options.ema2_type,
332                self.options.ema2_period,
333                close,
334                self.state.last_ema_2,
335                self.state.ema_2_k,
336            )
337        };
338        let new_ema_3 = if i == 0 {
339            close
340        } else {
341            self.calculate_ma(
342                &self.options.ema3_type,
343                self.options.ema3_period,
344                close,
345                self.state.last_ema_3,
346                self.state.ema_3_k,
347            )
348        };
349
350        // Directions
351        let ema_1_dir = if i > 0 {
352            self.get_ema_direction(self.state.last_ema_1, new_ema_1)
353        } else {
354            "Flat".to_string()
355        };
356        let ema_2_dir = if i > 0 {
357            self.get_ema_direction(self.state.last_ema_2, new_ema_2)
358        } else {
359            "Flat".to_string()
360        };
361        let ema_3_dir = if i > 0 {
362            self.get_ema_direction(self.state.last_ema_3, new_ema_3)
363        } else {
364            "Flat".to_string()
365        };
366
367        // Turn Type
368        let mut ema_short_turn_type = "-".to_string();
369        if let Some(prev_analysis) = &self.state.prev_analysis {
370            // Logic from JS: checks 2 steps back
371            // We need to store enough history or rely on prev_analysis values
372            // JS: if (i >= 2 && ...)
373            // We use the last stored EMA values + current.
374            // But we need ema1[i-2].
375            // In state we only have lastEma1 (i-1) and newEma1 (i).
376            // We need ema1[i-2], which is `prev_analysis.emaShortValue`?
377            // Wait, JS uses `this.ema1Data` array.
378            // In incremental, we access `st.prevAnalysis.emaShortValue`.
379            // `st.lastEma1` corresponds to `i-1`. `st.prevAnalysis` corresponds to `i-2` IF we just pushed `i-1`?
380            // No. `prev_analysis` is `i-2` relative to `new_candle` (which is `i`)?
381            // `state.last_analysis` is `i-1`. `state.prev_analysis` is `i-2`.
382            if let Some(prev_prev_val) = prev_analysis.ema_short_value {
383                if let Some(_prev_val) = self
384                    .state
385                    .last_analysis
386                    .as_ref()
387                    .map(|a| a.ema_short_value)
388                    .flatten()
389                {
390                    // This corresponds to state.last_ema_1
391                    // Wait, last_ema_1 IS the value at i-1.
392                    // prev_analysis.ema_short_value IS the value at i-2?
393                    // Let's check JS: `const prevEma1Before = st.prevAnalysis.emaShortValue;`
394                    // `st.prevAnalysis` is set to `st.lastAnalysis` at the END of append.
395                    // So at the START of append, `st.lastAnalysis` is index `i-1`. `st.prevAnalysis` is index `i-2`.
396                    // Correct.
397
398                    let curr_diff = new_ema_1 - self.state.last_ema_1;
399                    let prev_diff = self.state.last_ema_1 - prev_prev_val;
400
401                    let curr_dir_calc = if curr_diff > 0.0001 {
402                        "Up"
403                    } else if curr_diff < -0.0001 {
404                        "Down"
405                    } else {
406                        "Flat"
407                    };
408                    let prev_dir_calc = if prev_diff > 0.0001 {
409                        "Up"
410                    } else if prev_diff < -0.0001 {
411                        "Down"
412                    } else {
413                        "Flat"
414                    };
415
416                    if curr_dir_calc == "Up" && prev_dir_calc == "Down" {
417                        ema_short_turn_type = "TurnUp".to_string();
418                    } else if curr_dir_calc == "Down" && prev_dir_calc == "Up" {
419                        ema_short_turn_type = "TurnDown".to_string();
420                    }
421                }
422            }
423        }
424
425        // Consecutives
426        let mut up_con_medium_ema = self.state.up_con_medium_ema;
427        let mut down_con_medium_ema = self.state.down_con_medium_ema;
428
429        if ema_2_dir == "Up" {
430            up_con_medium_ema += 1;
431            down_con_medium_ema = 0;
432        } else if ema_2_dir == "Down" {
433            down_con_medium_ema += 1;
434            up_con_medium_ema = 0;
435        }
436
437        let mut up_con_long_ema = self.state.up_con_long_ema;
438        let mut down_con_long_ema = self.state.down_con_long_ema;
439
440        if ema_3_dir == "Up" {
441            up_con_long_ema += 1;
442            down_con_long_ema = 0;
443        } else if ema_3_dir == "Down" {
444            down_con_long_ema += 1;
445            up_con_long_ema = 0;
446        }
447
448        // 2. MACD
449        let ema_above = if new_ema_1 > new_ema_2 {
450            "ShortAbove"
451        } else {
452            "MediumAbove"
453        }
454        .to_string();
455        let ema_long_above = if new_ema_2 > new_ema_3 {
456            "MediumAbove"
457        } else {
458            "LongAbove"
459        }
460        .to_string();
461
462        let macd_12 = (new_ema_1 - new_ema_2).abs();
463        let macd_23 = (new_ema_2 - new_ema_3).abs();
464
465        let prev_macd_12 = self.state.last_analysis.as_ref().and_then(|a| a.macd_12);
466        let prev_macd_23 = self.state.last_analysis.as_ref().and_then(|a| a.macd_23);
467
468        let ema_convergence_type = if let Some(prev) = prev_macd_12 {
469            if macd_12 > prev {
470                "divergence".to_string()
471            } else if macd_12 < prev {
472                "convergence".to_string()
473            } else {
474                "neutral".to_string()
475            }
476        } else {
477            "neutral".to_string()
478        }; // Or handle as Option
479
480        let ema_long_convergence_type = if let Some(prev) = prev_macd_23 {
481            if macd_23 > prev {
482                "D".to_string()
483            } else if macd_23 < prev {
484                "C".to_string()
485            } else {
486                "N".to_string()
487            }
488        } else {
489            "N".to_string()
490        };
491
492        // EmaCutLongType
493        let mut ema_cut_long_type = None;
494        if i > 0 {
495            // Need prev
496            let prev_medium_above = self.state.last_ema_2 > self.state.last_ema_3;
497            let curr_medium_above = new_ema_2 > new_ema_3;
498            if curr_medium_above != prev_medium_above {
499                ema_cut_long_type = Some(if curr_medium_above {
500                    "UpTrend".to_string()
501                } else {
502                    "DownTrend".to_string()
503                });
504            }
505        }
506
507        let mut last_ema_cut_index = self.state.last_ema_cut_index;
508        if ema_cut_long_type.is_some() {
509            last_ema_cut_index = Some(i);
510        }
511        let candles_since_ema_cut = last_ema_cut_index.map(|idx| i - idx);
512
513        // 3. ATR
514        let tr = if let Some(p) = prev_candle {
515            (new_candle.high - new_candle.low)
516                .max((new_candle.high - p.close).abs())
517                .max((new_candle.low - p.close).abs())
518        } else {
519            new_candle.high - new_candle.low
520        };
521
522        let new_atr = if i < self.state.atr_period {
523            if i == 0 {
524                tr
525            } else {
526                ((self.state.last_atr * i as f64) + tr) / (i as f64 + 1.0)
527            }
528        } else {
529            ((self.state.last_atr * (self.state.atr_period as f64 - 1.0)) + tr)
530                / self.state.atr_period as f64
531        };
532
533        // 4. RSI
534        let mut rsi_value = None;
535        let mut new_rsi_avg_gain = self.state.rsi_avg_gain;
536        let mut new_rsi_avg_loss = self.state.rsi_avg_loss;
537
538        if let Some(p) = prev_candle {
539            // Need to verify if i >= rsi_period logic matches JS.
540            // JS: if (prevCandle && i >= st.rsiPeriod)
541            // But RSI usually needs initialization period.
542            // In strict Incremental, we assume stream is long enough.
543            // However, for the *start*, we need to handle the first N candles.
544            // JS `calculateRSI` function handles initial slice.
545            // Incremental `appendCandle` handles update.
546
547            let change = close - p.close;
548            let gain = if change > 0.0 { change } else { 0.0 };
549            let loss = if change < 0.0 { change.abs() } else { 0.0 };
550
551            if i < self.state.rsi_period {
552                // Accumulating for initial average
553                // Wait, `i` is 0-indexed.
554                // JS `appendCandle`: if (i >= st.rsiPeriod)
555                // This implies that for i < period, it just accumulates or does nothing?
556                // JS `generate` calculates full RSI array.
557                // JS `_saveState` recalculates avgGain/Loss from history.
558                // If we start from scratch:
559                // We need to accumulate gains/losses for the first period.
560                // `new_rsi_avg_gain` can store sum during init.
561                if i == 0 {
562                    new_rsi_avg_gain = gain;
563                    new_rsi_avg_loss = loss;
564                } else {
565                    // Simple cumulative average for init
566                    new_rsi_avg_gain += gain;
567                    new_rsi_avg_loss += loss;
568                }
569
570                if i + 1 == self.state.rsi_period {
571                    // Finalize initial average
572                    new_rsi_avg_gain /= self.state.rsi_period as f64;
573                    new_rsi_avg_loss /= self.state.rsi_period as f64;
574                    // Calculate first RSI?
575                    let rs = if new_rsi_avg_loss == 0.0 {
576                        100.0
577                    } else {
578                        new_rsi_avg_gain / new_rsi_avg_loss
579                    };
580                    rsi_value = Some(100.0 - (100.0 / (1.0 + rs)));
581                }
582            } else {
583                // Wilder's Smoothing
584                new_rsi_avg_gain = (self.state.rsi_avg_gain * (self.state.rsi_period as f64 - 1.0)
585                    + gain)
586                    / self.state.rsi_period as f64;
587                new_rsi_avg_loss = (self.state.rsi_avg_loss * (self.state.rsi_period as f64 - 1.0)
588                    + loss)
589                    / self.state.rsi_period as f64;
590
591                let rs = if new_rsi_avg_loss == 0.0 {
592                    100.0
593                } else {
594                    new_rsi_avg_gain / new_rsi_avg_loss
595                };
596                rsi_value = Some(100.0 - (100.0 / (1.0 + rs)));
597            }
598        }
599
600        // 5. BB
601        self.state.bb_window.push_back(close);
602        if self.state.bb_window.len() > self.state.bb_period {
603            self.state.bb_window.pop_front();
604        }
605
606        let (bb_upper, bb_middle, bb_lower) = if self.state.bb_window.len() >= self.state.bb_period
607        {
608            let sum: f64 = self.state.bb_window.iter().sum();
609            let avg = sum / self.state.bb_period as f64;
610            let variance: f64 = self.state.bb_window.iter().map(|x| (x - avg).powi(2)).sum();
611            let std = (variance / self.state.bb_period as f64).sqrt();
612            (Some(avg + 2.0 * std), Some(avg), Some(avg - 2.0 * std))
613        } else {
614            (None, None, None)
615        };
616
617        let bb_position = if let (Some(u), Some(l)) = (bb_upper, bb_lower) {
618            let range = u - l;
619            let upper_zone = u - (range * 0.33);
620            let lower_zone = l + (range * 0.33);
621            if close >= upper_zone {
622                "NearUpper".to_string()
623            } else if close <= lower_zone {
624                "NearLower".to_string()
625            } else {
626                "Middle".to_string()
627            }
628        } else {
629            "Unknown".to_string()
630        };
631
632        // 6. CI
633        self.state.ci_window.push_back(new_candle);
634        if self.state.ci_window.len() > self.state.ci_period {
635            self.state.ci_window.pop_front();
636        }
637        self.state.ci_atr_window.push_back(new_atr);
638        if self.state.ci_atr_window.len() > self.state.ci_period {
639            self.state.ci_atr_window.pop_front();
640        }
641
642        let choppy_indicator = if self.state.ci_window.len() >= self.state.ci_period {
643            let high_max = self
644                .state
645                .ci_window
646                .iter()
647                .map(|c| c.high)
648                .fold(f64::NEG_INFINITY, f64::max);
649            let low_min = self
650                .state
651                .ci_window
652                .iter()
653                .map(|c| c.low)
654                .fold(f64::INFINITY, f64::min);
655            let sum_atr: f64 = self.state.ci_atr_window.iter().sum();
656
657            if (high_max - low_min) > 0.0 {
658                Some(
659                    100.0 * (sum_atr / (high_max - low_min)).log10()
660                        / (self.state.ci_period as f64).log10(),
661                )
662            } else {
663                Some(0.0)
664            }
665        } else {
666            None
667        };
668
669        // 7. ADX
670        let mut adx_value = None;
671        if let Some(p) = prev_candle {
672            let up_move = new_candle.high - p.high;
673            let down_move = p.low - new_candle.low;
674            let pdm = if up_move > down_move && up_move > 0.0 {
675                up_move
676            } else {
677                0.0
678            };
679            let mdm = if down_move > up_move && down_move > 0.0 {
680                down_move
681            } else {
682                0.0
683            };
684
685            // Update sums
686            if i < self.state.adx_period {
687                // Initial accumulation
688                self.state.tr_sum += tr;
689                self.state.pdm_sum += pdm;
690                self.state.mdm_sum += mdm;
691            } else {
692                self.state.tr_sum =
693                    self.state.tr_sum - (self.state.tr_sum / self.state.adx_period as f64) + tr;
694                self.state.pdm_sum =
695                    self.state.pdm_sum - (self.state.pdm_sum / self.state.adx_period as f64) + pdm;
696                self.state.mdm_sum =
697                    self.state.mdm_sum - (self.state.mdm_sum / self.state.adx_period as f64) + mdm;
698            }
699
700            if i >= self.state.adx_period && self.state.tr_sum > 0.0 {
701                let di_plus = (self.state.pdm_sum / self.state.tr_sum) * 100.0;
702                let di_minus = (self.state.mdm_sum / self.state.tr_sum) * 100.0;
703                let sum_di = di_plus + di_minus;
704                let dx = if sum_di == 0.0 {
705                    0.0
706                } else {
707                    (di_plus - di_minus).abs() / sum_di * 100.0
708                };
709
710                self.state.dx_count += 1;
711
712                if self.state.dx_count <= self.state.adx_period {
713                    // Should be < ? JS says `j < period`
714                    // Wait. JS dxValues usage: `if (j < period) adx += ...`
715                    // Essentially, the first ADX is an average of the first DX values.
716                    if self.state.dx_count == 1 {
717                        self.state.adx_val = dx; // First DX? No, average...
718                                                 // The JS logic builds an array of DXs then loops.
719                                                 // Incremental logic needs to approximate or replicate.
720                                                 // JS Tick:
721                                                 // if (st.dxCount < st.adxPeriod) st.adxVal += dx / st.adxPeriod;
722                                                 // else st.adxVal = ((st.adxVal * (st.adxPeriod - 1)) + dx) / st.adxPeriod;
723
724                        self.state.adx_val += dx / self.state.adx_period as f64;
725                    // This is wrong for first element?
726                    // If we sum them up then we are good.
727                    } else {
728                        self.state.adx_val += dx / self.state.adx_period as f64;
729                    }
730
731                    // Actually, let's follow JS strictly:
732                    // `if (st.dxCount < st.adxPeriod) st.adxVal += dx / st.adxPeriod;`
733                    // This means for the first `adxPeriod` DX values, it accumulates `dx/N`.
734                    // At the end of `adxPeriod` counts, `adxVal` is the average.
735                    // Wait, `st.dxCount` starts at 0.
736                } else {
737                    // `else st.adxVal = ((st.adxVal * (st.adxPeriod - 1)) + dx) / st.adxPeriod;`
738                    // This happens when we have enough history.
739                    self.state.adx_val =
740                        ((self.state.adx_val * (self.state.adx_period as f64 - 1.0)) + dx)
741                            / self.state.adx_period as f64;
742                }
743
744                if self.state.dx_count >= self.state.adx_period {
745                    adx_value = Some(self.state.adx_val);
746                }
747            }
748        }
749
750        // 8. Properties
751        let color = if close > new_candle.open {
752            "Green"
753        } else if close < new_candle.open {
754            "Red"
755        } else {
756            "Equal"
757        }
758        .to_string();
759        let pip_size = (close - new_candle.open).abs();
760        let body_top = new_candle.open.max(close);
761        let body_bottom = new_candle.open.min(close);
762        let u_wick = new_candle.high - body_top;
763        let body = (close - new_candle.open).abs();
764        let l_wick = body_bottom - new_candle.low;
765        let full_candle_size = new_candle.high - new_candle.low;
766
767        let body_percent = if full_candle_size > 0.0 {
768            (body / full_candle_size) * 100.0
769        } else {
770            0.0
771        };
772        let u_wick_percent = if full_candle_size > 0.0 {
773            (u_wick / full_candle_size) * 100.0
774        } else {
775            0.0
776        };
777        let l_wick_percent = if full_candle_size > 0.0 {
778            (l_wick / full_candle_size) * 100.0
779        } else {
780            0.0
781        };
782
783        let is_abnormal_candle = if let Some(p) = prev_candle {
784            let tr_val = (new_candle.high - new_candle.low)
785                .max((new_candle.high - p.close).abs())
786                .max((new_candle.low - p.close).abs());
787            tr_val > (new_atr * self.options.atr_multiplier)
788        } else {
789            false
790        };
791
792        let is_abnormal_atr = if new_atr > 0.0 {
793            (body > new_atr * self.options.atr_multiplier)
794                || (full_candle_size > new_atr * self.options.atr_multiplier * 1.5)
795        } else {
796            false
797        };
798
799        // emaCutPosition
800        let ema_cut_position = if new_ema_1 > new_candle.high {
801            Some("1".to_string())
802        } else if new_ema_1 >= body_top && new_ema_1 <= new_candle.high {
803            Some("2".to_string())
804        } else if new_ema_1 >= body_bottom && new_ema_1 < body_top {
805            let body_range = body_top - body_bottom;
806            if body_range > 0.0 {
807                let pos = (new_ema_1 - body_bottom) / body_range;
808                if pos >= 0.66 {
809                    Some("B1".to_string())
810                } else if pos >= 0.33 {
811                    Some("B2".to_string())
812                } else {
813                    Some("B3".to_string())
814                }
815            } else {
816                Some("B2".to_string())
817            }
818        } else if new_ema_1 >= new_candle.low && new_ema_1 < body_bottom {
819            Some("3".to_string())
820        } else if new_ema_1 < new_candle.low {
821            Some("4".to_string())
822        } else {
823            None
824        };
825
826        // StatusDesc
827        let _ema_long_above_char =
828            if ema_long_above != "LongAbove" && ema_long_above != "MediumAbove" {
829                "-"
830            } else {
831                &ema_long_above[0..1]
832            }; // Rough approx, need check
833               // JS: `emaLongAbove ? emaLongAbove.substr(0, 1) : '-'`
834               // emaLongAbove is "MediumAbove" or "LongAbove". So 'M' or 'L'.
835        let c1 = if ema_long_above == "MediumAbove" {
836            "M"
837        } else if ema_long_above == "LongAbove" {
838            "L"
839        } else {
840            "-"
841        };
842        let c2 = if ema_2_dir == "Up" {
843            "U"
844        } else if ema_2_dir == "Down" {
845            "D"
846        } else {
847            "F"
848        };
849        let c3 = if ema_3_dir == "Up" {
850            "U"
851        } else if ema_3_dir == "Down" {
852            "D"
853        } else {
854            "F"
855        };
856        let c4 = &color[0..1];
857        let c5 = if ema_long_convergence_type != "" {
858            &ema_long_convergence_type
859        } else {
860            "-"
861        };
862
863        // JS: emaLongAbove.0 - ema2Dir.0 - ema3Dir.0 - color.0 - emaLongConver
864        // Note: JS `SeriesDesc` calc logic
865        let status_desc = format!("{}-{}-{}-{}-{}", c1, c2, c3, c4, c5);
866
867        let mut status_code = "".to_string();
868        for code in self.master_codes.iter() {
869            if code.status_desc == status_desc {
870                status_code = code.status_code.clone();
871                break;
872            }
873        }
874
875        // Match StatusCode
876        // We need the `CandleMasterCode` list from init?
877        // For now, I'll pass it? Or just leave logic for manager?
878        // The struct has `status_code` field. The library needs to know how to map.
879        // User prompt: "receive websocket, assetList, CandleMasterCode... return... analysisObject"
880        // So `CandleMasterCode` is available.
881        // I will add a method to resolve status code, or let the caller do it.
882        // But `AnalysisResult` includes it.
883        // I will add a placeholder for now.
884
885        let _display_time = ""; // Need Chrono formatting
886
887        // Calculate SMC
888        let mut smc_ind = crate::smc::SmcIndicator::new(crate::smc::SmcConfig::default());
889        let smc_result = smc_ind.calculate(self.candle_data.as_slice());
890
891        let mut analysis_obj = AnalysisResult {
892            index: i,
893            candletime: new_candle.time,
894            candletime_display: "".to_string(), // TODO
895            open: new_candle.open,
896            high: new_candle.high,
897            low: new_candle.low,
898            close: new_candle.close,
899            color: color.clone(),
900            next_color: None,
901            pip_size,
902            ema_short_value: Some(new_ema_1),
903            ema_short_direction: ema_1_dir,
904            ema_short_turn_type: ema_short_turn_type,
905            ema_medium_value: Some(new_ema_2),
906            ema_medium_direction: ema_2_dir,
907            ema_long_value: Some(new_ema_3),
908            ema_long_direction: ema_3_dir,
909            ema_above: Some(ema_above),
910            ema_long_above: Some(ema_long_above),
911            macd_12: Some(macd_12),
912            macd_23: Some(macd_23),
913            previous_ema_short_value: self
914                .state
915                .last_analysis
916                .as_ref()
917                .and_then(|a| a.ema_short_value),
918            previous_ema_medium_value: self
919                .state
920                .last_analysis
921                .as_ref()
922                .and_then(|a| a.ema_medium_value),
923            previous_ema_long_value: self
924                .state
925                .last_analysis
926                .as_ref()
927                .and_then(|a| a.ema_long_value),
928            previous_macd_12: prev_macd_12,
929            previous_macd_23: prev_macd_23,
930            ema_convergence_type: Some(ema_convergence_type),
931            ema_long_convergence_type,
932            choppy_indicator,
933            adx_value,
934            rsi_value,
935            bb_values: BBValues {
936                upper: bb_upper,
937                middle: bb_middle,
938                lower: bb_lower,
939            },
940            bb_position,
941            atr: Some(new_atr),
942            is_abnormal_candle,
943            is_abnormal_atr,
944            u_wick,
945            u_wick_percent,
946            body,
947            body_percent,
948            l_wick,
949            l_wick_percent,
950            ema_cut_position,
951            ema_cut_long_type,
952            candles_since_ema_cut,
953            up_con_medium_ema,
954            down_con_medium_ema,
955            up_con_long_ema,
956            down_con_long_ema,
957            is_mark: "n".to_string(),
958            status_code,
959            status_desc: status_desc.clone(),
960            status_desc_0: status_desc,
961            hint_status: "".to_string(),
962            suggest_color: "".to_string(),
963            win_status: "".to_string(),
964            win_con: 0,
965            loss_con: 0,
966            smc: Some(smc_result),
967        };
968
969        // Update next color of previous
970        if let Some(last) = self.analysis_array.last_mut() {
971            last.next_color = Some(color);
972        }
973
974        self.analysis_array.push(analysis_obj.clone());
975
976        // Update state
977        self.state.last_ema_1 = new_ema_1;
978        self.state.last_ema_2 = new_ema_2;
979        self.state.last_ema_3 = new_ema_3;
980        self.state.last_atr = new_atr;
981        self.state.rsi_avg_gain = new_rsi_avg_gain;
982        self.state.rsi_avg_loss = new_rsi_avg_loss;
983        self.state.up_con_medium_ema = up_con_medium_ema;
984        self.state.down_con_medium_ema = down_con_medium_ema;
985        self.state.up_con_long_ema = up_con_long_ema;
986        self.state.down_con_long_ema = down_con_long_ema;
987        self.state.last_ema_cut_index = last_ema_cut_index;
988
989        self.state.prev_analysis = self.state.last_analysis.clone();
990        self.state.last_analysis = Some(analysis_obj.clone());
991        self.state.last_candle = Some(new_candle);
992
993        analysis_obj
994    }
995
996    pub fn append_tick(&mut self, price: f64, time: u64) -> Option<AnalysisResult> {
997        let tick_minute = (time / 60) * 60;
998
999        if let Some(mut current) = self.current_candle {
1000            if time >= current.time + 60 {
1001                // Or robust check: tick_minute > current.time
1002                // Complete previous candle
1003                let completed_candle = current; // Copy
1004
1005                // Analyze it
1006                let result = self.append_candle(completed_candle);
1007
1008                // Start new candle
1009                self.current_candle = Some(Candle {
1010                    time: tick_minute,
1011                    open: price,
1012                    high: price,
1013                    low: price,
1014                    close: price,
1015                });
1016
1017                Some(result)
1018            } else {
1019                // Update current
1020                current.high = current.high.max(price);
1021                current.low = current.low.min(price);
1022                current.close = price;
1023                self.current_candle = Some(current);
1024                None
1025            }
1026        } else {
1027            // First tick ever seen (or after reset)
1028            self.current_candle = Some(Candle {
1029                time: tick_minute,
1030                open: price,
1031                high: price,
1032                low: price,
1033                close: price,
1034            });
1035            None
1036        }
1037    }
1038}