templar_proxy_oracle_kernel/proxy/circuit_breaker/rule/
windowed_change_delta.rs

1#[cfg(feature = "schemars")]
2use alloc::borrow::ToOwned;
3#[cfg(any(feature = "borsh", feature = "schemars"))]
4use alloc::string::ToString;
5use templar_primitives::Decimal;
6
7use crate::proxy::circuit_breaker::{
8    math::relative_sum_change_exceeds, CircuitBreakerRule, Observation, RingBuffer,
9};
10
11serialize! {
12    #[derive(Debug, Clone, PartialEq, Eq)]
13    /// Trips when equal-sized current and historical windows have different means.
14    pub struct WindowedChangeDelta {
15        /// Number of observations in each compared window.
16        pub window_len: u32,
17        /// Number of full windows to look back from the current window.
18        ///
19        /// This is an offset, not a scan count. A value of `1` compares the current
20        /// window to the immediately preceding window; a value of `2` skips one
21        /// full window and compares against the window before that.
22        pub lookback_windows: u32,
23        /// Maximum allowed relative difference between the two windows' means.
24        pub max_relative_mean_change: Decimal,
25    }
26}
27
28impl CircuitBreakerRule for WindowedChangeDelta {
29    fn should_trip(&self, history: &RingBuffer<Observation>) -> bool {
30        let window_len = self.window_len as usize;
31        let lookback_windows = self.lookback_windows as usize;
32        if window_len < 2 || lookback_windows == 0 {
33            return false;
34        }
35
36        let Some(current_start) = history.len().checked_sub(window_len) else {
37            return false;
38        };
39        let Some(lookback_offset) = lookback_windows.checked_mul(window_len) else {
40            return false;
41        };
42        let Some(previous_start) = current_start.checked_sub(lookback_offset) else {
43            return false;
44        };
45        let previous = history
46            .iter()
47            .skip(previous_start)
48            .take(window_len)
49            .map(|observation| &observation.price);
50        let current = history
51            .iter()
52            .skip(current_start)
53            .take(window_len)
54            .map(|observation| &observation.price);
55
56        relative_sum_change_exceeds(previous, current, self.max_relative_mean_change)
57    }
58}