use crate::curves::{Curve, Point2D};
use crate::error::CurveError;
use crate::geometrics::GeometricObject;
use optionstratlib_core::model::decimal::{d_add, d_div, d_mul, d_sub};
use rust_decimal::Decimal;
use std::collections::BTreeSet;
use tracing::warn;
fn sampling_err(err: optionstratlib_core::error::DecimalError) -> CurveError {
CurveError::ConstructionError(err.to_string())
}
pub(crate) fn grid_coordinate(
value: Decimal,
start: Decimal,
end: Decimal,
is_last: bool,
) -> Decimal {
if is_last {
end
} else if start <= end {
value.min(end)
} else {
value.max(end)
}
}
pub fn create_linear_curve(
start: Decimal,
end: Decimal,
slope: Decimal,
) -> Result<Curve, CurveError> {
let steps = 10;
let op = "create_linear_curve";
let span = d_sub(end, start, op).map_err(sampling_err)?;
let step_size = d_div(span, Decimal::from(steps), op).map_err(sampling_err)?;
let mut points: Vec<Point2D> = Vec::new();
for i in 0..=steps {
let offset = d_mul(step_size, Decimal::from(i), op).map_err(sampling_err)?;
let x = grid_coordinate(
d_add(start, offset, op).map_err(sampling_err)?,
start,
end,
i == steps,
);
let y = d_mul(slope, x, op).map_err(sampling_err)?;
points.push(Point2D::new(x, y));
}
Ok(Curve::from_vector(points.iter().collect()))
}
pub fn create_constant_curve(
start: Decimal,
end: Decimal,
value: Decimal,
) -> Result<Curve, CurveError> {
let steps = 10;
let op = "create_constant_curve";
let span = d_sub(end, start, op).map_err(sampling_err)?;
let step_size = d_div(span, Decimal::from(steps), op).map_err(sampling_err)?;
let mut point_values: Vec<Point2D> = Vec::new();
for i in 0..=steps {
let offset = d_mul(step_size, Decimal::from(i), op).map_err(sampling_err)?;
let x = grid_coordinate(
d_add(start, offset, op).map_err(sampling_err)?,
start,
end,
i == steps,
);
point_values.push(Point2D::new(x, value));
}
let points: Vec<&Point2D> = point_values.iter().collect();
Ok(Curve::from_vector(points))
}
pub fn detect_peaks_and_valleys(
points: &BTreeSet<Point2D>,
min_prominence: Decimal,
window_size: usize,
) -> Result<(Vec<Point2D>, Vec<Point2D>), CurveError> {
let points_vec: Vec<Point2D> = points.iter().cloned().collect();
let mut peaks = Vec::new();
let mut valleys = Vec::new();
let needed = window_size
.checked_mul(2)
.and_then(|width| width.checked_add(1))
.ok_or_else(|| {
CurveError::ConstructionError(format!(
"peak window of {window_size} points on each side is too large"
))
})?;
if points_vec.len() < needed {
warn!(
"Not enough points to detect peaks and valleys with window size {}. Need at least {} points, but got {}.",
window_size,
needed,
points_vec.len()
);
return Ok((peaks, valleys));
}
let last = points_vec
.len()
.checked_sub(window_size)
.ok_or_else(|| window_error(window_size, points_vec.len()))?;
for i in window_size..last {
let current = window_point(&points_vec, Some(i))?;
let mut is_peak = true;
let mut is_valley = true;
for j in 1..=window_size {
let before = window_point(&points_vec, i.checked_sub(j))?;
let after = window_point(&points_vec, i.checked_add(j))?;
if current.y <= before.y || current.y <= after.y {
is_peak = false;
}
if current.y >= before.y || current.y >= after.y {
is_valley = false;
}
if !is_peak && !is_valley {
break;
}
}
if is_peak {
let prominence = calculate_prominence(&points_vec, i, true)?;
if prominence >= min_prominence {
peaks.push(*current);
}
} else if is_valley {
let prominence = calculate_prominence(&points_vec, i, false)?;
if prominence >= min_prominence {
valleys.push(*current);
}
}
}
Ok((peaks, valleys))
}
fn window_point(points: &[Point2D], index: Option<usize>) -> Result<&Point2D, CurveError> {
index
.and_then(|i| points.get(i))
.ok_or_else(|| window_error(index.unwrap_or(usize::MAX), points.len()))
}
#[cold]
#[inline(never)]
fn window_error(index: usize, len: usize) -> CurveError {
CurveError::ConstructionError(format!(
"peak window index {index} is outside the {len} points"
))
}
fn calculate_prominence(
points: &[Point2D],
index: usize,
is_peak: bool,
) -> Result<Decimal, CurveError> {
let current = window_point(points, Some(index))?.y;
let left_bound = if is_peak {
points
.iter()
.take(index)
.map(|p| p.y)
.min()
.unwrap_or(Decimal::MAX)
} else {
points
.iter()
.take(index)
.map(|p| p.y)
.max()
.unwrap_or(Decimal::MIN)
};
let right_bound = if is_peak {
points
.iter()
.skip(index)
.skip(1)
.map(|p| p.y)
.min()
.unwrap_or(Decimal::MAX)
} else {
points
.iter()
.skip(index)
.skip(1)
.map(|p| p.y)
.max()
.unwrap_or(Decimal::MIN)
};
let op = "curves::utils::calculate_prominence";
if is_peak {
d_sub(current, Decimal::max(left_bound, right_bound), op).map_err(sampling_err)
} else {
d_sub(Decimal::min(left_bound, right_bound), current, op).map_err(sampling_err)
}
}
#[cfg(test)]
mod tests_utils {
use crate::curves::Point2D;
use crate::curves::utils::{calculate_prominence, detect_peaks_and_valleys};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
#[test]
fn test_detect_peaks_and_valleys_insufficient_points() {
let points = BTreeSet::from_iter(vec![
Point2D::new(dec!(1.0), dec!(2.0)),
Point2D::new(dec!(2.0), dec!(3.0)),
]);
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(0.1), 1).unwrap();
assert!(peaks.is_empty());
assert!(valleys.is_empty());
}
#[test]
fn test_calculate_prominence() {
let points = vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(2.0)), Point2D::new(dec!(2.0), dec!(1.0)),
Point2D::new(dec!(3.0), dec!(-1.0)), Point2D::new(dec!(4.0), dec!(0.0)),
];
let peak_prominence = calculate_prominence(&points, 1, true).unwrap();
assert_eq!(peak_prominence, dec!(2.0));
let valley_prominence = calculate_prominence(&points, 3, false).unwrap();
assert_eq!(valley_prominence, dec!(1.0));
}
#[test]
fn test_detect_peaks_and_valleys_with_prominence() {
let points = BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(3.0)),
Point2D::new(dec!(2.0), dec!(-2.0)),
Point2D::new(dec!(3.0), dec!(2.0)),
Point2D::new(dec!(4.0), dec!(-1.0)),
Point2D::new(dec!(5.0), dec!(0.0)),
]);
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(0.1), 1).unwrap();
assert_eq!(peaks.len(), 2);
assert_eq!(valleys.len(), 2);
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(4.0), 1).unwrap();
assert!(peaks.is_empty());
assert!(!valleys.is_empty());
let (peaks, valleys) = detect_peaks_and_valleys(&points, dec!(2.0), 1).unwrap();
assert_eq!(peaks.len(), 2);
assert_eq!(valleys.len(), 1);
assert_eq!(peaks[0].y, dec!(3.0));
assert_eq!(valleys[0].y, dec!(-2.0));
}
#[test]
fn test_prominence_overflow_is_error() {
let points = vec![
Point2D::new(dec!(0.0), Decimal::MIN),
Point2D::new(dec!(1.0), Decimal::MAX),
Point2D::new(dec!(2.0), Decimal::MIN),
];
assert!(calculate_prominence(&points, 1, true).is_err());
assert!(calculate_prominence(&points, 3, true).is_err());
let set: BTreeSet<Point2D> = points.into_iter().collect();
assert!(detect_peaks_and_valleys(&set, dec!(0.1), 1).is_err());
}
#[test]
fn test_window_size_overflow_is_error() {
let points = BTreeSet::from_iter(vec![Point2D::new(dec!(1.0), dec!(2.0))]);
assert!(detect_peaks_and_valleys(&points, dec!(0.1), usize::MAX).is_err());
}
}
#[cfg(test)]
mod tests_generators_stay_inside_the_range {
use super::*;
use rust_decimal_macros::dec;
fn long_mantissa_ends() -> Vec<Decimal> {
vec![
dec!(10.00000000000000000000000002),
dec!(3.333333333333333333333333333),
dec!(7.777777777777777777777777777),
dec!(1.234567890123456789012345678),
dec!(9.999999999999999999999999999),
dec!(0.1234567890123456789012345678),
dec!(123.4567890123456789012345678),
]
}
fn assert_inside(curve: &Curve, start: Decimal, end: Decimal) {
let (lo, hi) = if start <= end {
(start, end)
} else {
(end, start)
};
for p in &curve.points {
assert!(p.x >= lo && p.x <= hi, "x = {} outside [{lo}, {hi}]", p.x);
}
}
#[test]
fn test_parametric_curve_stays_inside_the_range() {
use crate::geometrics::{ConstructionMethod, ConstructionParams, GeometricObject};
for end in long_mantissa_ends() {
for (t_start, t_end) in [(Decimal::ZERO, end), (end, Decimal::ZERO)] {
let params = ConstructionParams::D2 {
t_start,
t_end,
steps: 10,
};
let f = |t: Decimal| -> Result<Point2D, CurveError> { Ok(Point2D::new(t, t)) };
match Curve::construct(ConstructionMethod::Parametric {
f: Box::new(f),
params,
}) {
Ok(curve) => assert_inside(&curve, t_start, t_end),
Err(e) => panic!("parametric curve must build: {e:?}"),
}
}
}
}
#[test]
fn test_parametric_surface_stays_inside_the_range() {
use crate::error::SurfaceError;
use crate::geometrics::{ConstructionMethod, ConstructionParams, GeometricObject};
use crate::surfaces::{Point3D, Surface};
for end in long_mantissa_ends() {
let params = ConstructionParams::D3 {
x_start: Decimal::ZERO,
x_end: end,
y_start: end,
y_end: Decimal::ZERO,
x_steps: 10,
y_steps: 10,
};
let f = |p: Point2D| -> Result<Point3D, SurfaceError> {
Ok(Point3D::new(p.x, p.y, Decimal::ZERO))
};
match Surface::construct(ConstructionMethod::Parametric {
f: Box::new(f),
params,
}) {
Ok(surface) => {
for p in &surface.points {
assert!(p.x >= Decimal::ZERO && p.x <= end, "x = {} past {end}", p.x);
assert!(p.y >= Decimal::ZERO && p.y <= end, "y = {} past {end}", p.y);
}
assert!(surface.points.iter().any(|p| p.x == end), "no x at {end}");
assert!(
surface.points.iter().any(|p| p.y == Decimal::ZERO),
"no y at 0"
);
}
Err(e) => panic!("parametric surface must build: {e:?}"),
}
}
}
#[test]
fn test_grid_coordinate_pins_the_last_point_and_clamps_the_rest() {
let (start, end) = (Decimal::ZERO, dec!(1));
assert_eq!(grid_coordinate(dec!(0.5), start, end, false), dec!(0.5));
assert_eq!(
grid_coordinate(dec!(1.0000000000000000000000000002), start, end, false),
end
);
assert_eq!(
grid_coordinate(dec!(0.9999999999999999999999999998), start, end, true),
end
);
assert_eq!(
grid_coordinate(dec!(1.0000000000000000000000000002), start, end, true),
end
);
assert_eq!(
grid_coordinate(dec!(-0.0000000000000000000000000002), end, start, false),
start
);
assert_eq!(grid_coordinate(dec!(0.5), end, start, false), dec!(0.5));
}
#[test]
fn test_linear_and_constant_curves_stay_inside_the_range() {
for end in long_mantissa_ends() {
for (start, end) in [(Decimal::ZERO, end), (end, Decimal::ZERO)] {
match create_linear_curve(start, end, dec!(2)) {
Ok(curve) => assert_inside(&curve, start, end),
Err(e) => panic!("linear curve must build: {e:?}"),
}
match create_constant_curve(start, end, dec!(5)) {
Ok(curve) => assert_inside(&curve, start, end),
Err(e) => panic!("constant curve must build: {e:?}"),
}
}
}
}
}