Skip to main content

finance_query/indicators/
fibonacci_retracement.rs

1//! Fibonacci Retracement indicator.
2//!
3//! Computes the standard Fibonacci retracement levels between the swing
4//! high and swing low of a rolling lookback window.
5
6use std::collections::VecDeque;
7
8use super::{IndicatorError, Result};
9use serde::{Deserialize, Serialize};
10
11/// Fibonacci retracement levels between a swing high and swing low.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct FibonacciLevels {
15    /// Swing high over the lookback window (0% retracement level)
16    pub swing_high: f64,
17    /// Swing low over the lookback window (100% retracement level)
18    pub swing_low: f64,
19    /// 23.6% retracement level
20    pub level_23_6: f64,
21    /// 38.2% retracement level
22    pub level_38_2: f64,
23    /// 50% retracement level
24    pub level_50: f64,
25    /// 61.8% retracement level
26    pub level_61_8: f64,
27    /// 78.6% retracement level
28    pub level_78_6: f64,
29}
30
31/// Calculate rolling Fibonacci retracement levels.
32///
33/// For each bar, the swing high/low are the highest high and lowest low over
34/// the trailing `period` bars (inclusive), and the standard retracement
35/// levels are interpolated between them (0% = swing high, 100% = swing low —
36/// the conventional orientation for retracing a preceding up-move).
37///
38/// # Arguments
39///
40/// * `highs` - High prices
41/// * `lows` - Low prices
42/// * `period` - Lookback window size (typically 50–100)
43///
44/// # Example
45///
46/// ```
47/// use finance_query::indicators::fibonacci_retracement;
48///
49/// let highs = vec![10.0, 12.0, 11.0, 9.0, 13.0];
50/// let lows = vec![8.0, 9.0, 8.5, 7.0, 10.0];
51/// let result = fibonacci_retracement(&highs, &lows, 3).unwrap();
52///
53/// assert!(result[0].is_none());
54/// assert!(result[2].is_some());
55/// ```
56pub fn fibonacci_retracement(
57    highs: &[f64],
58    lows: &[f64],
59    period: usize,
60) -> Result<Vec<Option<FibonacciLevels>>> {
61    if period == 0 {
62        return Err(IndicatorError::InvalidPeriod(
63            "Period must be greater than 0".to_string(),
64        ));
65    }
66    let len = highs.len();
67    if lows.len() != len {
68        return Err(IndicatorError::InvalidPeriod(
69            "highs and lows must have the same length".to_string(),
70        ));
71    }
72    if len < period {
73        return Err(IndicatorError::InsufficientData {
74            need: period,
75            got: len,
76        });
77    }
78
79    let mut result = vec![None; len];
80
81    // Monotonic deques for O(N) sliding window max/min (same technique as donchian_channels).
82    let mut max_deque: VecDeque<usize> = VecDeque::new();
83    let mut min_deque: VecDeque<usize> = VecDeque::new();
84
85    for i in 0..len {
86        while max_deque.front().is_some_and(|&j| j + period <= i) {
87            max_deque.pop_front();
88        }
89        while min_deque.front().is_some_and(|&j| j + period <= i) {
90            min_deque.pop_front();
91        }
92        while max_deque.back().is_some_and(|&j| highs[j] <= highs[i]) {
93            max_deque.pop_back();
94        }
95        while min_deque.back().is_some_and(|&j| lows[j] >= lows[i]) {
96            min_deque.pop_back();
97        }
98        max_deque.push_back(i);
99        min_deque.push_back(i);
100
101        if i + 1 >= period {
102            let swing_high = highs[*max_deque.front().unwrap()];
103            let swing_low = lows[*min_deque.front().unwrap()];
104            let range = swing_high - swing_low;
105            result[i] = Some(FibonacciLevels {
106                swing_high,
107                swing_low,
108                level_23_6: swing_high - range * 0.236,
109                level_38_2: swing_high - range * 0.382,
110                level_50: swing_high - range * 0.5,
111                level_61_8: swing_high - range * 0.618,
112                level_78_6: swing_high - range * 0.786,
113            });
114        }
115    }
116
117    Ok(result)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_fibonacci_retracement_basic() {
126        let highs = vec![10.0, 12.0, 11.0, 9.0, 13.0];
127        let lows = vec![8.0, 9.0, 8.5, 7.0, 10.0];
128        let result = fibonacci_retracement(&highs, &lows, 3).unwrap();
129
130        assert!(result[0].is_none());
131        assert!(result[1].is_none());
132        assert!(result[2].is_some());
133
134        // Window [10,12,11]/[8,9,8.5] -> swing_high=12, swing_low=8, range=4
135        let l = result[2].unwrap();
136        assert!((l.swing_high - 12.0).abs() < 1e-9);
137        assert!((l.swing_low - 8.0).abs() < 1e-9);
138        assert!((l.level_50 - 10.0).abs() < 1e-9);
139        assert!((l.level_23_6 - (12.0 - 4.0 * 0.236)).abs() < 1e-9);
140        assert!((l.level_61_8 - (12.0 - 4.0 * 0.618)).abs() < 1e-9);
141    }
142
143    #[test]
144    fn test_fibonacci_levels_ordering() {
145        let highs = vec![10.0, 12.0, 11.0, 9.0, 13.0];
146        let lows = vec![8.0, 9.0, 8.5, 7.0, 10.0];
147        let result = fibonacci_retracement(&highs, &lows, 3).unwrap();
148
149        for l in result.into_iter().flatten() {
150            assert!(l.swing_high >= l.level_23_6);
151            assert!(l.level_23_6 >= l.level_38_2);
152            assert!(l.level_38_2 >= l.level_50);
153            assert!(l.level_50 >= l.level_61_8);
154            assert!(l.level_61_8 >= l.level_78_6);
155            assert!(l.level_78_6 >= l.swing_low);
156        }
157    }
158
159    #[test]
160    fn test_fibonacci_retracement_insufficient_data() {
161        assert!(fibonacci_retracement(&[1.0, 2.0], &[1.0, 2.0], 5).is_err());
162    }
163
164    #[test]
165    fn test_fibonacci_retracement_invalid_period() {
166        assert!(fibonacci_retracement(&[1.0], &[1.0], 0).is_err());
167    }
168
169    #[test]
170    fn test_fibonacci_retracement_mismatched_lengths() {
171        assert!(fibonacci_retracement(&[1.0, 2.0], &[1.0], 1).is_err());
172    }
173}