Skip to main content

shape_runtime/
fuzzy.rs

1//! Fuzzy matching algorithms for pattern recognition
2
3use std::f64;
4
5/// Configuration for fuzzy matching
6#[derive(Debug, Clone)]
7pub struct FuzzyConfig {
8    /// Default tolerance for fuzzy equality (as a percentage)
9    pub default_tolerance: f64,
10    /// Tolerance for price-based comparisons
11    pub price_tolerance: f64,
12    /// Tolerance for volume-based comparisons
13    pub volume_tolerance: f64,
14    /// Whether to use adaptive tolerance based on volatility
15    pub adaptive_tolerance: bool,
16}
17
18impl Default for FuzzyConfig {
19    fn default() -> Self {
20        Self {
21            default_tolerance: 0.02, // 2%
22            price_tolerance: 0.02,   // 2%
23            volume_tolerance: 0.05,  // 5%
24            adaptive_tolerance: true,
25        }
26    }
27}
28
29/// Fuzzy comparison functions
30#[derive(Debug, Clone)]
31pub struct FuzzyMatcher {
32    config: FuzzyConfig,
33}
34
35impl FuzzyMatcher {
36    /// Create a new fuzzy matcher with default configuration
37    pub fn new() -> Self {
38        Self {
39            config: FuzzyConfig::default(),
40        }
41    }
42
43    /// Create a fuzzy matcher with custom configuration
44    pub fn with_config(config: FuzzyConfig) -> Self {
45        Self { config }
46    }
47
48    /// Fuzzy equality comparison
49    pub fn fuzzy_equal(&self, a: f64, b: f64, tolerance: Option<f64>) -> bool {
50        let tol = tolerance.unwrap_or(self.config.default_tolerance);
51        let diff = (a - b).abs();
52        let avg = (a.abs() + b.abs()) / 2.0;
53
54        if avg == 0.0 {
55            diff == 0.0
56        } else {
57            diff / avg <= tol
58        }
59    }
60
61    /// Fuzzy greater than comparison
62    pub fn fuzzy_greater(&self, a: f64, b: f64, tolerance: Option<f64>) -> bool {
63        let tol = tolerance.unwrap_or(self.config.default_tolerance);
64        a > b * (1.0 - tol)
65    }
66
67    /// Fuzzy less than comparison
68    pub fn fuzzy_less(&self, a: f64, b: f64, tolerance: Option<f64>) -> bool {
69        let tol = tolerance.unwrap_or(self.config.default_tolerance);
70        a < b * (1.0 + tol)
71    }
72
73    /// Calculate adaptive tolerance based on volatility
74    pub fn adaptive_tolerance(&self, values: &[f64]) -> f64 {
75        if !self.config.adaptive_tolerance || values.len() < 2 {
76            return self.config.default_tolerance;
77        }
78
79        // Calculate standard deviation
80        let mean = values.iter().sum::<f64>() / values.len() as f64;
81        let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
82        let std_dev = variance.sqrt();
83
84        // Adjust tolerance based on volatility
85        let volatility = if mean != 0.0 {
86            std_dev / mean.abs()
87        } else {
88            0.0
89        };
90
91        // Scale tolerance: higher volatility = higher tolerance
92        let scaled_tolerance = self.config.default_tolerance * (1.0 + volatility).min(3.0);
93
94        scaled_tolerance.min(0.1) // Cap at 10%
95    }
96
97    /// Fuzzy pattern matching with weighted conditions
98    pub fn match_pattern(&self, conditions: &[(bool, f64)], threshold: f64) -> bool {
99        if conditions.is_empty() {
100            return false;
101        }
102
103        let total_weight: f64 = conditions.iter().map(|(_, w)| w).sum();
104        let matched_weight: f64 = conditions
105            .iter()
106            .filter(|(matched, _)| *matched)
107            .map(|(_, w)| w)
108            .sum();
109
110        if total_weight == 0.0 {
111            // If no weights, use simple majority
112            let matched_count = conditions.iter().filter(|(m, _)| *m).count();
113            matched_count as f64 / conditions.len() as f64 >= threshold
114        } else {
115            matched_weight / total_weight >= threshold
116        }
117    }
118
119    /// Calculate similarity score between two numeric sequences
120    pub fn sequence_similarity(&self, seq1: &[f64], seq2: &[f64]) -> f64 {
121        if seq1.is_empty() || seq2.is_empty() || seq1.len() != seq2.len() {
122            return 0.0;
123        }
124
125        // Normalize sequences
126        let norm1 = Self::normalize_sequence(seq1);
127        let norm2 = Self::normalize_sequence(seq2);
128
129        // Calculate correlation coefficient
130        let n = norm1.len() as f64;
131        let sum_xy: f64 = norm1.iter().zip(&norm2).map(|(x, y)| x * y).sum();
132        let sum_x: f64 = norm1.iter().sum();
133        let sum_y: f64 = norm2.iter().sum();
134        let sum_x2: f64 = norm1.iter().map(|x| x * x).sum();
135        let sum_y2: f64 = norm2.iter().map(|y| y * y).sum();
136
137        let numerator = n * sum_xy - sum_x * sum_y;
138        let denominator = ((n * sum_x2 - sum_x * sum_x) * (n * sum_y2 - sum_y * sum_y)).sqrt();
139
140        if denominator == 0.0 {
141            1.0 // Both sequences are constant
142        } else {
143            (numerator / denominator).abs() // Take absolute value for similarity
144        }
145    }
146
147    /// Normalize a sequence to have mean 0 and std dev 1
148    fn normalize_sequence(seq: &[f64]) -> Vec<f64> {
149        let mean = seq.iter().sum::<f64>() / seq.len() as f64;
150        let variance = seq.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / seq.len() as f64;
151        let std_dev = variance.sqrt();
152
153        if std_dev == 0.0 {
154            vec![0.0; seq.len()]
155        } else {
156            seq.iter().map(|v| (v - mean) / std_dev).collect()
157        }
158    }
159}
160
161impl Default for FuzzyMatcher {
162    fn default() -> Self {
163        Self::new()
164    }
165}