optionstratlib_core/utils/
numeric.rs1use crate::error::DecimalError;
14use crate::model::decimal::d_ln_f64;
15use positive::Positive;
16use rust_decimal::Decimal;
17
18const TOLERANCE_F64: f64 = 1e-8;
21
22#[allow(dead_code)]
51#[must_use]
52pub fn approx_equal(a: f64, b: f64) -> bool {
53 (a - b).abs() < TOLERANCE_F64
54}
55
56pub fn calculate_log_returns(close_prices: &[Positive]) -> Result<Vec<Decimal>, DecimalError> {
87 if close_prices.len() < 2 {
88 return Ok(Vec::new());
89 }
90
91 let windows = close_prices.windows(2);
92 let mut log_returns = Vec::with_capacity(windows.len());
93
94 for window in windows {
95 let [previous_price, current_price] = window else {
96 continue;
99 };
100
101 let ratio = current_price.checked_div(previous_price).map_err(|_| {
107 DecimalError::arithmetic_error(
108 "utils::calculate_log_returns",
109 "price ratio is zero or not representable",
110 )
111 })?;
112 log_returns.push(d_ln_f64(ratio.to_dec(), "utils::calculate_log_returns")?);
115 }
116
117 Ok(log_returns)
118}
119
120#[cfg(test)]
121mod tests_approx_equal {
122 use super::*;
123
124 #[test]
125 fn test_approx_equal_exact_values() {
126 assert!(approx_equal(1.0, 1.0));
127 }
128
129 #[test]
130 fn test_approx_equal_within_tolerance() {
131 let a = 1.00000001;
132 let b = 1.0;
133 assert!(approx_equal(a, b));
134 }
135
136 #[test]
137 fn test_approx_equal_outside_tolerance() {
138 let a = 1.0001;
139 let b = 1.0;
140 assert!(!approx_equal(a, b));
141 }
142
143 #[test]
144 fn test_approx_equal_negative_values() {
145 let a = -1.00000001;
146 let b = -1.0;
147 assert!(approx_equal(a, b));
148 }
149
150 #[test]
151 fn test_approx_equal_large_values_within_tolerance() {
152 let a = 1000000.000000001;
153 let b = 1000000.0;
154 assert!(approx_equal(a, b));
155 }
156
157 #[test]
158 fn test_approx_equal_large_values_outside_tolerance() {
159 let a = 1000000.1;
160 let b = 1000000.0;
161 assert!(!approx_equal(a, b));
162 }
163
164 #[test]
165 fn test_approx_equal_zero() {
166 let a = 0.0;
167 let b = 0.0;
168 assert!(approx_equal(a, b));
169 }
170
171 #[test]
172 fn test_approx_equal_zero_with_small_value() {
173 let a = 0.000000001;
174 let b = 0.0;
175 assert!(approx_equal(a, b));
176 }
177
178 #[test]
179 fn test_approx_equal_zero_outside_tolerance() {
180 let a = 0.01;
181 let b = 0.0;
182 assert!(!approx_equal(a, b));
183 }
184}
185
186#[cfg(test)]
187mod tests_log_returns {
188 use super::*;
189
190 use approx::assert_relative_eq;
191 use num_traits::ToPrimitive;
192 use positive::pos_or_panic;
193
194 #[test]
195 fn test_empty_input() {
196 let prices: Vec<Positive> = Vec::new();
197 let result = calculate_log_returns(&prices);
198 assert!(result.is_ok());
199 assert_eq!(result.unwrap().len(), 0);
200 }
201
202 #[test]
203 fn test_single_input() {
204 let prices = vec![Positive::HUNDRED];
205 let result = calculate_log_returns(&prices);
206 assert!(result.is_ok());
207 assert_eq!(result.unwrap().len(), 0);
208 }
209
210 #[test]
211 fn test_basic_calculation() {
212 let prices = vec![
213 Positive::HUNDRED,
214 pos_or_panic!(110.0),
215 pos_or_panic!(105.0),
216 ];
217 let result = calculate_log_returns(&prices).unwrap();
218
219 assert_eq!(result.len(), 2);
220
221 assert_relative_eq!(
225 result[0].to_f64().unwrap_or(f64::NAN),
226 0.09531018,
227 epsilon = 0.00001
228 );
229 assert_relative_eq!(
230 result[1].to_f64().unwrap_or(f64::NAN),
231 -0.04652,
232 epsilon = 0.00001
233 );
234 }
235
236 #[test]
240 fn test_zero_price() {
241 let prices = vec![Positive::HUNDRED, Positive::ZERO, pos_or_panic!(105.0)];
242 assert!(calculate_log_returns(&prices).is_err());
243 }
244
245 #[test]
246 #[should_panic]
247 fn test_negative_price() {
248 let prices = vec![
249 Positive::HUNDRED,
250 pos_or_panic!(-50.0),
251 pos_or_panic!(105.0),
252 ];
253 let _ = calculate_log_returns(&prices);
254 }
255
256 #[test]
257 fn test_realistic_stock_prices() {
258 let prices = vec![
259 pos_or_panic!(150.25),
260 pos_or_panic!(151.50),
261 pos_or_panic!(149.75),
262 pos_or_panic!(152.25),
263 pos_or_panic!(153.00),
264 ];
265
266 let result = calculate_log_returns(&prices).unwrap();
267
268 assert_eq!(result.len(), 4);
269
270 assert_relative_eq!(
271 result[0].to_f64().unwrap_or(f64::NAN),
272 0.00829,
273 epsilon = 0.00001
274 );
275 assert_relative_eq!(
276 result[1].to_f64().unwrap_or(f64::NAN),
277 -0.01161,
278 epsilon = 0.00001
279 );
280 assert_relative_eq!(
281 result[2].to_f64().unwrap_or(f64::NAN),
282 0.01656,
283 epsilon = 0.00001
284 );
285 assert_relative_eq!(
286 result[3].to_f64().unwrap_or(f64::NAN),
287 0.00491,
288 epsilon = 0.00001
289 );
290 }
291
292 #[test]
293 fn test_large_price_movements() {
294 let prices = vec![
296 Positive::HUNDRED, pos_or_panic!(200.0), pos_or_panic!(50.0), pos_or_panic!(300.0), ];
301
302 let result = calculate_log_returns(&prices).unwrap();
303
304 assert_eq!(result.len(), 3);
305
306 assert_relative_eq!(
312 result[0].to_f64().unwrap_or(f64::NAN),
313 std::f64::consts::LN_2,
314 epsilon = 0.0001
315 );
316 assert_relative_eq!(
317 result[1].to_f64().unwrap_or(f64::NAN),
318 -1.3863,
319 epsilon = 0.0001
320 );
321 assert_relative_eq!(
322 result[2].to_f64().unwrap_or(f64::NAN),
323 1.7918,
324 epsilon = 0.0001
325 );
326 }
327
328 #[test]
329 fn test_no_change_prices() {
330 let prices = vec![Positive::HUNDRED, Positive::HUNDRED, Positive::HUNDRED];
332 let result = calculate_log_returns(&prices).unwrap();
333
334 assert_eq!(result.len(), 2);
335 assert_relative_eq!(
337 result[0].to_f64().unwrap_or(f64::NAN),
338 0.0,
339 epsilon = 0.00001
340 );
341 assert_relative_eq!(
342 result[1].to_f64().unwrap_or(f64::NAN),
343 0.0,
344 epsilon = 0.00001
345 );
346 }
347
348 #[test]
349 fn test_very_small_price_changes() {
350 let prices = vec![
352 pos_or_panic!(100.000),
353 pos_or_panic!(100.001),
354 pos_or_panic!(100.002),
355 ];
356
357 let result = calculate_log_returns(&prices).unwrap();
358
359 assert_eq!(result.len(), 2);
360
361 assert!(result[0].to_f64().unwrap_or(f64::NAN) > 0.0);
366 assert!(result[0].to_f64().unwrap_or(f64::NAN) < 0.0001);
367 assert!(result[1].to_f64().unwrap_or(f64::NAN) > 0.0);
368 assert!(result[1].to_f64().unwrap_or(f64::NAN) < 0.0001);
369 }
370}
371
372#[cfg(test)]
375mod tests_log_returns_f64 {
376 use super::*;
377 use rust_decimal_macros::dec;
378
379 #[test]
380 fn test_log_returns_within_the_owner_bound() {
381 let prices: Vec<Positive> = (0..500u32)
383 .map(|i| {
384 let k = Decimal::from((i * 7919) % 1009) - dec!(504);
385 Positive::new_decimal(dec!(100) + k * dec!(0.06)).expect("positive price")
386 })
387 .collect();
388 let fast = calculate_log_returns(&prices).expect("log returns");
389 let mut worst = Decimal::ZERO;
390 for (window, value) in prices.windows(2).zip(&fast) {
391 let [previous, current] = window else {
392 continue;
393 };
394 let exact = crate::model::decimal::d_ln(current.to_dec() / previous.to_dec(), "test")
395 .expect("series ln");
396 let error = if exact.is_zero() {
397 value.abs()
398 } else {
399 (*value - exact).abs() / exact.abs()
400 };
401 assert!(
402 error <= dec!(0.000000000001),
403 "{previous} -> {current}: {error}"
404 );
405 worst = worst.max(error);
406 }
407 assert!(worst < dec!(0.00000000000001), "worst {worst}");
408 }
409}