Skip to main content

finance_query/indicators/
pivot_points.rs

1//! Pivot Points indicator (Standard and Fibonacci variants).
2//!
3//! Pivot points are classic intraday/swing support-resistance levels derived
4//! from the *previous* bar's high/low/close and held for the current bar —
5//! the same convention floor traders have used since well before charting
6//! software existed.
7
8use super::{IndicatorError, Result};
9use serde::{Deserialize, Serialize};
10
11/// Pivot point support/resistance levels for a single bar.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct PivotPoints {
15    /// Central pivot point: `(high + low + close) / 3`
16    pub pivot: f64,
17    /// First resistance level
18    pub r1: f64,
19    /// Second resistance level
20    pub r2: f64,
21    /// Third resistance level
22    pub r3: f64,
23    /// First support level
24    pub s1: f64,
25    /// Second support level
26    pub s2: f64,
27    /// Third support level
28    pub s3: f64,
29}
30
31fn validate(highs: &[f64], lows: &[f64], closes: &[f64]) -> Result<()> {
32    if highs.len() != lows.len() || highs.len() != closes.len() {
33        return Err(IndicatorError::InvalidPeriod(
34            "highs, lows, and closes must have the same length".to_string(),
35        ));
36    }
37    if highs.len() < 2 {
38        return Err(IndicatorError::InsufficientData {
39            need: 2,
40            got: highs.len(),
41        });
42    }
43    Ok(())
44}
45
46/// Calculate classic (standard) pivot points.
47///
48/// Each bar's levels are derived from the **previous** bar's high/low/close;
49/// the first bar has no prior bar and is therefore `None`.
50///
51/// # Formula
52///
53/// - Pivot = (High + Low + Close) / 3
54/// - R1 = 2×Pivot − Low, S1 = 2×Pivot − High
55/// - R2 = Pivot + (High − Low), S2 = Pivot − (High − Low)
56/// - R3 = High + 2×(Pivot − Low), S3 = Low − 2×(High − Pivot)
57///
58/// # Example
59///
60/// ```
61/// use finance_query::indicators::pivot_points;
62///
63/// let highs = vec![10.0, 12.0, 11.0];
64/// let lows = vec![8.0, 9.0, 8.5];
65/// let closes = vec![9.0, 11.0, 10.0];
66/// let result = pivot_points(&highs, &lows, &closes).unwrap();
67///
68/// assert!(result[0].is_none());
69/// assert!(result[1].is_some());
70/// ```
71pub fn pivot_points(
72    highs: &[f64],
73    lows: &[f64],
74    closes: &[f64],
75) -> Result<Vec<Option<PivotPoints>>> {
76    validate(highs, lows, closes)?;
77    let mut result = vec![None; highs.len()];
78    for i in 1..highs.len() {
79        let (h, l, c) = (highs[i - 1], lows[i - 1], closes[i - 1]);
80        let pivot = (h + l + c) / 3.0;
81        let range = h - l;
82        result[i] = Some(PivotPoints {
83            pivot,
84            r1: 2.0 * pivot - l,
85            s1: 2.0 * pivot - h,
86            r2: pivot + range,
87            s2: pivot - range,
88            r3: h + 2.0 * (pivot - l),
89            s3: l - 2.0 * (h - pivot),
90        });
91    }
92    Ok(result)
93}
94
95/// Calculate Fibonacci pivot points.
96///
97/// Uses the same central pivot as the standard variant, but Fibonacci
98/// retracement ratios (38.2%, 61.8%, 100%) of the previous bar's range for
99/// the support/resistance levels instead of the classic multiples.
100///
101/// # Formula
102///
103/// - Pivot = (High + Low + Close) / 3
104/// - R1/S1 = Pivot ± 0.382×Range, R2/S2 = Pivot ± 0.618×Range, R3/S3 = Pivot ± 1.000×Range
105///
106/// # Example
107///
108/// ```
109/// use finance_query::indicators::fibonacci_pivot_points;
110///
111/// let highs = vec![10.0, 12.0, 11.0];
112/// let lows = vec![8.0, 9.0, 8.5];
113/// let closes = vec![9.0, 11.0, 10.0];
114/// let result = fibonacci_pivot_points(&highs, &lows, &closes).unwrap();
115///
116/// assert!(result[0].is_none());
117/// assert!(result[1].is_some());
118/// ```
119pub fn fibonacci_pivot_points(
120    highs: &[f64],
121    lows: &[f64],
122    closes: &[f64],
123) -> Result<Vec<Option<PivotPoints>>> {
124    validate(highs, lows, closes)?;
125    let mut result = vec![None; highs.len()];
126    for i in 1..highs.len() {
127        let (h, l, c) = (highs[i - 1], lows[i - 1], closes[i - 1]);
128        let pivot = (h + l + c) / 3.0;
129        let range = h - l;
130        result[i] = Some(PivotPoints {
131            pivot,
132            r1: pivot + 0.382 * range,
133            r2: pivot + 0.618 * range,
134            r3: pivot + 1.000 * range,
135            s1: pivot - 0.382 * range,
136            s2: pivot - 0.618 * range,
137            s3: pivot - 1.000 * range,
138        });
139    }
140    Ok(result)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_pivot_points_basic() {
149        // Prior bar: H=12, L=8, C=10 -> pivot = 30/3 = 10.0
150        let highs = vec![12.0, 15.0];
151        let lows = vec![8.0, 9.0];
152        let closes = vec![10.0, 13.0];
153        let result = pivot_points(&highs, &lows, &closes).unwrap();
154
155        assert!(result[0].is_none());
156        let p = result[1].unwrap();
157        assert!((p.pivot - 10.0).abs() < 1e-9);
158        assert!((p.r1 - 12.0).abs() < 1e-9); // 2*10 - 8
159        assert!((p.s1 - 8.0).abs() < 1e-9); // 2*10 - 12
160        assert!((p.r2 - 14.0).abs() < 1e-9); // 10 + 4
161        assert!((p.s2 - 6.0).abs() < 1e-9); // 10 - 4
162        assert!(p.r3 > p.r2);
163        assert!(p.s3 < p.s2);
164    }
165
166    #[test]
167    fn test_fibonacci_pivot_points_basic() {
168        let highs = vec![12.0, 15.0];
169        let lows = vec![8.0, 9.0];
170        let closes = vec![10.0, 13.0];
171        let result = fibonacci_pivot_points(&highs, &lows, &closes).unwrap();
172
173        assert!(result[0].is_none());
174        let p = result[1].unwrap();
175        assert!((p.pivot - 10.0).abs() < 1e-9);
176        // range = 4.0
177        assert!((p.r1 - (10.0 + 0.382 * 4.0)).abs() < 1e-9);
178        assert!((p.s1 - (10.0 - 0.382 * 4.0)).abs() < 1e-9);
179        assert!((p.r3 - 14.0).abs() < 1e-9);
180        assert!((p.s3 - 6.0).abs() < 1e-9);
181    }
182
183    #[test]
184    fn test_pivot_points_insufficient_data() {
185        assert!(pivot_points(&[1.0], &[1.0], &[1.0]).is_err());
186    }
187
188    #[test]
189    fn test_pivot_points_mismatched_lengths() {
190        assert!(pivot_points(&[1.0, 2.0], &[1.0], &[1.0, 2.0]).is_err());
191    }
192}