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