finance_solution/stocks/ta/
obv.rs1use crate::stocks::ta::common::{
64 opt_cell, require_same_len, validate_positive_volume, validate_series,
65};
66use crate::util::error::{require_finite, FinanceError, FinanceResult};
67use crate::{columns_with_strings, print_table_locale_opt};
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
71pub struct ObvParams;
72
73impl ObvParams {
74 pub const fn default_pack() -> Self {
75 Self
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
81pub struct ValidatedObv {
82 params: ObvParams,
83}
84
85impl ValidatedObv {
86 pub fn new(params: ObvParams) -> FinanceResult<Self> {
87 Ok(Self { params })
88 }
89
90 pub fn params(self) -> ObvParams {
91 self.params
92 }
93
94 pub fn compute(self, closes: &[f64], volume: &[f64]) -> FinanceResult<ObvSeries> {
95 obv_validated(closes, volume, self)
96 }
97}
98
99#[derive(Clone, Debug, PartialEq)]
100pub struct ObvSeries {
101 pub obv: Vec<Option<f64>>,
102 pub params: ObvParams,
103}
104
105impl ObvSeries {
106 pub fn last(&self) -> Option<f64> {
107 self.obv.iter().rev().find_map(|x| *x)
108 }
109}
110
111#[derive(Clone, Debug)]
113pub struct ObvState {
114 params: ObvParams,
115 prev_close: Option<f64>,
116 obv: Option<f64>,
117 last: Option<f64>,
118}
119
120impl ObvState {
121 pub fn new(params: ObvParams) -> FinanceResult<Self> {
122 let _ = ValidatedObv::new(params)?;
123 Ok(Self {
124 params,
125 prev_close: None,
126 obv: None,
127 last: None,
128 })
129 }
130
131 pub fn from_history(params: ObvParams, closes: &[f64], volume: &[f64]) -> FinanceResult<Self> {
132 let mut s = Self::new(params)?;
133 let _ = s.push_bars(closes, volume)?;
134 Ok(s)
135 }
136
137 pub fn params(&self) -> ObvParams {
138 self.params
139 }
140
141 pub fn reset(&mut self) {
142 self.prev_close = None;
143 self.obv = None;
144 self.last = None;
145 }
146
147 pub fn push(&mut self, close: f64, volume: f64) -> FinanceResult<Option<f64>> {
148 require_finite("close", close)?;
149 require_finite("volume", volume)?;
150 if volume < 0.0 {
151 return Err(FinanceError::InvalidCashflow {
152 message: "volume must be non-negative",
153 });
154 }
155 let val = match self.prev_close {
156 None => volume,
157 Some(pc) => {
158 let mut o = self.obv.unwrap_or(0.0);
159 if close > pc {
160 o += volume;
161 } else if close < pc {
162 o -= volume;
163 }
164 o
165 }
166 };
167 self.prev_close = Some(close);
168 self.obv = Some(val);
169 self.last = Some(val);
170 Ok(Some(val))
171 }
172
173 pub fn push_bars(&mut self, closes: &[f64], volume: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
174 validate_series("close", closes)?;
175 validate_positive_volume(volume)?;
176 require_same_len(closes, volume, "close/volume")?;
177 let mut out = Vec::with_capacity(closes.len());
178 for i in 0..closes.len() {
179 out.push(self.push(closes[i], volume[i])?);
180 }
181 Ok(out)
182 }
183
184 pub fn last(&self) -> Option<f64> {
185 self.last
186 }
187}
188
189pub fn obv(closes: &[f64], volume: &[f64], params: ObvParams) -> FinanceResult<ObvSeries> {
190 ValidatedObv::new(params)?.compute(closes, volume)
191}
192
193fn obv_validated(closes: &[f64], volume: &[f64], eng: ValidatedObv) -> FinanceResult<ObvSeries> {
194 let mut st = ObvState::new(eng.params)?;
195 let obv = st.push_bars(closes, volume)?;
196 Ok(ObvSeries {
197 obv,
198 params: eng.params,
199 })
200}
201
202#[derive(Clone, Debug)]
203pub struct ObvSolution {
204 series: ObvSeries,
205 close: Vec<f64>,
206 volume: Vec<f64>,
207 formula: String,
208}
209
210impl ObvSolution {
211 pub fn series(&self) -> &ObvSeries {
212 &self.series
213 }
214 pub fn formula(&self) -> &str {
215 &self.formula
216 }
217
218 pub fn print_table(&self) {
219 self.print_table_locale_opt(None, None);
220 }
221
222 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
223 self.print_table_locale_opt(Some(locale), Some(precision));
224 }
225
226 fn print_table_locale_opt(
227 &self,
228 locale: Option<&num_format::Locale>,
229 precision: Option<usize>,
230 ) {
231 let columns = columns_with_strings(&[
232 ("period", "i", true),
233 ("close", "f", true),
234 ("volume", "f", true),
235 ("obv", "f", true),
236 ]);
237 let data = self
238 .close
239 .iter()
240 .enumerate()
241 .map(|(i, c)| {
242 vec![
243 i.to_string(),
244 c.to_string(),
245 self.volume[i].to_string(),
246 opt_cell(self.series.obv[i]),
247 ]
248 })
249 .collect();
250 print_table_locale_opt(&columns, data, locale, precision);
251 }
252}
253
254pub fn obv_solution(
263 closes: &[f64],
264 volume: &[f64],
265 params: ObvParams,
266) -> FinanceResult<ObvSolution> {
267 let series = obv(closes, volume, params)?;
268 Ok(ObvSolution {
269 series,
270 close: closes.to_vec(),
271 volume: volume.to_vec(),
272 formula: "OBV: +vol on up close, −vol on down close, flat otherwise; seed = vol[0]"
273 .to_string(),
274 })
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn word_problem() {
283 let c = [10.0, 11.0, 10.0];
284 let v = [100.0, 200.0, 50.0];
285 let s = obv(&c, &v, ObvParams::default_pack()).unwrap();
286 assert!((s.obv[0].unwrap() - 100.0).abs() < 1e-12);
287 assert!((s.obv[1].unwrap() - 300.0).abs() < 1e-12);
288 assert!((s.obv[2].unwrap() - 250.0).abs() < 1e-12);
289 }
290
291 #[test]
292 fn flat_close_no_volume_change() {
293 let c = [10.0, 10.0];
294 let v = [100.0, 999.0];
295 let s = obv(&c, &v, ObvParams::default_pack()).unwrap();
296 assert!((s.obv[1].unwrap() - 100.0).abs() < 1e-12);
297 }
298
299 #[test]
300 fn state_parity() {
301 let c: Vec<_> = (0..40).map(|i| 100.0 + (i as f64).sin()).collect();
302 let v: Vec<_> = (0..40).map(|i| 1000.0 + i as f64).collect();
303 let p = ObvParams::default_pack();
304 let batch = obv(&c, &v, p).unwrap();
305 let mut st = ObvState::new(p).unwrap();
306 for i in 0..c.len() {
307 let o = st.push(c[i], v[i]).unwrap();
308 assert!((o.unwrap() - batch.obv[i].unwrap()).abs() < 1e-12);
309 }
310 }
311
312 #[test]
313 fn negative_volume_err() {
314 assert!(obv(&[1.0], &[-1.0], ObvParams::default_pack()).is_err());
315 }
316}