fin_primitives/signals/indicators/
normalized_volume.rs1use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6use std::collections::VecDeque;
7
8pub struct NormalizedVolume {
25 name: String,
26 period: usize,
27 volumes: VecDeque<Decimal>,
28}
29
30impl NormalizedVolume {
31 pub fn new(name: impl Into<String>, period: usize) -> Result<Self, FinError> {
36 if period == 0 {
37 return Err(FinError::InvalidPeriod(period));
38 }
39 Ok(Self {
40 name: name.into(),
41 period,
42 volumes: VecDeque::with_capacity(period),
43 })
44 }
45}
46
47impl Signal for NormalizedVolume {
48 fn name(&self) -> &str {
49 &self.name
50 }
51
52 fn period(&self) -> usize {
53 self.period
54 }
55
56 fn is_ready(&self) -> bool {
57 self.volumes.len() >= self.period
58 }
59
60 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
61 self.volumes.push_back(bar.volume);
62 if self.volumes.len() > self.period {
63 self.volumes.pop_front();
64 }
65 if self.volumes.len() < self.period {
66 return Ok(SignalValue::Unavailable);
67 }
68
69 #[allow(clippy::cast_possible_truncation)]
70 let period_d = Decimal::from(self.period as u32);
71 let sum: Decimal = self.volumes.iter().copied().sum();
72 let avg = sum
73 .checked_div(period_d)
74 .ok_or(FinError::ArithmeticOverflow)?;
75
76 if avg.is_zero() {
77 return Ok(SignalValue::Unavailable);
78 }
79
80 let ratio = bar
81 .volume
82 .checked_div(avg)
83 .ok_or(FinError::ArithmeticOverflow)?;
84 Ok(SignalValue::Scalar(ratio))
85 }
86
87 fn reset(&mut self) {
88 self.volumes.clear();
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::ohlcv::OhlcvBar;
96 use crate::signals::Signal;
97 use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
98 use rust_decimal_macros::dec;
99
100 fn bar_with_vol(vol: &str) -> OhlcvBar {
101 let p = Price::new(dec!(100)).unwrap();
102 OhlcvBar {
103 symbol: Symbol::new("X").unwrap(),
104 open: p, high: p, low: p, close: p,
105 volume: Quantity::new(vol.parse().unwrap()).unwrap(),
106 ts_open: NanoTimestamp::new(0),
107 ts_close: NanoTimestamp::new(1),
108 tick_count: 1,
109 }
110 }
111
112 #[test]
113 fn test_nvol_invalid_period() {
114 assert!(NormalizedVolume::new("nv", 0).is_err());
115 }
116
117 #[test]
118 fn test_nvol_unavailable_before_period() {
119 let mut nv = NormalizedVolume::new("nv", 3).unwrap();
120 assert_eq!(nv.update_bar(&bar_with_vol("100")).unwrap(), SignalValue::Unavailable);
121 assert_eq!(nv.update_bar(&bar_with_vol("200")).unwrap(), SignalValue::Unavailable);
122 assert!(!nv.is_ready());
123 }
124
125 #[test]
126 fn test_nvol_equal_volumes_gives_one() {
127 let mut nv = NormalizedVolume::new("nv", 3).unwrap();
128 nv.update_bar(&bar_with_vol("100")).unwrap();
129 nv.update_bar(&bar_with_vol("100")).unwrap();
130 let v = nv.update_bar(&bar_with_vol("100")).unwrap();
131 assert_eq!(v, SignalValue::Scalar(dec!(1)));
132 }
133
134 #[test]
135 fn test_nvol_double_avg_gives_two() {
136 let mut nv = NormalizedVolume::new("nv", 3).unwrap();
137 nv.update_bar(&bar_with_vol("100")).unwrap();
140 nv.update_bar(&bar_with_vol("100")).unwrap();
141 nv.update_bar(&bar_with_vol("100")).unwrap();
142 let v = nv.update_bar(&bar_with_vol("200")).unwrap();
144 if let SignalValue::Scalar(ratio) = v {
145 assert!((ratio - dec!(1.5)).abs() < dec!(0.001), "expected 1.5, got {ratio}");
147 } else {
148 panic!("expected Scalar");
149 }
150 }
151
152 #[test]
153 fn test_nvol_zero_average_returns_unavailable() {
154 let mut nv = NormalizedVolume::new("nv", 2).unwrap();
155 nv.update_bar(&bar_with_vol("0")).unwrap();
156 let v = nv.update_bar(&bar_with_vol("0")).unwrap();
157 assert_eq!(v, SignalValue::Unavailable);
158 }
159
160 #[test]
161 fn test_nvol_reset() {
162 let mut nv = NormalizedVolume::new("nv", 2).unwrap();
163 nv.update_bar(&bar_with_vol("100")).unwrap();
164 nv.update_bar(&bar_with_vol("100")).unwrap();
165 assert!(nv.is_ready());
166 nv.reset();
167 assert!(!nv.is_ready());
168 }
169
170 #[test]
171 fn test_nvol_period_and_name() {
172 let nv = NormalizedVolume::new("my_nv", 20).unwrap();
173 assert_eq!(nv.period(), 20);
174 assert_eq!(nv.name(), "my_nv");
175 }
176}