1use crate::error::FinError;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Normalization {
23 None,
25 ZScore,
27 MinMax,
29}
30
31#[derive(Debug, Clone)]
35pub struct FeatureVector {
36 names: Vec<String>,
38 raw: Vec<f64>,
40 normalization: Normalization,
42}
43
44impl FeatureVector {
45 pub fn len(&self) -> usize {
47 self.names.len()
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.names.is_empty()
53 }
54
55 pub fn names(&self) -> &[String] {
57 &self.names
58 }
59
60 pub fn raw_values(&self) -> &[f64] {
62 &self.raw
63 }
64
65 pub fn normalization(&self) -> Normalization {
67 self.normalization
68 }
69
70 pub fn get_by_name(&self, name: &str) -> Option<f64> {
72 self.names.iter().position(|n| n == name).map(|i| self.raw[i])
73 }
74
75 pub fn to_normalized_vec(&self) -> Vec<f64> {
84 match self.normalization {
85 Normalization::None => self.raw.clone(),
86 Normalization::ZScore => zscore_normalize(&self.raw),
87 Normalization::MinMax => minmax_normalize(&self.raw),
88 }
89 }
90}
91
92impl From<FeatureVector> for Vec<f64> {
94 fn from(fv: FeatureVector) -> Vec<f64> {
95 fv.to_normalized_vec()
96 }
97}
98
99impl<const N: usize> From<&[f64; N]> for FeatureVector {
104 fn from(arr: &[f64; N]) -> Self {
105 let names: Vec<String> = (0..N).map(|i| format!("f{i}")).collect();
106 let raw: Vec<f64> = arr.iter().copied().collect();
107 Self { names, raw, normalization: Normalization::None }
108 }
109}
110
111#[derive(Debug, Default)]
128pub struct FeatureVectorBuilder {
129 names: Vec<String>,
130 raw: Vec<f64>,
131 normalization: Normalization,
132}
133
134impl Default for Normalization {
135 fn default() -> Self {
136 Normalization::None
137 }
138}
139
140impl FeatureVectorBuilder {
141 pub fn new(normalization: Normalization) -> Self {
143 Self { names: Vec::new(), raw: Vec::new(), normalization }
144 }
145
146 pub fn add(mut self, name: impl Into<String>, value: f64) -> Result<Self, FinError> {
153 let name = name.into();
154 if name.trim().is_empty() {
155 return Err(FinError::InvalidInput(
156 "feature name must not be empty or whitespace".to_owned(),
157 ));
158 }
159 if self.names.iter().any(|n| n == &name) {
160 return Err(FinError::InvalidInput(format!(
161 "duplicate feature name: '{name}'"
162 )));
163 }
164 self.names.push(name);
165 self.raw.push(value);
166 Ok(self)
167 }
168
169 pub fn add_all(
174 mut self,
175 features: &[(&str, f64)],
176 ) -> Result<Self, FinError> {
177 for (name, value) in features {
178 self = self.add(*name, *value)?;
179 }
180 Ok(self)
181 }
182
183 pub fn build(self) -> FeatureVector {
185 FeatureVector {
186 names: self.names,
187 raw: self.raw,
188 normalization: self.normalization,
189 }
190 }
191}
192
193fn zscore_normalize(values: &[f64]) -> Vec<f64> {
196 let n = values.len() as f64;
197 if n == 0.0 {
198 return Vec::new();
199 }
200 let mean = values.iter().sum::<f64>() / n;
201 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
202 let std_dev = variance.sqrt();
203 if std_dev == 0.0 {
204 return vec![0.0; values.len()];
205 }
206 values.iter().map(|v| (v - mean) / std_dev).collect()
207}
208
209fn minmax_normalize(values: &[f64]) -> Vec<f64> {
210 if values.is_empty() {
211 return Vec::new();
212 }
213 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
214 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
215 let range = max - min;
216 if range == 0.0 {
217 return vec![0.0; values.len()];
218 }
219 values.iter().map(|v| (v - min) / range).collect()
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_builder_basic() {
228 let fv = FeatureVectorBuilder::new(Normalization::None)
229 .add("rsi", 60.0)
230 .unwrap()
231 .add("adx", 30.0)
232 .unwrap()
233 .build();
234 assert_eq!(fv.len(), 2);
235 assert_eq!(fv.names(), &["rsi", "adx"]);
236 assert_eq!(fv.raw_values(), &[60.0, 30.0]);
237 }
238
239 #[test]
240 fn test_duplicate_name_rejected() {
241 let result = FeatureVectorBuilder::new(Normalization::None)
242 .add("rsi", 60.0)
243 .unwrap()
244 .add("rsi", 70.0);
245 assert!(matches!(result, Err(FinError::InvalidInput(_))));
246 }
247
248 #[test]
249 fn test_empty_name_rejected() {
250 let result = FeatureVectorBuilder::new(Normalization::None).add(" ", 1.0);
251 assert!(matches!(result, Err(FinError::InvalidInput(_))));
252 }
253
254 #[test]
255 fn test_zscore_normalization() {
256 let fv = FeatureVectorBuilder::new(Normalization::ZScore)
257 .add("a", 10.0)
258 .unwrap()
259 .add("b", 20.0)
260 .unwrap()
261 .add("c", 30.0)
262 .unwrap()
263 .build();
264 let norm: Vec<f64> = fv.into();
265 assert!((norm[1]).abs() < 1e-10, "middle value should be 0 after z-score");
267 assert!(norm[0] < 0.0);
268 assert!(norm[2] > 0.0);
269 }
270
271 #[test]
272 fn test_minmax_normalization() {
273 let fv = FeatureVectorBuilder::new(Normalization::MinMax)
274 .add("a", 0.0)
275 .unwrap()
276 .add("b", 50.0)
277 .unwrap()
278 .add("c", 100.0)
279 .unwrap()
280 .build();
281 let norm: Vec<f64> = fv.into();
282 assert!((norm[0] - 0.0).abs() < 1e-10);
283 assert!((norm[1] - 0.5).abs() < 1e-10);
284 assert!((norm[2] - 1.0).abs() < 1e-10);
285 }
286
287 #[test]
288 fn test_zero_variance_zscore() {
289 let fv = FeatureVectorBuilder::new(Normalization::ZScore)
290 .add("a", 5.0)
291 .unwrap()
292 .add("b", 5.0)
293 .unwrap()
294 .build();
295 let norm: Vec<f64> = fv.into();
296 assert_eq!(norm, vec![0.0, 0.0]);
297 }
298
299 #[test]
300 fn test_zero_variance_minmax() {
301 let fv = FeatureVectorBuilder::new(Normalization::MinMax)
302 .add("a", 7.0)
303 .unwrap()
304 .add("b", 7.0)
305 .unwrap()
306 .build();
307 let norm: Vec<f64> = fv.into();
308 assert_eq!(norm, vec![0.0, 0.0]);
309 }
310
311 #[test]
312 fn test_from_fixed_array() {
313 let arr = [1.0_f64, 2.0, 3.0];
314 let fv = FeatureVector::from(&arr);
315 assert_eq!(fv.len(), 3);
316 assert_eq!(fv.names(), &["f0", "f1", "f2"]);
317 assert_eq!(fv.raw_values(), &[1.0, 2.0, 3.0]);
318 }
319
320 #[test]
321 fn test_get_by_name() {
322 let fv = FeatureVectorBuilder::new(Normalization::None)
323 .add("vol", 0.25)
324 .unwrap()
325 .build();
326 assert_eq!(fv.get_by_name("vol"), Some(0.25));
327 assert_eq!(fv.get_by_name("missing"), None);
328 }
329
330 #[test]
331 fn test_add_all() {
332 let features = [("x", 1.0), ("y", 2.0), ("z", 3.0)];
333 let fv = FeatureVectorBuilder::new(Normalization::None)
334 .add_all(&features)
335 .unwrap()
336 .build();
337 assert_eq!(fv.len(), 3);
338 }
339
340 #[test]
341 fn test_into_vec_none_normalization() {
342 let fv = FeatureVectorBuilder::new(Normalization::None)
343 .add("a", 42.0)
344 .unwrap()
345 .build();
346 let v: Vec<f64> = fv.into();
347 assert_eq!(v, vec![42.0]);
348 }
349}