use crate::curves::Point2D;
use crate::curves::traits::StatisticalCurve;
use crate::curves::utils::{detect_peaks_and_valleys, grid_coordinate};
use crate::error::{CurveError, InterpolationError, MetricsError};
use crate::geometrics::{
Arithmetic, AxisOperations, BasicMetrics, BiLinearInterpolation, ConstructionMethod,
ConstructionParams, CubicInterpolation, GeometricObject, GeometricTransformations, Interpolate,
InterpolationType, LinearInterpolation, MergeAxisInterpolate, MergeOperation, MetricsExtractor,
RangeMetrics, RiskMetrics, ShapeMetrics, SplineInterpolation, TrendMetrics,
coefficient_of_variation, population_std_dev, powu_checked,
};
use optionstratlib_core::error::DecimalError;
use optionstratlib_core::model::decimal::{
d_add, d_div, d_mul, d_product_iter, d_sqrt, d_sub, d_sum_iter,
};
use optionstratlib_core::utils::Len;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rayon::prelude::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, btree_set};
use std::fmt::{Display, Formatter};
use std::ops::{Bound, Index};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct Curve {
pub points: BTreeSet<Point2D>,
pub x_range: (Decimal, Decimal),
}
impl Display for Curve {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for point in self.points.iter() {
writeln!(f, "{point}")?;
}
Ok(())
}
}
impl Default for Curve {
fn default() -> Self {
Curve {
points: BTreeSet::new(),
x_range: (Decimal::ZERO, Decimal::ZERO),
}
}
}
impl Curve {
#[must_use]
pub fn new(points: BTreeSet<Point2D>) -> Self {
let x_range = Self::calculate_range(points.iter().map(|p| p.x));
Curve { points, x_range }
}
fn exact_point_at(&self, x: Decimal) -> Result<Option<Point2D>, InterpolationError> {
let mut at_x = self.points_at(x);
let Some(point) = at_x.next() else {
return Ok(None);
};
if at_x.next().is_some() {
return Err(InterpolationError::DegenerateInterval);
}
Ok(Some(*point))
}
fn points_at(&self, x: Decimal) -> btree_set::Range<'_, Point2D> {
self.points
.range(Point2D { x, y: Decimal::MIN }..=Point2D { x, y: Decimal::MAX })
}
fn point_after(&self, point: &Point2D) -> Option<&Point2D> {
self.points
.range((Bound::Excluded(*point), Bound::Unbounded))
.next()
}
fn point_before(&self, point: &Point2D) -> Option<&Point2D> {
self.points.range(..*point).next_back()
}
fn bracket(&self, x: Decimal) -> Result<(&Point2D, &Point2D), InterpolationError> {
if self.points.len() < 2 {
return Err(InterpolationError::EmptyData);
}
let (Some(first), Some(last)) = (self.points.first(), self.points.last()) else {
return Err(InterpolationError::EmptyData);
};
if x < first.x || x > last.x {
return Err(InterpolationError::OutOfRange {
target: x.to_string(),
});
}
let floor = Point2D { x, y: Decimal::MIN };
let right = self
.points
.range(floor..)
.next()
.ok_or(InterpolationError::DegenerateInterval)?;
match self.points.range(..floor).next_back() {
Some(left) => Ok((left, right)),
None => self
.point_after(right)
.map(|second| (right, second))
.ok_or(InterpolationError::DegenerateInterval),
}
}
fn bilinear_cell(&self, x: Decimal) -> Result<[&Point2D; 4], InterpolationError> {
let (near_left, near_right) = self.bracket(x)?;
let mut beyond = self
.points
.range((Bound::Excluded(*near_right), Bound::Unbounded));
let far = match (beyond.next(), beyond.next()) {
(Some(left), Some(right)) => Some((left, right)),
_ => {
let mut tail = self.points.iter().rev();
match (tail.next(), tail.next()) {
(Some(right), Some(left)) => Some((left, right)),
_ => None,
}
}
};
let (far_left, far_right) = far.ok_or_else(|| {
InterpolationError::Bilinear(format!(
"the far edge needs two points, the curve has {}",
self.points.len()
))
})?;
Ok([near_left, near_right, far_left, far_right])
}
fn cubic_window(&self, x: Decimal) -> Result<[&Point2D; 4], InterpolationError> {
let (left, right) = self.bracket(x)?;
let window = match (self.point_before(left), self.point_after(right)) {
(None, _) => {
let mut head = self.points.iter();
(head.next(), head.next(), head.next(), head.next())
}
(Some(_), None) => {
let mut tail = self.points.iter().rev();
let (p3, p2, p1, p0) = (tail.next(), tail.next(), tail.next(), tail.next());
(p0, p1, p2, p3)
}
(Some(before), Some(after)) => (Some(before), Some(left), Some(right), Some(after)),
};
match window {
(Some(p0), Some(p1), Some(p2), Some(p3)) => Ok([p0, p1, p2, p3]),
_ => Err(InterpolationError::Cubic(format!(
"the window needs four points, the curve has {}",
self.points.len()
))),
}
}
fn spline_precheck(&self, x: Decimal) -> Result<Option<Point2D>, InterpolationError> {
let len = self.points.len();
if len < 3 {
return Err(InterpolationError::Spline(
"Need at least three points for spline interpolation".to_string(),
));
}
let (Some(first), Some(last)) = (self.points.first(), self.points.last()) else {
return Err(InterpolationError::Spline(format!(
"point index 0 is out of bounds for a curve of {len} points"
)));
};
if x < first.x || x > last.x {
return Err(InterpolationError::Spline(
"x is outside the range of points".to_string(),
));
}
self.exact_point_at(x)
}
}
#[derive(Debug, Clone)]
pub struct CurveSpline<'a> {
curve: &'a Curve,
knots: Vec<&'a Point2D>,
moments: Option<Vec<Decimal>>,
}
impl<'a> CurveSpline<'a> {
#[must_use]
pub fn new(curve: &'a Curve) -> Self {
let knots: Vec<&'a Point2D> = curve.points.iter().collect();
let moments = if knots.len() < 3 {
None
} else {
spline_moments(&knots).ok()
};
CurveSpline {
curve,
knots,
moments,
}
}
pub fn interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
if let Some(point) = self.curve.spline_precheck(x)? {
return Ok(point);
}
match &self.moments {
Some(moments) => spline_eval(&self.knots, moments, x),
None => self.unsolvable(),
}
}
#[cold]
fn unsolvable(&self) -> Result<Point2D, InterpolationError> {
spline_moments(&self.knots).and_then(|_| {
Err(InterpolationError::Spline(
"the spline system solved on retry after failing once".to_string(),
))
})
}
}
fn construction_err(err: DecimalError) -> CurveError {
CurveError::ConstructionError(err.to_string())
}
fn analysis_err(err: DecimalError) -> CurveError {
CurveError::AnalysisError(err.to_string())
}
fn interp_err(
kind: fn(String) -> InterpolationError,
) -> impl Fn(DecimalError) -> InterpolationError {
move |err| kind(err.to_string())
}
fn third_quartile_index(len: usize) -> Result<usize, MetricsError> {
len.checked_mul(3)
.map(|three_len| three_len / 4)
.ok_or_else(|| {
MetricsError::BasicError(format!("third quartile index of {len} samples overflowed"))
})
}
fn sample_at(
values: &[Decimal],
index: usize,
what: &'static str,
) -> Result<Decimal, MetricsError> {
values.get(index).copied().ok_or_else(|| {
MetricsError::BasicError(format!(
"{what}: sample {index} is out of bounds for {} values",
values.len()
))
})
}
fn mean_of(values: &[Decimal], op: &'static str) -> Result<Decimal, DecimalError> {
let sum = d_sum_iter(values.iter().copied(), op)?;
d_div(sum, Decimal::from(values.len()), op)
}
fn variance_of(
values: &[Decimal],
mean: Decimal,
op: &'static str,
) -> Result<Decimal, DecimalError> {
let mut acc = Decimal::ZERO;
for &value in values {
let centered = d_sub(value, mean, op)?;
let squared = powu_checked(centered, 2, op)?;
acc = d_add(acc, squared, op)?;
}
d_div(acc, Decimal::from(values.len()), op)
}
fn standardized_moment(
centered: &[Decimal],
std_dev: Decimal,
order: u64,
op: &'static str,
) -> Result<Decimal, DecimalError> {
let mut acc = Decimal::ZERO;
for &value in centered {
let z = d_div(value, std_dev, op)?;
let moment = powu_checked(z, order, op)?;
acc = d_add(acc, moment, op)?;
}
d_div(acc, Decimal::from(centered.len()), op)
}
fn shift_index(
index: usize,
by: isize,
kind: fn(String) -> InterpolationError,
) -> Result<usize, InterpolationError> {
index.checked_add_signed(by).ok_or_else(|| {
kind(format!(
"index {index} shifted by {by} leaves the usize range"
))
})
}
fn band_at(
band: &[Decimal],
index: usize,
name: &'static str,
) -> Result<Decimal, InterpolationError> {
band.get(index).copied().ok_or_else(|| {
InterpolationError::Spline(format!(
"spline band `{name}` has no slot {index} (len {})",
band.len()
))
})
}
fn band_at_mut<'a>(
band: &'a mut [Decimal],
index: usize,
name: &'static str,
) -> Result<&'a mut Decimal, InterpolationError> {
let len = band.len();
band.get_mut(index).ok_or_else(|| {
InterpolationError::Spline(format!(
"spline band `{name}` has no slot {index} (len {len})"
))
})
}
fn cube_scaled(
moment: Decimal,
d: Decimal,
six_h: Decimal,
op: &'static str,
) -> Result<Decimal, InterpolationError> {
let scaled = d_mul(moment, d, op).map_err(interp_err(InterpolationError::Spline))?;
let scaled = d_mul(scaled, d, op).map_err(interp_err(InterpolationError::Spline))?;
let scaled = d_mul(scaled, d, op).map_err(interp_err(InterpolationError::Spline))?;
d_div(scaled, six_h, op).map_err(interp_err(InterpolationError::Spline))
}
fn linear_term(
y: Decimal,
moment: Decimal,
h: Decimal,
d: Decimal,
op: &'static str,
) -> Result<Decimal, InterpolationError> {
let level = d_div(y, h, op).map_err(interp_err(InterpolationError::Spline))?;
let bend = d_mul(moment, h, op).map_err(interp_err(InterpolationError::Spline))?;
let bend = d_div(bend, dec!(6), op).map_err(interp_err(InterpolationError::Spline))?;
let slope = d_sub(level, bend, op).map_err(interp_err(InterpolationError::Spline))?;
d_mul(slope, d, op).map_err(interp_err(InterpolationError::Spline))
}
impl Len for Curve {
fn len(&self) -> usize {
self.points.len()
}
fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
impl GeometricObject<Point2D, Decimal> for Curve {
type Error = CurveError;
fn get_points(&self) -> BTreeSet<&Point2D> {
self.points.iter().collect()
}
fn from_vector<T>(points: Vec<T>) -> Self
where
T: Into<Point2D> + Clone,
{
let points: BTreeSet<Point2D> = points.into_iter().map(|p| p.into()).collect();
let x_range = Self::calculate_range(points.iter().map(|p| p.x));
Curve { points, x_range }
}
fn construct<T>(method: T) -> Result<Self, Self::Error>
where
Self: Sized,
T: Into<ConstructionMethod<Point2D, Decimal, CurveError>>,
{
let method = method.into();
match method {
ConstructionMethod::FromData { points } => {
if points.is_empty() {
return Err(CurveError::Point2DError {
reason: "Empty points array",
});
}
Ok(Curve::new(points))
}
ConstructionMethod::Parametric { f, params } => {
let (t_start, t_end, steps) = match params {
ConstructionParams::D2 {
t_start,
t_end,
steps,
} => (t_start, t_end, steps),
_ => {
return Err(CurveError::ConstructionError(
"Invalid parameters".to_string(),
));
}
};
if steps == 0 {
return Err(CurveError::ConstructionError(
"Parametric construction needs at least one step".to_string(),
));
}
let op = "Curve::construct::step_size";
let span = d_sub(t_end, t_start, op).map_err(construction_err)?;
let step_size = d_div(span, Decimal::from(steps), op).map_err(construction_err)?;
let points: Result<BTreeSet<Point2D>, CurveError> = (0..=steps)
.into_par_iter()
.map(|i| {
let offset = d_mul(step_size, Decimal::from(i), "Curve::construct::offset")
.map_err(construction_err)?;
let t = grid_coordinate(
d_add(t_start, offset, "Curve::construct::t")
.map_err(construction_err)?,
t_start,
t_end,
i == steps,
);
f(t)
})
.collect();
points.map(Curve::new)
}
}
}
}
impl Index<usize> for Curve {
type Output = Point2D;
fn index(&self, index: usize) -> &Self::Output {
match self.points.iter().nth(index) {
Some(p) => p,
None => {
let len = self.points.len();
panic!("Curve::index: out of bounds (index = {index}, len = {len})") }
}
}
}
impl Interpolate<Point2D, Decimal> for Curve {
fn find_bracket_points(&self, x: Decimal) -> Result<(usize, usize), InterpolationError> {
let (left, _) = self.bracket(x)?;
let i = self.points.range(..*left).count();
i.checked_add(1)
.map(|j| (i, j))
.ok_or(InterpolationError::DegenerateInterval)
}
}
impl LinearInterpolation<Point2D, Decimal> for Curve {
fn linear_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
if let Some(point) = self.exact_point_at(x)? {
return Ok(point);
}
let (p1, p2) = self.bracket(x)?;
let run = d_sub(p2.x, p1.x, "Curve::linear_interpolate::run")
.map_err(interp_err(InterpolationError::Linear))?;
if run.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let dx = d_sub(x, p1.x, "Curve::linear_interpolate::dx")
.map_err(interp_err(InterpolationError::Linear))?;
let rise = d_sub(p2.y, p1.y, "Curve::linear_interpolate::rise")
.map_err(interp_err(InterpolationError::Linear))?;
let scaled = d_mul(dx, rise, "Curve::linear_interpolate::scaled")
.map_err(interp_err(InterpolationError::Linear))?;
let ratio = d_div(scaled, run, "Curve::linear_interpolate::ratio")
.map_err(interp_err(InterpolationError::Linear))?;
let y = d_add(p1.y, ratio, "Curve::linear_interpolate::y")
.map_err(interp_err(InterpolationError::Linear))?;
Ok(Point2D::new(x, y))
}
}
impl BiLinearInterpolation<Point2D, Decimal> for Curve {
fn bilinear_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
let len = self.len();
if len < 4 {
return Err(InterpolationError::Bilinear(
"Need at least four points for bilinear interpolation".to_string(),
));
}
if let Some(point) = self.exact_point_at(x)? {
return Ok(point);
}
let [p11, p12, p21, p22] = self.bilinear_cell(x)?;
let span = d_sub(p12.x, p11.x, "Curve::bilinear_interpolate::span")
.map_err(interp_err(InterpolationError::Bilinear))?;
if span.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let offset = d_sub(x, p11.x, "Curve::bilinear_interpolate::offset")
.map_err(interp_err(InterpolationError::Bilinear))?;
let dx = d_div(offset, span, "Curve::bilinear_interpolate::dx")
.map_err(interp_err(InterpolationError::Bilinear))?;
let bottom_rise = d_sub(p12.y, p11.y, "Curve::bilinear_interpolate::bottom_rise")
.map_err(interp_err(InterpolationError::Bilinear))?;
let bottom_step = d_mul(dx, bottom_rise, "Curve::bilinear_interpolate::bottom_step")
.map_err(interp_err(InterpolationError::Bilinear))?;
let bottom = d_add(p11.y, bottom_step, "Curve::bilinear_interpolate::bottom")
.map_err(interp_err(InterpolationError::Bilinear))?;
let top_rise = d_sub(p22.y, p21.y, "Curve::bilinear_interpolate::top_rise")
.map_err(interp_err(InterpolationError::Bilinear))?;
let top_step = d_mul(dx, top_rise, "Curve::bilinear_interpolate::top_step")
.map_err(interp_err(InterpolationError::Bilinear))?;
let top = d_add(p21.y, top_step, "Curve::bilinear_interpolate::top")
.map_err(interp_err(InterpolationError::Bilinear))?;
let edge_gap = d_sub(top, bottom, "Curve::bilinear_interpolate::edge_gap")
.map_err(interp_err(InterpolationError::Bilinear))?;
let half_gap = d_div(edge_gap, dec!(2), "Curve::bilinear_interpolate::half_gap")
.map_err(interp_err(InterpolationError::Bilinear))?;
let y = d_add(bottom, half_gap, "Curve::bilinear_interpolate::y")
.map_err(interp_err(InterpolationError::Bilinear))?;
Ok(Point2D::new(x, y))
}
}
impl CubicInterpolation<Point2D, Decimal> for Curve {
fn cubic_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
let len = self.len();
if len < 4 {
return Err(InterpolationError::Cubic(
"Need at least four points for cubic interpolation".to_string(),
));
}
if let Some(point) = self.exact_point_at(x)? {
return Ok(point);
}
let [p0, p1, p2, p3] = self.cubic_window(x)?;
let span = d_sub(p2.x, p1.x, "Curve::cubic_interpolate::span")
.map_err(interp_err(InterpolationError::Cubic))?;
if span.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let offset = d_sub(x, p1.x, "Curve::cubic_interpolate::offset")
.map_err(interp_err(InterpolationError::Cubic))?;
let t = d_div(offset, span, "Curve::cubic_interpolate::t")
.map_err(interp_err(InterpolationError::Cubic))?;
let t2 = d_mul(t, t, "Curve::cubic_interpolate::t2")
.map_err(interp_err(InterpolationError::Cubic))?;
let t3 = d_mul(t2, t, "Curve::cubic_interpolate::t3")
.map_err(interp_err(InterpolationError::Cubic))?;
let term0 = d_mul(dec!(2), p1.y, "Curve::cubic_interpolate::term0")
.map_err(interp_err(InterpolationError::Cubic))?;
let linear_coeff = d_sub(p2.y, p0.y, "Curve::cubic_interpolate::linear_coeff")
.map_err(interp_err(InterpolationError::Cubic))?;
let term1 = d_mul(linear_coeff, t, "Curve::cubic_interpolate::term1")
.map_err(interp_err(InterpolationError::Cubic))?;
let two_p0 = d_mul(dec!(2), p0.y, "Curve::cubic_interpolate::two_p0")
.map_err(interp_err(InterpolationError::Cubic))?;
let five_p1 = d_mul(dec!(5), p1.y, "Curve::cubic_interpolate::five_p1")
.map_err(interp_err(InterpolationError::Cubic))?;
let four_p2 = d_mul(dec!(4), p2.y, "Curve::cubic_interpolate::four_p2")
.map_err(interp_err(InterpolationError::Cubic))?;
let quad_coeff = d_sub(two_p0, five_p1, "Curve::cubic_interpolate::quad_coeff_a")
.map_err(interp_err(InterpolationError::Cubic))?;
let quad_coeff = d_add(
quad_coeff,
four_p2,
"Curve::cubic_interpolate::quad_coeff_b",
)
.map_err(interp_err(InterpolationError::Cubic))?;
let quad_coeff = d_sub(quad_coeff, p3.y, "Curve::cubic_interpolate::quad_coeff_c")
.map_err(interp_err(InterpolationError::Cubic))?;
let term2 = d_mul(quad_coeff, t2, "Curve::cubic_interpolate::term2")
.map_err(interp_err(InterpolationError::Cubic))?;
let three_p1 = d_mul(dec!(3), p1.y, "Curve::cubic_interpolate::three_p1")
.map_err(interp_err(InterpolationError::Cubic))?;
let three_p2 = d_mul(dec!(3), p2.y, "Curve::cubic_interpolate::three_p2")
.map_err(interp_err(InterpolationError::Cubic))?;
let cubic_coeff = d_sub(three_p1, p0.y, "Curve::cubic_interpolate::cubic_coeff_a")
.map_err(interp_err(InterpolationError::Cubic))?;
let cubic_coeff = d_sub(
cubic_coeff,
three_p2,
"Curve::cubic_interpolate::cubic_coeff_b",
)
.map_err(interp_err(InterpolationError::Cubic))?;
let cubic_coeff = d_add(cubic_coeff, p3.y, "Curve::cubic_interpolate::cubic_coeff_c")
.map_err(interp_err(InterpolationError::Cubic))?;
let term3 = d_mul(cubic_coeff, t3, "Curve::cubic_interpolate::term3")
.map_err(interp_err(InterpolationError::Cubic))?;
let sum = d_sum_iter(
[term0, term1, term2, term3],
"Curve::cubic_interpolate::sum",
)
.map_err(interp_err(InterpolationError::Cubic))?;
let y = d_mul(dec!(0.5), sum, "Curve::cubic_interpolate::y")
.map_err(interp_err(InterpolationError::Cubic))?;
Ok(Point2D::new(x, y))
}
}
impl SplineInterpolation<Point2D, Decimal> for Curve {
fn spline_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
if let Some(point) = self.spline_precheck(x)? {
return Ok(point);
}
let pts: Vec<&Point2D> = self.points.iter().collect();
let m = spline_moments(&pts)?;
spline_eval(&pts, &m, x)
}
}
fn spline_moments(pts: &[&Point2D]) -> Result<Vec<Decimal>, InterpolationError> {
let n = pts.len();
let spline = InterpolationError::Spline;
let last_index = shift_index(n, -1, spline)?;
let at = |index: usize| -> Result<&Point2D, InterpolationError> {
pts.get(index).copied().ok_or_else(|| {
InterpolationError::Spline(format!(
"point index {index} is out of bounds for a curve of {n} points"
))
})
};
let mut a = vec![Decimal::ZERO];
let mut b = vec![Decimal::ONE];
let mut c = vec![Decimal::ZERO];
let mut r = vec![Decimal::ZERO];
for i in 1..last_index {
let prev = at(shift_index(i, -1, spline)?)?;
let curr = at(i)?;
let next = at(shift_index(i, 1, spline)?)?;
let hi = d_sub(curr.x, prev.x, "Curve::spline_interpolate::hi")
.map_err(interp_err(InterpolationError::Spline))?;
let hi1 = d_sub(next.x, curr.x, "Curve::spline_interpolate::hi1")
.map_err(interp_err(InterpolationError::Spline))?;
if hi.is_zero() || hi1.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let band = d_add(hi, hi1, "Curve::spline_interpolate::band")
.map_err(interp_err(InterpolationError::Spline))?;
let diag = d_mul(dec!(2), band, "Curve::spline_interpolate::diag")
.map_err(interp_err(InterpolationError::Spline))?;
let rise_next = d_sub(next.y, curr.y, "Curve::spline_interpolate::rise_next")
.map_err(interp_err(InterpolationError::Spline))?;
let slope_next = d_div(rise_next, hi1, "Curve::spline_interpolate::slope_next")
.map_err(interp_err(InterpolationError::Spline))?;
let rise_prev = d_sub(curr.y, prev.y, "Curve::spline_interpolate::rise_prev")
.map_err(interp_err(InterpolationError::Spline))?;
let slope_prev = d_div(rise_prev, hi, "Curve::spline_interpolate::slope_prev")
.map_err(interp_err(InterpolationError::Spline))?;
let curvature = d_sub(
slope_next,
slope_prev,
"Curve::spline_interpolate::curvature",
)
.map_err(interp_err(InterpolationError::Spline))?;
let rhs = d_mul(dec!(6), curvature, "Curve::spline_interpolate::rhs")
.map_err(interp_err(InterpolationError::Spline))?;
a.push(hi);
b.push(diag);
c.push(hi1);
r.push(rhs);
}
a.push(Decimal::ZERO);
b.push(Decimal::ONE);
c.push(Decimal::ZERO);
r.push(Decimal::ZERO);
let mut m = vec![Decimal::ZERO; n];
for i in 1..last_index {
let prev = shift_index(i, -1, spline)?;
let a_i = band_at(&a, i, "a")?;
let b_prev = band_at(&b, prev, "b")?;
if b_prev.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let w = d_div(a_i, b_prev, "Curve::spline_interpolate::w")
.map_err(interp_err(InterpolationError::Spline))?;
let c_prev = band_at(&c, prev, "c")?;
let wc = d_mul(w, c_prev, "Curve::spline_interpolate::wc")
.map_err(interp_err(InterpolationError::Spline))?;
let b_i = band_at(&b, i, "b")?;
*band_at_mut(&mut b, i, "b")? = d_sub(b_i, wc, "Curve::spline_interpolate::b_i")
.map_err(interp_err(InterpolationError::Spline))?;
let r_prev = band_at(&r, prev, "r")?;
let wr = d_mul(w, r_prev, "Curve::spline_interpolate::wr")
.map_err(interp_err(InterpolationError::Spline))?;
let r_i = band_at(&r, i, "r")?;
*band_at_mut(&mut r, i, "r")? = d_sub(r_i, wr, "Curve::spline_interpolate::r_i")
.map_err(interp_err(InterpolationError::Spline))?;
}
let b_last = band_at(&b, last_index, "b")?;
if b_last.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let r_last = band_at(&r, last_index, "r")?;
*band_at_mut(&mut m, last_index, "m")? =
d_div(r_last, b_last, "Curve::spline_interpolate::m_last")
.map_err(interp_err(InterpolationError::Spline))?;
for i in (1..last_index).rev() {
let c_i = band_at(&c, i, "c")?;
let m_next = band_at(&m, shift_index(i, 1, spline)?, "m")?;
let cm = d_mul(c_i, m_next, "Curve::spline_interpolate::cm")
.map_err(interp_err(InterpolationError::Spline))?;
let r_i = band_at(&r, i, "r")?;
let numerator = d_sub(r_i, cm, "Curve::spline_interpolate::m_numerator")
.map_err(interp_err(InterpolationError::Spline))?;
let b_i = band_at(&b, i, "b")?;
if b_i.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
*band_at_mut(&mut m, i, "m")? = d_div(numerator, b_i, "Curve::spline_interpolate::m_i")
.map_err(interp_err(InterpolationError::Spline))?;
}
Ok(m)
}
fn spline_eval(pts: &[&Point2D], m: &[Decimal], x: Decimal) -> Result<Point2D, InterpolationError> {
let n = pts.len();
let spline = InterpolationError::Spline;
let at = |index: usize| -> Result<&Point2D, InterpolationError> {
pts.get(index).copied().ok_or_else(|| {
InterpolationError::Spline(format!(
"point index {index} is out of bounds for a curve of {n} points"
))
})
};
let first_at_or_after = pts.partition_point(|p| p.x < x);
let segment = shift_index(first_at_or_after.max(1), -1, spline)?;
if segment >= shift_index(n, -1, spline)? {
return Err(InterpolationError::Spline(
"Could not find valid segment for interpolation".to_string(),
));
}
let left = at(segment)?;
let right = at(shift_index(segment, 1, spline)?)?;
let h = d_sub(right.x, left.x, "Curve::spline_interpolate::h")
.map_err(interp_err(InterpolationError::Spline))?;
if h.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let dx = d_sub(right.x, x, "Curve::spline_interpolate::dx")
.map_err(interp_err(InterpolationError::Spline))?;
let dx1 = d_sub(x, left.x, "Curve::spline_interpolate::dx1")
.map_err(interp_err(InterpolationError::Spline))?;
let m_left = band_at(m, segment, "m")?;
let m_right = band_at(m, shift_index(segment, 1, spline)?, "m")?;
let six_h = d_mul(dec!(6), h, "Curve::spline_interpolate::six_h")
.map_err(interp_err(InterpolationError::Spline))?;
let left_cube = cube_scaled(m_left, dx, six_h, "Curve::spline_interpolate::left_cube")?;
let right_cube = cube_scaled(m_right, dx1, six_h, "Curve::spline_interpolate::right_cube")?;
let left_linear = linear_term(left.y, m_left, h, dx, "Curve::spline_interpolate::left")?;
let right_linear = linear_term(right.y, m_right, h, dx1, "Curve::spline_interpolate::right")?;
let y = d_sum_iter(
[left_cube, right_cube, left_linear, right_linear],
"Curve::spline_interpolate::y",
)
.map_err(interp_err(InterpolationError::Spline))?;
Ok(Point2D::new(x, y))
}
impl StatisticalCurve for Curve {
fn get_x_values(&self) -> Vec<Decimal> {
self.points.iter().map(|p| p.x).collect()
}
}
impl MetricsExtractor for Curve {
fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
let y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
if y_values.is_empty() {
return Ok(BasicMetrics {
mean: Decimal::ZERO,
median: Decimal::ZERO,
mode: Decimal::ZERO,
std_dev: Decimal::ZERO,
});
}
let mean = mean_of(&y_values, "Curve::compute_basic_metrics::mean")
.map_err(|e| MetricsError::BasicError(e.to_string()))?;
let mut sorted_values = y_values.clone();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = sorted_values.len() / 2;
let median = if sorted_values.len().is_multiple_of(2) {
let below = mid.checked_sub(1).ok_or_else(|| {
MetricsError::BasicError(
"median: empty sample after the emptiness guard".to_string(),
)
})?;
let lower = sample_at(&sorted_values, below, "median")?;
let upper = sample_at(&sorted_values, mid, "median")?;
let pair = d_add(lower, upper, "Curve::compute_basic_metrics::median_pair")
.map_err(|e| MetricsError::BasicError(e.to_string()))?;
d_div(pair, Decimal::TWO, "Curve::compute_basic_metrics::median")
.map_err(|e| MetricsError::BasicError(e.to_string()))?
} else {
sample_at(&sorted_values, mid, "median")?
};
let mode = {
let mut freq_map = std::collections::HashMap::new();
for &val in &y_values {
let count = freq_map.entry(val).or_insert(0usize);
*count = count.checked_add(1).ok_or_else(|| {
MetricsError::BasicError("mode: frequency count overflowed".to_string())
})?;
}
freq_map
.into_iter()
.max_by_key(|&(_, count)| count)
.map(|(val, _)| val)
.unwrap_or(Decimal::ZERO)
};
let variance = variance_of(&y_values, mean, "Curve::compute_basic_metrics::variance")
.map_err(|e| MetricsError::BasicError(e.to_string()))?;
let std_dev = d_sqrt(variance, "Curve::compute_basic_metrics::std_dev")
.map_err(|e| MetricsError::BasicError(e.to_string()))?;
Ok(BasicMetrics {
mean,
median,
mode,
std_dev,
})
}
fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
let y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
if y_values.len() < 2 {
return Ok(ShapeMetrics {
skewness: Decimal::ZERO,
kurtosis: Decimal::ZERO,
peaks: vec![],
valleys: vec![],
inflection_points: vec![],
});
}
let mean = mean_of(&y_values, "Curve::compute_shape_metrics::mean")
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let centered_values: Vec<Decimal> = y_values
.iter()
.map(|&x| d_sub(x, mean, "Curve::compute_shape_metrics::centered"))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let variance = variance_of(&y_values, mean, "Curve::compute_shape_metrics::variance")
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let std_dev = d_sqrt(variance, "Curve::compute_shape_metrics::std_dev")
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
if std_dev.is_zero() || std_dev < dec!(1e-9) {
return Err(MetricsError::ShapeError(format!(
"standard deviation ({std_dev}) is too small to compute skewness/kurtosis; the curve is degenerate"
)));
}
let skewness = standardized_moment(
¢ered_values,
std_dev,
3,
"Curve::compute_shape_metrics::skewness",
)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let raw_kurtosis = standardized_moment(
¢ered_values,
std_dev,
4,
"Curve::compute_shape_metrics::kurtosis",
)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let kurtosis = d_sub(
raw_kurtosis,
Decimal::from(3),
"Curve::compute_shape_metrics::excess_kurtosis",
)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let (peaks, valleys) = detect_peaks_and_valleys(&self.points, dec!(0.1), 2)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
Ok(ShapeMetrics {
skewness,
kurtosis,
peaks,
valleys,
inflection_points: vec![],
})
}
fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
if self.points.is_empty() {
return Ok(RangeMetrics {
min: Point2D::new(Decimal::ZERO, Decimal::ZERO),
max: Point2D::new(Decimal::ZERO, Decimal::ZERO),
range: Decimal::ZERO,
quartiles: (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
interquartile_range: Decimal::ZERO,
});
}
let mut y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
y_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let len = y_values.len();
let min_point = self
.points
.iter()
.min_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
.cloned()
.ok_or_else(|| MetricsError::BasicError("empty curve in min_point".to_string()))?;
let max_point = self
.points
.iter()
.max_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
.cloned()
.ok_or_else(|| MetricsError::BasicError("empty curve in max_point".to_string()))?;
let range = d_sub(
max_point.y,
min_point.y,
"Curve::compute_range_metrics::range",
)
.map_err(|e| MetricsError::RangeError(e.to_string()))?;
let q1 = sample_at(&y_values, len / 4, "first quartile")?;
let q2 = sample_at(&y_values, len / 2, "median")?;
let q3 = sample_at(&y_values, third_quartile_index(len)?, "third quartile")?;
let interquartile_range = d_sub(q3, q1, "Curve::compute_range_metrics::iqr")
.map_err(|e| MetricsError::RangeError(e.to_string()))?;
Ok(RangeMetrics {
min: min_point,
max: max_point,
range,
quartiles: (q1, q2, q3),
interquartile_range,
})
}
fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
let points: Vec<Point2D> = self.points.clone().into_iter().collect();
if points.len() < 2 {
return Ok(TrendMetrics {
slope: Decimal::ZERO,
intercept: Decimal::ZERO,
r_squared: Decimal::ZERO,
moving_average: vec![],
});
}
let n = Decimal::from(points.len());
let x_vals: Vec<Decimal> = points.iter().map(|p| p.x).collect();
let y_vals: Vec<Decimal> = points.iter().map(|p| p.y).collect();
let trend = || -> Result<(Decimal, Decimal, Decimal), DecimalError> {
let op = "Curve::compute_trend_metrics";
let sum_x = d_sum_iter(x_vals.iter().copied(), op)?;
let sum_y = d_sum_iter(y_vals.iter().copied(), op)?;
let mut sum_xy = Decimal::ZERO;
let mut sum_xx = Decimal::ZERO;
for (x, y) in x_vals.iter().zip(&y_vals) {
sum_xy = d_add(sum_xy, d_mul(*x, *y, op)?, op)?;
sum_xx = d_add(sum_xx, d_mul(*x, *x, op)?, op)?;
}
let numerator = d_sub(d_mul(n, sum_xy, op)?, d_mul(sum_x, sum_y, op)?, op)?;
let denominator = d_sub(d_mul(n, sum_xx, op)?, d_mul(sum_x, sum_x, op)?, op)?;
let slope = d_div(numerator, denominator, op)?;
let intercept = d_div(d_sub(sum_y, d_mul(slope, sum_x, op)?, op)?, n, op)?;
let mean_y = d_div(sum_y, n, op)?;
let mut sst = Decimal::ZERO;
for y in &y_vals {
let centered = d_sub(*y, mean_y, op)?;
sst = d_add(sst, powu_checked(centered, 2, op)?, op)?;
}
let mut ssr = Decimal::ZERO;
for (y, x) in y_vals.iter().zip(&x_vals) {
let y_predicted = d_add(d_mul(slope, *x, op)?, intercept, op)?;
let residual = d_sub(*y, y_predicted, op)?;
ssr = d_add(ssr, powu_checked(residual, 2, op)?, op)?;
}
let r_squared = if sst == Decimal::ZERO {
Decimal::ONE
} else {
d_sub(Decimal::ONE, d_div(ssr, sst, op)?, op)?
};
Ok((slope, intercept, r_squared))
};
let (slope, intercept, r_squared) =
trend().map_err(|e| MetricsError::TrendError(e.to_string()))?;
let window_sizes = [3, 5, 7];
let mut moving_average: Vec<Point2D> = Vec::new();
for window in window_sizes {
if window > points.len() {
continue;
}
for window_points in points.windows(window) {
let xs: Vec<Decimal> = window_points.iter().map(|p| p.x).collect();
let ys: Vec<Decimal> = window_points.iter().map(|p| p.y).collect();
let avg_x = mean_of(&xs, "Curve::compute_trend_metrics::moving_average_x")
.map_err(|e| MetricsError::TrendError(e.to_string()))?;
let avg_y = mean_of(&ys, "Curve::compute_trend_metrics::moving_average_y")
.map_err(|e| MetricsError::TrendError(e.to_string()))?;
moving_average.push(Point2D::new(avg_x, avg_y));
}
}
Ok(TrendMetrics {
slope,
intercept,
r_squared,
moving_average,
})
}
fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
let y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
if y_values.is_empty() {
return Ok(RiskMetrics {
volatility: Decimal::ZERO,
value_at_risk: Decimal::ZERO,
expected_shortfall: Decimal::ZERO,
coefficient_of_variation: Decimal::ZERO,
sharpe_ratio: Decimal::ZERO,
});
}
let op = "Curve::compute_risk_metrics";
let mean = mean_of(&y_values, op).map_err(|e| MetricsError::RiskError(e.to_string()))?;
let volatility = population_std_dev(&y_values, mean, op)
.map_err(|e| MetricsError::RiskError(e.to_string()))?;
let z_score = dec!(1.645);
let scaled_vol =
d_mul(z_score, volatility, op).map_err(|e| MetricsError::RiskError(e.to_string()))?;
let var =
d_sub(mean, scaled_vol, op).map_err(|e| MetricsError::RiskError(e.to_string()))?;
let tail: Vec<Decimal> = y_values.iter().copied().filter(|&x| x < var).collect();
let expected_shortfall = if tail.is_empty() {
Decimal::ZERO
} else {
mean_of(&tail, op).map_err(|e| MetricsError::RiskError(e.to_string()))?
};
let coefficient_of_variation = coefficient_of_variation(volatility, mean, op)?;
let sharpe_ratio = if volatility.is_zero() {
Decimal::ZERO
} else {
d_div(mean, volatility, op).map_err(|e| MetricsError::RiskError(e.to_string()))?
};
Ok(RiskMetrics {
volatility,
value_at_risk: var,
expected_shortfall,
coefficient_of_variation,
sharpe_ratio,
})
}
}
impl Arithmetic<Curve> for Curve {
type Error = CurveError;
fn merge(curves: &[&Curve], operation: MergeOperation) -> Result<Curve, CurveError> {
if curves.is_empty() {
return Err(CurveError::invalid_parameters(
"merge_curves",
"No curves provided for merging",
));
}
if let [only] = curves {
return Ok((*only).clone());
}
let min_x = curves
.iter()
.map(|c| c.x_range.0)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(Decimal::ZERO);
let max_x = curves
.iter()
.map(|c| c.x_range.1)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(Decimal::ZERO);
if min_x >= max_x {
return Err(CurveError::invalid_parameters(
"merge_curves",
"Curves have incompatible x-ranges",
));
}
let steps = 100; let op = "Curve::merge";
let span = d_sub(max_x, min_x, op).map_err(construction_err)?;
let step_size = d_div(span, Decimal::from(steps), op).map_err(construction_err)?;
let result_points: Result<Vec<Point2D>, CurveError> = (0..=steps)
.into_par_iter()
.map(|i| {
let offset = d_mul(step_size, Decimal::from(i), op).map_err(construction_err)?;
let x = d_add(min_x, offset, op)
.map_err(construction_err)?
.min(max_x);
let y_values: Result<Vec<Decimal>, CurveError> = curves
.iter()
.map(|curve| {
curve
.interpolate(x, InterpolationType::Cubic)
.map(|point| point.y)
.map_err(CurveError::from)
})
.collect();
let y_values = y_values?;
let result_y: Decimal = match operation {
MergeOperation::Add => {
d_sum_iter(y_values.iter().copied(), op).map_err(construction_err)?
}
MergeOperation::Subtract => {
let signed = y_values
.iter()
.enumerate()
.map(|(i, &val)| if i == 0 { val } else { -val });
d_sum_iter(signed, op).map_err(construction_err)?
}
MergeOperation::Multiply => {
d_product_iter(y_values.iter().copied(), op).map_err(construction_err)?
}
MergeOperation::Divide => {
y_values.iter().copied().enumerate().try_fold(
Decimal::ONE,
|acc, (i, val)| {
let factor = if i == 0 {
val
} else if val == Decimal::ZERO {
Decimal::MAX
} else {
d_div(Decimal::ONE, val, op).map_err(construction_err)?
};
d_mul(acc, factor, op).map_err(construction_err)
},
)?
}
MergeOperation::Max => y_values
.par_iter()
.cloned()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(Decimal::ZERO),
MergeOperation::Min => y_values
.par_iter()
.cloned()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(Decimal::ZERO),
};
Ok(Point2D::new(x, result_y))
})
.collect();
let result_points = result_points?;
Ok(Curve::from_vector(result_points))
}
fn merge_with(&self, other: &Curve, operation: MergeOperation) -> Result<Curve, CurveError> {
Self::merge(&[self, other], operation)
}
}
impl AxisOperations<Point2D, Decimal> for Curve {
type Error = CurveError;
fn contains_point(&self, x: &Decimal) -> bool {
self.points_at(*x).next().is_some()
}
fn get_index_values(&self) -> Vec<Decimal> {
self.points.iter().map(|p| p.x).collect()
}
fn get_values(&self, x: Decimal) -> Vec<&Decimal> {
self.points_at(x).map(|p| &p.y).collect()
}
fn get_closest_point(&self, x: &Decimal) -> Result<&Point2D, Self::Error> {
let mut closest: Option<(&Point2D, Decimal)> = None;
for point in &self.points {
let distance = d_sub(point.x, *x, "Curve::get_closest_point")
.map_err(analysis_err)?
.abs();
match closest {
Some((_, best)) if best <= distance => {}
_ => closest = Some((point, distance)),
}
}
closest
.map(|(point, _)| point)
.ok_or(CurveError::Point2DError {
reason: "No points available",
})
}
fn get_point(&self, x: &Decimal) -> Option<&Point2D> {
self.points_at(*x).next()
}
}
fn intersect_all_pairs(
curve: &Curve,
other: &Curve,
epsilon: Decimal,
) -> Result<Vec<Point2D>, CurveError> {
let mut intersections = Vec::new();
for p1 in &curve.points {
for p2 in &other.points {
let dx = d_sub(p1.x, p2.x, "Curve::intersect_with::dx")
.map_err(analysis_err)?
.abs();
if dx >= epsilon {
continue;
}
let dy = d_sub(p1.y, p2.y, "Curve::intersect_with::dy")
.map_err(analysis_err)?
.abs();
if dy < epsilon {
intersections.push(*p1);
}
}
}
Ok(intersections)
}
fn resample<'a>(
curve: &'a Curve,
spline: &mut Option<CurveSpline<'a>>,
x: Decimal,
interpolation: InterpolationType,
) -> Result<Point2D, InterpolationError> {
if interpolation == InterpolationType::Spline {
spline
.get_or_insert_with(|| CurveSpline::new(curve))
.interpolate(x)
} else {
curve.interpolate(x, interpolation)
}
}
impl MergeAxisInterpolate<Point2D, Decimal> for Curve
where
Self: Sized,
{
fn merge_axis_interpolate(
&self,
other: &Self,
interpolation: InterpolationType,
) -> Result<(Self, Self), Self::Error> {
let merged_x_values = self.merge_axis_index(other);
let mut sorted_x_values: Vec<Decimal> = merged_x_values.into_iter().collect();
sorted_x_values.sort();
let mut interpolated_self_points = BTreeSet::new();
let mut interpolated_other_points = BTreeSet::new();
let mut self_spline: Option<CurveSpline<'_>> = None;
let mut other_spline: Option<CurveSpline<'_>> = None;
for x in &sorted_x_values {
if self.contains_point(x) {
let pt = self.get_point(x).ok_or_else(|| {
CurveError::InterpolationError(format!(
"missing self point at x={x} despite contains_point()"
))
})?;
interpolated_self_points.insert(*pt);
} else {
let interpolated_point = resample(self, &mut self_spline, *x, interpolation)?;
interpolated_self_points.insert(interpolated_point);
}
if other.contains_point(x) {
let pt = other.get_point(x).ok_or_else(|| {
CurveError::InterpolationError(format!(
"missing other point at x={x} despite contains_point()"
))
})?;
interpolated_other_points.insert(*pt);
} else {
let interpolated_point = resample(other, &mut other_spline, *x, interpolation)?;
interpolated_other_points.insert(interpolated_point);
}
}
Ok((
Curve::new(interpolated_self_points),
Curve::new(interpolated_other_points),
))
}
}
impl GeometricTransformations<Point2D> for Curve {
type Error = CurveError;
fn translate(&self, deltas: Vec<&Decimal>) -> Result<Self, Self::Error> {
if deltas.len() != 2 {
return Err(CurveError::invalid_parameters(
"translate",
"Expected 2 deltas for 2D translation",
));
}
let (Some(dx), Some(dy)) = (deltas.first(), deltas.get(1)) else {
return Err(CurveError::invalid_parameters(
"translate",
"Expected 2 deltas for 2D translation",
));
};
let translated_points = self
.points
.iter()
.map(|point| {
let x = d_add(point.x, **dx, "Curve::translate::x")?;
let y = d_add(point.y, **dy, "Curve::translate::y")?;
Ok(Point2D::new(x, y))
})
.collect::<Result<BTreeSet<Point2D>, DecimalError>>()
.map_err(construction_err)?;
Ok(Curve::new(translated_points))
}
fn scale(&self, factors: Vec<&Decimal>) -> Result<Self, Self::Error> {
if factors.len() != 2 {
return Err(CurveError::invalid_parameters(
"scale",
"Expected 2 factors for 2D scaling",
));
}
let (Some(fx), Some(fy)) = (factors.first(), factors.get(1)) else {
return Err(CurveError::invalid_parameters(
"scale",
"Expected 2 factors for 2D scaling",
));
};
let scaled_points = self
.points
.iter()
.map(|point| {
let x = d_mul(point.x, **fx, "Curve::scale::x")?;
let y = d_mul(point.y, **fy, "Curve::scale::y")?;
Ok(Point2D::new(x, y))
})
.collect::<Result<BTreeSet<Point2D>, DecimalError>>()
.map_err(construction_err)?;
Ok(Curve::new(scaled_points))
}
fn intersect_with(&self, other: &Self) -> Result<Vec<Point2D>, Self::Error> {
let epsilon = Decimal::new(1, 6);
let fits = match (
self.points.first(),
self.points.last(),
other.points.first(),
other.points.last(),
) {
(Some(first), Some(last), Some(other_first), Some(other_last)) => {
d_sub(first.x, other_last.x, "Curve::intersect_with::dx").is_ok()
&& d_sub(last.x, other_first.x, "Curve::intersect_with::dx").is_ok()
}
_ => true,
};
if !fits {
return intersect_all_pairs(self, other, epsilon);
}
let mut intersections = Vec::new();
let mut window = other.points.iter();
for p1 in &self.points {
while let Some(p2) = window.clone().next() {
let dx = d_sub(p1.x, p2.x, "Curve::intersect_with::dx").map_err(analysis_err)?;
if dx < epsilon {
break;
}
window.next();
}
for p2 in window.clone() {
let dx = d_sub(p1.x, p2.x, "Curve::intersect_with::dx")
.map_err(analysis_err)?
.abs();
if dx >= epsilon {
break;
}
let dy = d_sub(p1.y, p2.y, "Curve::intersect_with::dy")
.map_err(analysis_err)?
.abs();
if dy < epsilon {
intersections.push(*p1);
}
}
}
Ok(intersections)
}
fn derivative_at(&self, point: &Point2D) -> Result<Vec<Decimal>, Self::Error> {
let (p0, p1) = self.bracket(point.x)?;
let op = "Curve::derivative_at";
let rise = d_sub(p1.y, p0.y, op).map_err(analysis_err)?;
let x1_sq = d_mul(p1.x, p1.x, op).map_err(analysis_err)?;
let x0_sq = d_mul(p0.x, p0.x, op).map_err(analysis_err)?;
let run = d_sub(x1_sq, x0_sq, op).map_err(analysis_err)?;
if run.is_zero() {
return Err(CurveError::AnalysisError(
"derivative_at: bracketing abscissas have equal squares".to_string(),
));
}
let a = d_div(rise, run, op).map_err(analysis_err)?;
let slope = d_mul(dec!(2.0), a, op).map_err(analysis_err)?;
let derivative = d_mul(slope, point.x, op).map_err(analysis_err)?;
Ok(vec![derivative])
}
fn extrema(&self) -> Result<(Point2D, Point2D), Self::Error> {
if self.points.is_empty() {
return Err(CurveError::invalid_parameters(
"extrema",
"Curve has no points",
));
}
let min_point = self
.points
.iter()
.min_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
.cloned()
.ok_or_else(|| {
CurveError::AnalysisError("extrema: empty curve in min_by".to_string())
})?;
let max_point = self
.points
.iter()
.max_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
.cloned()
.ok_or_else(|| {
CurveError::AnalysisError("extrema: empty curve in max_by".to_string())
})?;
Ok((min_point, max_point))
}
fn measure_under(&self, base_value: &Decimal) -> Result<Decimal, Self::Error> {
if self.points.len() < 2 {
return Ok(Decimal::ZERO);
}
let mut area = Decimal::ZERO;
let points: Vec<_> = self.points.iter().collect();
let op = "Curve::measure_under";
for pair in points.windows(2) {
let (Some(left), Some(right)) = (pair.first(), pair.get(1)) else {
return Err(CurveError::AnalysisError(
"measure_under: trapezoid window is shorter than two points".to_string(),
));
};
let width = d_sub(right.x, left.x, op).map_err(analysis_err)?;
let left_height = d_sub(left.y, *base_value, op).map_err(analysis_err)?;
let right_height = d_sub(right.y, *base_value, op).map_err(analysis_err)?;
let sum_heights = d_add(left_height, right_height, op).map_err(analysis_err)?;
let height = d_div(sum_heights, Decimal::TWO, op).map_err(analysis_err)?;
let slice = d_mul(width, height, op).map_err(analysis_err)?;
area = d_add(area, slice, op).map_err(analysis_err)?;
}
Ok(area.abs())
}
}
#[cfg(test)]
mod tests_curves {
use super::*;
use crate::curves::utils::{create_constant_curve, create_linear_curve};
use Decimal;
use optionstratlib_core::{model::Positive, pos_or_panic};
use rust_decimal_macros::dec;
#[test]
fn test_new_with_decimal() {
let x = dec!(1.5);
let y = dec!(2.5);
let point = Point2D::new(x, y);
assert_eq!(point.x, dec!(1.5));
assert_eq!(point.y, dec!(2.5));
}
#[test]
fn test_new_with_positive() {
let x = pos_or_panic!(1.5_f64);
let y = pos_or_panic!(2.5_f64);
let point = Point2D::new(x, y);
assert_eq!(point.x, dec!(1.5));
assert_eq!(point.y, dec!(2.5));
}
#[test]
fn test_to_tuple_with_decimal() {
let point = Point2D::new(dec!(1.5), dec!(2.5));
let tuple: (Decimal, Decimal) = point.to_tuple().unwrap();
assert_eq!(tuple, (dec!(1.5), dec!(2.5)));
}
#[test]
fn test_to_tuple_with_positive() {
let point = Point2D::new(dec!(1.5), dec!(2.5));
let tuple: (Positive, Positive) = point.to_tuple().unwrap();
assert_eq!(tuple, (pos_or_panic!(1.5), pos_or_panic!(2.5)));
}
#[test]
fn test_from_tuple_with_decimal() {
let x = dec!(1.5);
let y = dec!(2.5);
let point = Point2D::from_tuple(x, y).unwrap();
assert_eq!(point, Point2D::new(dec!(1.5), dec!(2.5)));
}
#[test]
fn test_from_tuple_with_positive() {
let x = pos_or_panic!(1.5_f64);
let y = pos_or_panic!(2.5_f64);
let point = Point2D::from_tuple(x, y).unwrap();
assert_eq!(point, Point2D::new(dec!(1.5), dec!(2.5)));
}
#[test]
fn test_new_with_mixed_types() {
let x = dec!(1.5);
let y = pos_or_panic!(2.5_f64);
let point = Point2D::new(x, y);
assert_eq!(point.x, dec!(1.5));
assert_eq!(point.y, dec!(2.5));
}
#[test]
fn test_create_constant_curve() {
let curve = create_constant_curve(dec!(1.0), dec!(2.0), dec!(5.0)).unwrap();
assert_eq!(curve.get_points().len(), 11);
for point in curve.get_points() {
assert_eq!(point.y, dec!(5.0));
}
}
#[test]
fn test_create_linear_curve() {
let curve = create_linear_curve(dec!(1.0), dec!(2.0), dec!(2.0)).unwrap();
assert_eq!(curve.get_points().len(), 11);
let mut slope = dec!(2.0);
for point in curve.get_points() {
assert_eq!(point.y, slope);
slope += dec!(0.2);
}
}
}
#[cfg(test)]
mod tests_linear_interpolate {
use super::*;
use crate::geometrics::InterpolationType;
use rust_decimal_macros::dec;
#[test]
fn test_linear_interpolation_exact_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(Decimal::ZERO, Decimal::ZERO),
Point2D::new(Decimal::ONE, Decimal::TWO),
]));
let p0 = curve
.interpolate(Decimal::ZERO, InterpolationType::Linear)
.unwrap();
assert_eq!(p0.x, Decimal::ZERO);
assert_eq!(p0.y, Decimal::ZERO);
let p1 = curve
.interpolate(Decimal::ONE, InterpolationType::Linear)
.unwrap();
assert_eq!(p1.x, Decimal::ONE);
assert_eq!(p1.y, Decimal::TWO);
}
#[test]
fn test_linear_interpolation_midpoint() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(Decimal::ZERO, Decimal::ZERO),
Point2D::new(Decimal::ONE, Decimal::TWO),
]));
let mid = curve
.interpolate(dec!(0.5), InterpolationType::Linear)
.unwrap();
assert_eq!(mid.x, dec!(0.5));
assert_eq!(mid.y, dec!(1.0));
}
#[test]
fn test_linear_interpolation_quarter_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(Decimal::ZERO, Decimal::ZERO),
Point2D::new(Decimal::ONE, Decimal::TWO),
]));
let p25 = curve
.interpolate(dec!(0.25), InterpolationType::Linear)
.unwrap();
assert_eq!(p25.x, dec!(0.25));
assert_eq!(p25.y, dec!(0.5));
let p75 = curve
.interpolate(dec!(0.75), InterpolationType::Linear)
.unwrap();
assert_eq!(p75.x, dec!(0.75));
assert_eq!(p75.y, dec!(1.5));
}
#[test]
fn test_linear_interpolation_out_of_range() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(2.0)),
]));
assert!(
curve
.interpolate(dec!(-0.1), InterpolationType::Linear)
.is_err()
);
assert!(
curve
.interpolate(dec!(1.1), InterpolationType::Linear)
.is_err()
);
}
#[test]
fn test_linear_interpolation_insufficient_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
dec!(0.0),
dec!(0.0),
)]));
assert!(
curve
.interpolate(dec!(0.5), InterpolationType::Linear)
.is_err()
);
}
#[test]
fn test_linear_interpolation_non_monotonic() {
let curve = Curve::new(BTreeSet::from_iter(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)),
]));
let p15 = curve
.interpolate(dec!(1.5), InterpolationType::Linear)
.unwrap();
assert_eq!(p15.x, dec!(1.5));
assert_eq!(p15.y, dec!(1.5));
}
}
#[cfg(test)]
mod tests_bilinear_interpolate {
use super::*;
use crate::geometrics::InterpolationType;
use rust_decimal_macros::dec;
#[test]
fn test_bilinear_interpolation() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(Decimal::ZERO, Decimal::ZERO),
Point2D::new(Decimal::TWO, Decimal::ONE),
Point2D::new(Decimal::TEN, Decimal::ONE),
Point2D::new(Decimal::ONE, Decimal::TWO),
]));
let corner = curve
.interpolate(Decimal::ZERO, InterpolationType::Bilinear)
.unwrap();
assert_eq!(corner.x, Decimal::ZERO);
assert_eq!(corner.y, Decimal::ZERO);
let mid = curve
.interpolate(dec!(0.5), InterpolationType::Bilinear)
.unwrap();
assert_eq!(mid.x, dec!(0.5));
assert_eq!(mid.y, dec!(1.0));
}
#[test]
fn test_bilinear_interpolation_out_of_range() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(2.0)),
]));
assert!(
curve
.interpolate(dec!(-0.5), InterpolationType::Bilinear)
.is_err()
);
assert!(
curve
.interpolate(dec!(1.5), InterpolationType::Bilinear)
.is_err()
);
}
#[test]
fn test_bilinear_interpolation_insufficient_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(2.0)),
]));
assert!(
curve
.interpolate(dec!(0.5), InterpolationType::Bilinear)
.is_err()
);
}
#[test]
fn test_bilinear_interpolation_quarter_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(Decimal::ZERO, Decimal::ZERO), Point2D::new(Decimal::ONE, Decimal::ONE), Point2D::new(Decimal::TWO, Decimal::ONE), Point2D::new(Decimal::TEN, Decimal::TWO), ]));
let p25 = curve
.interpolate(dec!(0.25), InterpolationType::Bilinear)
.unwrap();
assert_eq!(p25.x, dec!(0.25));
assert_eq!(p25.y, dec!(0.75));
let p75 = curve
.interpolate(dec!(0.75), InterpolationType::Bilinear)
.unwrap();
assert_eq!(p75.x, dec!(0.75));
assert_eq!(p75.y, dec!(1.25));
}
#[test]
fn test_bilinear_interpolation_boundaries() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(2.0)),
]));
assert!(
curve
.interpolate(dec!(-0.1), InterpolationType::Bilinear)
.is_err()
);
assert!(
curve
.interpolate(dec!(1.1), InterpolationType::Bilinear)
.is_err()
);
}
#[test]
fn test_out_of_range() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(Decimal::ZERO, Decimal::ZERO),
Point2D::new(Decimal::ONE, Decimal::ONE),
Point2D::new(Decimal::ZERO, Decimal::ONE),
Point2D::new(Decimal::ONE, Decimal::TWO),
]));
assert!(
curve
.interpolate(dec!(-1), InterpolationType::Bilinear)
.is_err()
);
assert!(
curve
.interpolate(Decimal::TWO, InterpolationType::Bilinear)
.is_err()
);
}
fn quadratic_four() -> Curve {
Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0), dec!(0)),
Point2D::new(dec!(1), dec!(1)),
Point2D::new(dec!(2), dec!(4)),
Point2D::new(dec!(3), dec!(9)),
]))
}
fn quadratic_five() -> Curve {
Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0), dec!(0)),
Point2D::new(dec!(1), dec!(1)),
Point2D::new(dec!(2), dec!(4)),
Point2D::new(dec!(3), dec!(9)),
Point2D::new(dec!(4), dec!(16)),
]))
}
fn irregular() -> Curve {
Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0), dec!(1)),
Point2D::new(dec!(0.5), dec!(2.25)),
Point2D::new(dec!(2), dec!(-1.5)),
Point2D::new(dec!(3.25), dec!(4)),
Point2D::new(dec!(5), dec!(0.75)),
]))
}
#[test]
fn test_bilinear_interpolate_four_point_upper_segments_match_reference() {
let curve = quadratic_four();
let mid = curve.bilinear_interpolate(dec!(1.5)).unwrap();
assert_eq!(mid.x, dec!(1.5));
assert_eq!(mid.y, dec!(4.5));
let last = curve.bilinear_interpolate(dec!(2.5)).unwrap();
assert_eq!(last.x, dec!(2.5));
assert_eq!(last.y, dec!(6.5));
}
#[test]
fn test_bilinear_interpolate_five_point_upper_segments_match_reference() {
let curve = quadratic_five();
let second_to_last = curve.bilinear_interpolate(dec!(2.5)).unwrap();
assert_eq!(second_to_last.y, dec!(9.5));
let last = curve.bilinear_interpolate(dec!(3.5)).unwrap();
assert_eq!(last.y, dec!(12.5));
}
#[test]
fn test_bilinear_interpolate_interior_is_unchanged_by_the_clamp() {
let four = quadratic_four();
assert_eq!(four.bilinear_interpolate(dec!(0.5)).unwrap().y, dec!(3.5));
let five = quadratic_five();
assert_eq!(five.bilinear_interpolate(dec!(0.5)).unwrap().y, dec!(3.5));
assert_eq!(five.bilinear_interpolate(dec!(1.5)).unwrap().y, dec!(7.5));
}
#[test]
fn test_bilinear_interpolate_final_segment_equals_linear() {
for (curve, x) in [(quadratic_four(), dec!(2.5)), (quadratic_five(), dec!(3.5))] {
let bilinear = curve.bilinear_interpolate(x).unwrap();
let linear = curve.linear_interpolate(x).unwrap();
assert_eq!(bilinear.y, linear.y);
}
}
#[test]
fn test_bilinear_interpolate_final_segment_equals_linear_to_the_last_place() {
let curve = irregular();
let bilinear = curve.bilinear_interpolate(dec!(4)).unwrap().y;
let linear = curve.linear_interpolate(dec!(4)).unwrap().y;
assert_ne!(bilinear, linear);
assert!((bilinear - linear).abs() < dec!(0.0000000000000000000000001));
}
#[test]
fn test_bilinear_interpolate_irregular_curve_matches_reference() {
const TOLERANCE: Decimal = dec!(0.0000000000000000000000001);
let curve = irregular();
assert_eq!(
curve.bilinear_interpolate(dec!(0.25)).unwrap().y,
dec!(1.4375)
);
assert_eq!(curve.bilinear_interpolate(dec!(2.5)).unwrap().y, dec!(1.7));
let recurring = curve.bilinear_interpolate(dec!(1)).unwrap().y;
assert!((recurring - dec!(1.9583333333333333333333333333)).abs() < TOLERANCE);
let last = curve.bilinear_interpolate(dec!(4)).unwrap().y;
assert!((last - dec!(2.6071428571428571428571428571)).abs() < TOLERANCE);
}
#[test]
fn test_bilinear_interpolate_sample_abscissa_returns_the_sample() {
let curve = quadratic_four();
for (x, y) in [
(dec!(0), dec!(0)),
(dec!(1), dec!(1)),
(dec!(2), dec!(4)),
(dec!(3), dec!(9)),
] {
let point = curve.bilinear_interpolate(x).unwrap();
assert_eq!(point.x, x);
assert_eq!(point.y, y);
}
}
#[test]
fn test_bilinear_interpolate_stacked_abscissa_is_degenerate() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0), dec!(0)),
Point2D::new(dec!(1), dec!(1)),
Point2D::new(dec!(1), dec!(7)),
Point2D::new(dec!(2), dec!(4)),
Point2D::new(dec!(3), dec!(9)),
]));
let err = curve.bilinear_interpolate(dec!(1)).unwrap_err();
assert!(matches!(err, InterpolationError::DegenerateInterval));
}
#[test]
fn test_bilinear_interpolate_jump_at_a_sample_predates_the_clamp() {
const EPSILON: Decimal = dec!(0.0000000001);
const SLACK: Decimal = dec!(0.00000001);
let curve = quadratic_five();
let from_left = curve.bilinear_interpolate(dec!(1) - EPSILON).unwrap().y;
assert!((from_left - dec!(5)).abs() < SLACK);
let from_right = curve.bilinear_interpolate(dec!(1) + EPSILON).unwrap().y;
assert!((from_right - dec!(5)).abs() < SLACK);
assert_eq!(curve.bilinear_interpolate(dec!(1)).unwrap().y, dec!(1));
}
#[test]
fn test_bilinear_interpolate_jumps_where_the_clamp_starts() {
const EPSILON: Decimal = dec!(0.0000000001);
const SLACK: Decimal = dec!(0.00000001);
let curve = quadratic_five();
let from_left = curve.bilinear_interpolate(dec!(2) - EPSILON).unwrap().y;
assert!((from_left - dec!(10)).abs() < SLACK);
let from_right = curve.bilinear_interpolate(dec!(2) + EPSILON).unwrap().y;
assert!((from_right - dec!(6.5)).abs() < SLACK);
assert_eq!(curve.bilinear_interpolate(dec!(2)).unwrap().y, dec!(4));
}
#[test]
fn test_bilinear_interpolate_stays_within_the_ordinate_range() {
for curve in [quadratic_four(), quadratic_five(), irregular()] {
let ordinates: Vec<Decimal> = curve.points.iter().map(|p| p.y).collect();
let low = ordinates.iter().copied().fold(Decimal::MAX, Decimal::min);
let high = ordinates.iter().copied().fold(Decimal::MIN, Decimal::max);
let first = curve.points.iter().next().unwrap().x;
let last = curve.points.iter().next_back().unwrap().x;
let step = (last - first) / dec!(64);
let mut x = first;
while x <= last {
let y = curve.bilinear_interpolate(x).unwrap().y;
assert!(y >= low, "{y} below the sample range at x = {x}");
assert!(y <= high, "{y} above the sample range at x = {x}");
x += step;
}
}
}
}
#[cfg(test)]
mod tests_cubic_interpolate {
use super::*;
use crate::geometrics::InterpolationType;
use rust_decimal_macros::dec;
use tracing::info;
#[test]
fn test_cubic_interpolation_exact_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let p1 = curve
.interpolate(dec!(1.0), InterpolationType::Cubic)
.unwrap();
assert_eq!(p1.x, dec!(1.0));
assert_eq!(p1.y, dec!(1.0));
}
#[test]
fn test_cubic_interpolation_midpoints() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let mid = curve
.interpolate(dec!(1.5), InterpolationType::Cubic)
.unwrap();
assert_eq!(mid.x, dec!(1.5));
assert!(mid.y > dec!(1.0) && mid.y < dec!(4.0));
}
#[test]
fn test_cubic_interpolation_insufficient_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
]));
assert!(
curve
.interpolate(dec!(1.5), InterpolationType::Cubic)
.is_err()
);
}
#[test]
fn test_cubic_interpolation_out_of_range() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
assert!(
curve
.interpolate(dec!(-0.5), InterpolationType::Cubic)
.is_err()
);
assert!(
curve
.interpolate(dec!(3.5), InterpolationType::Cubic)
.is_err()
);
}
#[test]
fn test_cubic_interpolation_monotonicity() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let p1 = curve
.interpolate(dec!(0.5), InterpolationType::Cubic)
.unwrap();
let p2 = curve
.interpolate(dec!(1.5), InterpolationType::Cubic)
.unwrap();
let p3 = curve
.interpolate(dec!(2.5), InterpolationType::Cubic)
.unwrap();
assert!(p1.y < p2.y);
assert!(p2.y < p3.y);
info!("p1: {:?}, p2: {:?}, p3: {:?}", p1, p2, p3);
}
}
#[cfg(test)]
mod tests_spline_interpolate {
use super::*;
use crate::geometrics::InterpolationType;
use rust_decimal_macros::dec;
#[test]
fn test_spline_interpolation_exact_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let p1 = curve
.interpolate(dec!(1.0), InterpolationType::Spline)
.unwrap();
assert_eq!(p1.x, dec!(1.0));
assert_eq!(p1.y, dec!(1.0));
}
#[test]
fn test_spline_interpolation_midpoints() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let mid = curve
.interpolate(dec!(1.5), InterpolationType::Spline)
.unwrap();
assert_eq!(mid.x, dec!(1.5));
assert!(mid.y > dec!(1.0) && mid.y < dec!(4.0));
}
#[test]
fn test_spline_interpolation_insufficient_points() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
assert!(
curve
.interpolate(dec!(0.5), InterpolationType::Spline)
.is_err()
);
}
#[test]
fn test_spline_interpolation_out_of_range() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
]));
assert!(
curve
.interpolate(dec!(-0.5), InterpolationType::Spline)
.is_err()
);
assert!(
curve
.interpolate(dec!(2.5), InterpolationType::Spline)
.is_err()
);
}
#[test]
fn test_spline_interpolation_smoothness() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let p1 = curve
.interpolate(dec!(1.48), InterpolationType::Spline)
.unwrap();
let p2 = curve
.interpolate(dec!(1.49), InterpolationType::Spline)
.unwrap();
let p3 = curve
.interpolate(dec!(1.50), InterpolationType::Spline)
.unwrap();
let p4 = curve
.interpolate(dec!(1.51), InterpolationType::Spline)
.unwrap();
let p5 = curve
.interpolate(dec!(1.52), InterpolationType::Spline)
.unwrap();
assert!(p1.y < p2.y);
assert!(p2.y < p3.y);
assert!(p3.y < p4.y);
assert!(p4.y < p5.y);
let d1 = p2.y - p1.y;
let d2 = p3.y - p2.y;
let d3 = p4.y - p3.y;
let d4 = p5.y - p4.y;
assert!((d2 - d1).abs() < dec!(0.001));
assert!((d3 - d2).abs() < dec!(0.001));
assert!((d4 - d3).abs() < dec!(0.001));
}
}
#[cfg(test)]
mod tests_curve_arithmetic {
use super::*;
use crate::curves::utils::{create_constant_curve, create_linear_curve};
use crate::geometrics::InterpolationType;
#[test]
fn test_merge_curves_add() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Add).unwrap();
let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
for x in &test_points {
let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
+ curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
assert!(
(result_point.y - expected_y).abs() < dec!(0.001),
"Failed at x = {}, expected {}, got {}",
x,
expected_y,
result_point.y
);
}
}
#[test]
fn test_merge_curves_subtract() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Subtract).unwrap();
let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
for x in &test_points {
let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
- curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
assert!(
(result_point.y - expected_y).abs() < dec!(0.001),
"Failed at x = {}, expected {}, got {}",
x,
expected_y,
result_point.y
);
}
}
#[test]
fn test_merge_curves_multiply() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Multiply).unwrap();
let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
for x in &test_points {
let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
* curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
assert!(
(result_point.y - expected_y).abs() < dec!(0.001),
"Failed at x = {}, expected {}, got {}",
x,
expected_y,
result_point.y
);
}
}
#[test]
fn test_merge_curves_divide() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(6.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Divide).unwrap();
let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
for x in &test_points {
let y2 = curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
if y2 == Decimal::ZERO {
continue;
}
let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y / y2;
let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
assert!(
(result_point.y - expected_y).abs() < dec!(0.001),
"Failed at x = {}, expected {}, got {}",
x,
expected_y,
result_point.y
);
}
}
#[test]
fn test_merge_curves_divide_folds_every_divisor() {
let eight = create_constant_curve(dec!(0.0), dec!(10.0), dec!(8.0)).unwrap();
let two_a = create_constant_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let two_b = create_constant_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let result = Curve::merge(&[&eight, &two_a, &two_b], MergeOperation::Divide).unwrap();
for x in [dec!(0.0), dec!(5.0), dec!(10.0)] {
let y = result.interpolate(x, InterpolationType::Cubic).unwrap().y;
assert!(
(y - dec!(2)).abs() < dec!(0.0001),
"expected 8 / 2 / 2 = 2 at x = {x}, got {y}"
);
}
}
const DETERMINISM_REPEATS: usize = 512;
fn ordinate_fingerprint(curve: &Curve) -> Vec<(i128, u32)> {
curve
.points
.iter()
.map(|point| (point.y.mantissa(), point.y.scale()))
.collect()
}
const POOL_SIZES: [usize; 5] = [1, 2, 4, 8, 16];
const RUNS_PER_POOL: usize = 4;
const CROSS_POOL_OPERANDS: u32 = 40;
fn cross_pool_operands() -> Vec<Curve> {
(0..CROSS_POOL_OPERANDS)
.map(|i| {
let value = Decimal::ONE - Decimal::ONE / Decimal::from(1000 + i * 7);
create_constant_curve(dec!(0), dec!(10), value)
.expect("constant curve fixture must build")
})
.collect()
}
fn assert_reproducible_across_pools(curves: &[&Curve], operation: MergeOperation) {
let mut reference: Option<Vec<(i128, u32)>> = None;
for threads in POOL_SIZES {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("rayon must build a pool of a fixed size");
for run in 0..RUNS_PER_POOL {
let merged = pool
.install(|| Curve::merge(curves, operation))
.expect("the fixture merge must succeed");
let fingerprint = ordinate_fingerprint(&merged);
match &reference {
None => reference = Some(fingerprint),
Some(reference) => assert_eq!(
&fingerprint, reference,
"run {run} on a {threads}-thread pool produced different digits \
from the first merge, under {operation:?}"
),
}
}
}
}
#[test]
fn test_merge_curves_multiply_is_reproducible_across_pool_sizes() {
let curves = cross_pool_operands();
let refs: Vec<&Curve> = curves.iter().collect();
assert_reproducible_across_pools(&refs, MergeOperation::Multiply);
}
#[test]
fn test_merge_curves_divide_is_reproducible_across_pool_sizes() {
let curves = cross_pool_operands();
let refs: Vec<&Curve> = curves.iter().collect();
assert_reproducible_across_pools(&refs, MergeOperation::Divide);
}
#[test]
fn test_merge_curves_multiply_is_reproducible() {
let a =
create_constant_curve(dec!(0), dec!(10), dec!(0.010989010989010989010989011)).unwrap();
let b =
create_constant_curve(dec!(0), dec!(10), dec!(0.010752688172043010752688172)).unwrap();
let c =
create_constant_curve(dec!(0), dec!(10), dec!(0.0094339622641509433962264151)).unwrap();
let expected = dec!(0.0000011147302687168785768908);
let first = Curve::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
for point in first.points.iter() {
assert_eq!(
(point.y.mantissa(), point.y.scale()),
(expected.mantissa(), expected.scale()),
"expected the left-to-right product {expected} at x = {}, got {}",
point.x,
point.y
);
}
let reference = ordinate_fingerprint(&first);
for run in 1..DETERMINISM_REPEATS {
let again = Curve::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
assert_eq!(
ordinate_fingerprint(&again),
reference,
"run {run} of {DETERMINISM_REPEATS} produced different digits from the first"
);
}
}
#[test]
fn test_merge_curves_divide_is_reproducible() {
let a = create_constant_curve(dec!(0), dec!(10), dec!(97)).unwrap();
let b = create_constant_curve(dec!(0), dec!(10), dec!(91)).unwrap();
let c = create_constant_curve(dec!(0), dec!(10), dec!(93)).unwrap();
let first = Curve::merge(&[&a, &b, &c], MergeOperation::Divide).unwrap();
let reference = ordinate_fingerprint(&first);
for run in 1..DETERMINISM_REPEATS {
let again = Curve::merge(&[&a, &b, &c], MergeOperation::Divide).unwrap();
assert_eq!(
ordinate_fingerprint(&again),
reference,
"run {run} of {DETERMINISM_REPEATS} produced different digits from the first"
);
}
}
#[test]
fn test_merge_curves_max() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Max).unwrap();
let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
for x in &test_points {
let y1 = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let y2 = curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let expected_y = y1.max(y2);
let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
assert!(
(result_point.y - expected_y).abs() < dec!(0.001),
"Failed at x = {}, expected {}, got {}",
x,
expected_y,
result_point.y
);
}
}
#[test]
fn test_merge_curves_min() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Min).unwrap();
let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
for x in &test_points {
let y1 = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let y2 = curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let expected_y = y1.min(y2);
let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
assert!(
(result_point.y - expected_y).abs() < dec!(0.001),
"Failed at x = {}, expected {}, got {}",
x,
expected_y,
result_point.y
);
}
}
#[test]
fn test_merge_with_single_operation() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
let result = curve1.merge_with(&curve2, MergeOperation::Add).unwrap();
let merged_result = Curve::merge(&[&curve1, &curve2], MergeOperation::Add).unwrap();
assert_eq!(result.points.len(), merged_result.points.len());
for i in 0..result.points.len() {
assert!((result[i].x - merged_result[i].x).abs() < dec!(0.001));
assert!((result[i].y - merged_result[i].y).abs() < dec!(0.001));
}
}
fn long_mantissa_curve() -> Curve {
let step = dec!(10) / dec!(511);
let points: Vec<Point2D> = (0..=511i64)
.map(|i| {
let x = step * Decimal::from(i);
Point2D::new(x, x)
})
.collect();
Curve::from_vector(points.iter().collect())
}
#[test]
fn test_merge_curves_long_mantissa_range_ends_on_max_x() {
let curve = long_mantissa_curve();
let max_x = curve.x_range.1;
assert_eq!(max_x, dec!(10.000000000000000000000000016));
let result = curve.merge_with(&curve, MergeOperation::Add).unwrap();
assert_eq!(result.points.len(), 101);
assert!(result.points.iter().all(|p| p.x <= max_x));
assert_eq!(result.x_range, (Decimal::ZERO, max_x));
let last = result.points.last().unwrap();
assert!((last.y - dec!(2) * max_x).abs() < dec!(0.0000000001));
let mid = result
.interpolate(dec!(5), InterpolationType::Linear)
.unwrap();
assert!((mid.y - dec!(10)).abs() < dec!(0.0000000001));
}
#[test]
fn test_merge_curves_error_handling() {
let result = Curve::merge(&[], MergeOperation::Add);
assert!(result.is_err());
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
let curve2 = create_linear_curve(dec!(5.0), dec!(15.0), dec!(2.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Add);
assert!(result.is_ok());
}
#[test]
fn test_merge_multiple_curves() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
let curve3 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
let result = Curve::merge(&[&curve1, &curve2, &curve3], MergeOperation::Add).unwrap();
let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
for x in &test_points {
let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
+ curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y
+ curve3.interpolate(*x, InterpolationType::Cubic).unwrap().y;
let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
assert!(
(result_point.y - expected_y).abs() < dec!(0.001),
"Failed at x = {}, expected {}, got {}",
x,
expected_y,
result_point.y
);
}
}
}
#[cfg(test)]
mod tests_extended {
use super::*;
use crate::error::CurveError::OperationError;
use crate::geometrics::{ConstructionMethod, ConstructionParams};
use optionstratlib_core::error::OperationErrorKind;
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
struct GeneratorFailure(&'static str);
#[test]
fn test_construct_from_data_empty() {
let result = Curve::construct(ConstructionMethod::FromData {
points: BTreeSet::new(),
});
assert!(result.is_err());
let error = result.unwrap_err();
match error {
CurveError::Point2DError { reason } => {
assert_eq!(reason, "Empty points array");
}
_ => {
panic!("Unexpected error type");
}
}
}
#[test]
fn test_construct_parametric_valid() {
let f = |t: Decimal| Ok(Point2D::new(t, t * dec!(2.0)));
let params = ConstructionParams::D2 {
t_start: Decimal::ZERO,
t_end: dec!(10.0),
steps: 10,
};
let result = Curve::construct(ConstructionMethod::Parametric {
f: Box::new(f),
params,
});
assert!(result.is_ok());
}
#[test]
fn test_construct_parametric_invalid_function() {
let f = |_t: Decimal| -> Result<Point2D, CurveError> {
Err(CurveError::generator(GeneratorFailure(
"Function evaluation failed",
)))
};
let params = ConstructionParams::D2 {
t_start: Decimal::ZERO,
t_end: dec!(10.0),
steps: 10,
};
let result = Curve::construct(ConstructionMethod::Parametric {
f: Box::new(f),
params,
});
assert!(result.is_err());
let error = result.unwrap_err();
match error {
CurveError::Generator(source) => {
let cause = source
.downcast_ref::<GeneratorFailure>()
.expect("the generator's own error must survive boxing");
assert!(cause.to_string().contains("Function evaluation failed"));
}
_ => {
panic!("Unexpected error type");
}
}
}
#[test]
fn test_segment_not_found_error() {
let segment: Option<Point2D> = None;
let result: Result<Point2D, CurveError> = segment.ok_or_else(|| {
CurveError::ConstructionError(
"Could not find valid segment for interpolation".to_string(),
)
});
assert!(result.is_err());
let error = result.unwrap_err();
match error {
CurveError::ConstructionError(reason) => {
assert_eq!(reason, "Could not find valid segment for interpolation");
}
_ => {
panic!("Unexpected error type");
}
}
}
#[test]
fn test_compute_basic_metrics_placeholder() {
let curve = Curve {
points: BTreeSet::new(),
x_range: (Default::default(), Default::default()),
};
let metrics = curve.compute_basic_metrics();
assert!(metrics.is_ok());
let metrics = metrics.unwrap();
assert_eq!(metrics.mean, Decimal::ZERO);
}
#[test]
fn test_single_curve_return() {
let curve = Curve {
points: BTreeSet::new(),
x_range: (Default::default(), Default::default()),
};
let result = if vec![curve.clone()].len() == 1 {
Ok(curve.clone())
} else {
Err(CurveError::invalid_parameters(
"merge_curves",
"Invalid state",
))
};
assert!(result.is_ok());
}
#[test]
fn test_merge_curves_invalid_x_range() {
let min_x = dec!(10.0);
let max_x = dec!(5.0);
let result = if min_x >= max_x {
Err(CurveError::invalid_parameters(
"merge_curves",
"Curves have incompatible x-ranges",
))
} else {
Ok(())
};
assert!(result.is_err());
let error = result.unwrap_err();
match error {
OperationError(OperationErrorKind::InvalidParameters { operation, reason }) => {
assert_eq!(operation, "merge_curves");
assert_eq!(reason, "Curves have incompatible x-ranges");
}
_ => {
panic!("Unexpected error type");
}
}
}
}
#[cfg(test)]
mod tests_curve_metrics {
use super::*;
use optionstratlib_core::assert_decimal_eq;
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
fn create_linear_curve() -> Curve {
let points = BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(2.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(6.0)),
Point2D::new(dec!(4.0), dec!(8.0)),
]);
Curve::new(points)
}
fn create_non_linear_curve() -> Curve {
Curve {
points: (0..=20)
.map(|x| Point2D {
x: Decimal::from(x),
y: Decimal::from(x * x % 7), })
.collect(),
x_range: (Default::default(), Default::default()),
}
}
fn create_constant_curve() -> Curve {
let points = BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(5.0)),
Point2D::new(dec!(1.0), dec!(5.0)),
Point2D::new(dec!(2.0), dec!(5.0)),
]);
Curve::new(points)
}
#[test]
fn test_basic_metrics() {
let linear_curve = create_linear_curve();
let basic_metrics = linear_curve.compute_basic_metrics().unwrap();
assert_decimal_eq!(basic_metrics.mean, dec!(4.0), dec!(0.001));
assert_decimal_eq!(basic_metrics.median, dec!(4.0), dec!(0.001));
assert_decimal_eq!(basic_metrics.std_dev, dec!(2.82842712), dec!(0.001));
let constant_curve = create_constant_curve();
let constant_metrics = constant_curve.compute_basic_metrics().unwrap();
assert_decimal_eq!(constant_metrics.mean, dec!(5.0), dec!(0.001));
assert_decimal_eq!(constant_metrics.median, dec!(5.0), dec!(0.001));
assert_decimal_eq!(constant_metrics.std_dev, dec!(0.0), dec!(0.001));
}
#[test]
fn test_shape_metrics() {
let linear_curve = create_linear_curve();
let shape_metrics = linear_curve.compute_shape_metrics().unwrap();
assert!(
shape_metrics.skewness.abs() < dec!(0.5),
"Skewness for linear curve should be very close to 0, got {}",
shape_metrics.skewness
);
assert!(
shape_metrics.kurtosis.abs() < dec!(2.0),
"Kurtosis for linear curve should be close to 0, got {}",
shape_metrics.kurtosis
);
let non_linear_curve = create_non_linear_curve();
let non_linear_metrics = non_linear_curve.compute_shape_metrics().unwrap();
assert!(
non_linear_metrics.skewness.abs() > dec!(0.003),
"Non-linear curve should have significant skewness, got {}",
non_linear_metrics.skewness
);
assert!(
non_linear_metrics.kurtosis.abs() > dec!(1.0),
"Non-linear curve should have significant kurtosis, got {}",
non_linear_metrics.kurtosis
);
assert!(
!non_linear_metrics.peaks.is_empty(),
"Peaks should be detected"
);
assert!(
!non_linear_metrics.valleys.is_empty(),
"Valleys should be detected"
);
}
#[test]
fn test_range_metrics() {
let linear_curve = create_linear_curve();
let range_metrics = linear_curve.compute_range_metrics().unwrap();
assert_decimal_eq!(range_metrics.min.y, dec!(0.0), dec!(0.001));
assert_decimal_eq!(range_metrics.max.y, dec!(8.0), dec!(0.001));
assert_decimal_eq!(range_metrics.range, dec!(8.0), dec!(0.001));
let constant_curve = create_constant_curve();
let constant_range_metrics = constant_curve.compute_range_metrics().unwrap();
assert_decimal_eq!(constant_range_metrics.min.y, dec!(5.0), dec!(0.001));
assert_decimal_eq!(constant_range_metrics.max.y, dec!(5.0), dec!(0.001));
assert_decimal_eq!(constant_range_metrics.range, dec!(0.0), dec!(0.001));
}
#[test]
fn test_trend_metrics() {
let linear_curve = create_linear_curve();
let trend_metrics = linear_curve.compute_trend_metrics().unwrap();
assert_decimal_eq!(trend_metrics.slope, dec!(2.0), dec!(0.001));
assert_decimal_eq!(trend_metrics.intercept, dec!(0.0), dec!(0.001));
assert_decimal_eq!(trend_metrics.r_squared, dec!(1.0), dec!(0.001));
let non_linear_curve = create_non_linear_curve();
let non_linear_trend_metrics = non_linear_curve.compute_trend_metrics().unwrap();
assert!(non_linear_trend_metrics.r_squared < dec!(1.0));
assert!(!non_linear_trend_metrics.moving_average.is_empty());
}
#[test]
fn test_constant_curve_risk_metrics() {
let constant_curve = create_constant_curve();
let risk_metrics = constant_curve.compute_risk_metrics().unwrap();
assert_eq!(risk_metrics.volatility, dec!(0.0));
assert_eq!(risk_metrics.coefficient_of_variation, dec!(0.0));
assert_eq!(risk_metrics.sharpe_ratio, dec!(0.0));
}
#[test]
fn test_risk_metrics_flat_curve_keeps_deterministic_var() {
let curve = create_constant_curve();
let metrics = curve.compute_risk_metrics().unwrap();
assert_eq!(metrics.volatility, Decimal::ZERO);
assert_eq!(metrics.value_at_risk, dec!(5));
assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
assert_eq!(metrics.coefficient_of_variation, Decimal::ZERO);
assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
}
#[test]
fn test_risk_metrics_flat_negative_curve_keeps_deterministic_var() {
let points = BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(-2.5)),
Point2D::new(dec!(1.0), dec!(-2.5)),
Point2D::new(dec!(2.0), dec!(-2.5)),
]);
let metrics = Curve::new(points).compute_risk_metrics().unwrap();
assert_eq!(metrics.volatility, Decimal::ZERO);
assert_eq!(metrics.value_at_risk, dec!(-2.5));
assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
}
#[test]
fn test_risk_metrics() {
let linear_curve = create_linear_curve();
let risk_metrics = linear_curve.compute_risk_metrics().unwrap();
assert!(
risk_metrics.volatility > dec!(0.0),
"Volatility debe ser mayor a cero."
);
assert!(
risk_metrics.value_at_risk != dec!(0.0),
"Value at Risk no debe ser cero."
);
assert!(
risk_metrics.coefficient_of_variation != dec!(0.0),
"the coefficient of variation must not be zero"
);
}
#[test]
fn test_risk_metrics_bis() {
let linear_curve = create_linear_curve();
let risk_metrics = linear_curve.compute_risk_metrics().unwrap();
assert!(risk_metrics.volatility > dec!(0.0));
assert!(risk_metrics.value_at_risk != dec!(0.0));
assert!(risk_metrics.coefficient_of_variation != dec!(0.0));
let constant_curve = create_constant_curve();
let constant_risk_metrics = constant_curve.compute_risk_metrics().unwrap();
assert_decimal_eq!(constant_risk_metrics.volatility, dec!(0.0), dec!(0.001));
}
#[test]
fn test_edge_cases() {
let empty_curve = Curve::new(BTreeSet::new());
assert!(empty_curve.compute_basic_metrics().is_ok());
assert!(empty_curve.compute_shape_metrics().is_ok());
assert!(empty_curve.compute_range_metrics().is_ok());
assert!(empty_curve.compute_trend_metrics().is_ok());
assert!(empty_curve.compute_risk_metrics().is_ok());
let single_point_curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
dec!(1.0),
dec!(1.0),
)]));
assert!(single_point_curve.compute_basic_metrics().is_ok());
assert!(single_point_curve.compute_shape_metrics().is_ok());
assert!(single_point_curve.compute_range_metrics().is_ok());
assert!(single_point_curve.compute_trend_metrics().is_ok());
assert!(single_point_curve.compute_risk_metrics().is_ok());
}
}
#[cfg(test)]
mod tests_merge_axis_interpolate {
use super::*;
use crate::curves::utils::create_linear_curve;
use crate::geometrics::InterpolationType;
use rust_decimal_macros::dec;
#[test]
fn test_merge_axis_interpolate_linear() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(0.5)).unwrap();
let curve2 = create_linear_curve(dec!(4.0), dec!(20.0), dec!(1.0)).unwrap();
let result = curve1.merge_axis_interpolate(&curve2, InterpolationType::Linear);
assert!(result.is_ok());
let (interpolated_curve1, interpolated_curve2) = result.unwrap();
assert_eq!(interpolated_curve1.x_range.0, interpolated_curve2.x_range.0);
assert_eq!(interpolated_curve1.x_range.1, interpolated_curve2.x_range.1);
assert_eq!(interpolated_curve1.points.len(), 10);
assert_eq!(interpolated_curve2.points.len(), 10);
assert_eq!(interpolated_curve1.x_range, interpolated_curve2.x_range);
assert_eq!(
interpolated_curve1.get_index_values(),
interpolated_curve2.get_index_values()
);
}
#[test]
fn test_merge_axis_interpolate_cubic() {
let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(0.5)).unwrap();
let curve2 = create_linear_curve(dec!(4.0), dec!(20.0), dec!(1.0)).unwrap();
let result = curve1.merge_axis_interpolate(&curve2, InterpolationType::Cubic);
assert!(result.is_ok());
let (interpolated_curve1, interpolated_curve2) = result.unwrap();
assert_eq!(interpolated_curve1.x_range.0, interpolated_curve2.x_range.0);
assert_eq!(interpolated_curve1.x_range.1, interpolated_curve2.x_range.1);
assert_eq!(interpolated_curve1.points.len(), 10);
assert_eq!(interpolated_curve2.points.len(), 10);
assert_eq!(interpolated_curve1.x_range, interpolated_curve2.x_range);
assert_eq!(
interpolated_curve1.get_index_values(),
interpolated_curve2.get_index_values()
);
}
}
#[cfg(test)]
mod tests_geometric_transformations {
use super::*;
use rust_decimal_macros::dec;
fn create_test_curve() -> Curve {
Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]))
}
mod test_translate {
use super::*;
#[test]
fn test_translate_positive() {
let curve = create_test_curve();
let result = curve.translate(vec![&dec!(2.0), &dec!(3.0)]).unwrap();
let translated_points: Vec<_> = result.points.iter().collect();
assert_eq!(translated_points[0].x, dec!(2.0));
assert_eq!(translated_points[0].y, dec!(3.0));
assert_eq!(translated_points[1].x, dec!(3.0));
assert_eq!(translated_points[1].y, dec!(4.0));
assert_eq!(translated_points[2].x, dec!(4.0));
assert_eq!(translated_points[2].y, dec!(7.0));
assert_eq!(translated_points[3].x, dec!(5.0));
assert_eq!(translated_points[3].y, dec!(12.0));
}
#[test]
fn test_translate_negative() {
let curve = create_test_curve();
let result = curve.translate(vec![&dec!(-1.0), &dec!(-2.0)]).unwrap();
let translated_points: Vec<_> = result.points.iter().collect();
assert_eq!(translated_points[0].x, dec!(-1.0));
assert_eq!(translated_points[0].y, dec!(-2.0));
}
#[test]
fn test_translate_zero() {
let curve = create_test_curve();
let result = curve.translate(vec![&dec!(0.0), &dec!(0.0)]).unwrap();
assert_eq!(curve.points, result.points);
}
#[test]
fn test_translate_wrong_dimensions() {
let curve = create_test_curve();
let result = curve.translate(vec![&dec!(1.0)]);
assert!(result.is_err());
}
#[test]
fn test_translate_preserves_shape() {
let curve = create_test_curve();
let result = curve.translate(vec![&dec!(1.0), &dec!(1.0)]).unwrap();
let original_diffs: Vec<Decimal> = curve
.points
.iter()
.zip(curve.points.iter().skip(1))
.map(|(a, b)| b.y - a.y)
.collect();
let translated_diffs: Vec<Decimal> = result
.points
.iter()
.zip(result.points.iter().skip(1))
.map(|(a, b)| b.y - a.y)
.collect();
assert_eq!(original_diffs, translated_diffs);
}
}
mod test_scale {
use super::*;
#[test]
fn test_scale_uniform() {
let curve = create_test_curve();
let result = curve.scale(vec![&dec!(2.0), &dec!(2.0)]).unwrap();
let scaled_points: Vec<_> = result.points.iter().collect();
assert_eq!(scaled_points[0].x, dec!(0.0));
assert_eq!(scaled_points[0].y, dec!(0.0));
assert_eq!(scaled_points[1].x, dec!(2.0));
assert_eq!(scaled_points[1].y, dec!(2.0));
assert_eq!(scaled_points[2].x, dec!(4.0));
assert_eq!(scaled_points[2].y, dec!(8.0));
assert_eq!(scaled_points[3].x, dec!(6.0));
assert_eq!(scaled_points[3].y, dec!(18.0));
}
#[test]
fn test_scale_non_uniform() {
let curve = create_test_curve();
let result = curve.scale(vec![&dec!(2.0), &dec!(3.0)]).unwrap();
let scaled_points: Vec<_> = result.points.iter().collect();
assert_eq!(scaled_points[1].x, dec!(2.0));
assert_eq!(scaled_points[1].y, dec!(3.0));
}
#[test]
fn test_scale_zero() {
let curve = create_test_curve();
let result = curve.scale(vec![&dec!(0.0), &dec!(0.0)]).unwrap();
assert!(
result
.points
.iter()
.all(|p| p.x == dec!(0.0) && p.y == dec!(0.0))
);
}
#[test]
fn test_scale_wrong_dimensions() {
let curve = create_test_curve();
let result = curve.scale(vec![&dec!(2.0)]);
assert!(result.is_err());
}
#[test]
fn test_scale_negative() {
let curve = create_test_curve();
let result = curve.scale(vec![&dec!(-1.0), &dec!(-1.0)]).unwrap();
assert_eq!(result[1].x, dec!(-2.0));
assert_eq!(result[1].y, dec!(-4.0));
assert_eq!(result[3].x, dec!(0.0));
assert_eq!(result[3].y, dec!(0.0));
}
}
mod test_intersect_with {
use super::*;
#[test]
fn test_curves_intersect() {
let curve1 = create_test_curve();
let curve2 = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(2.0)),
]));
let intersections = curve1.intersect_with(&curve2).unwrap();
assert_eq!(intersections.len(), 1);
}
#[test]
fn test_no_intersection() {
let curve1 = create_test_curve();
let curve2 = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(10.0), dec!(10.0)),
Point2D::new(dec!(11.0), dec!(11.0)),
]));
let intersections = curve1.intersect_with(&curve2).unwrap();
assert!(intersections.is_empty());
}
#[test]
fn test_multiple_intersections() {
let curve1 = create_test_curve();
let curve2 = create_test_curve();
let intersections = curve1.intersect_with(&curve2).unwrap();
assert_eq!(intersections.len(), curve1.points.len());
}
#[test]
fn test_self_intersection() {
let curve = create_test_curve();
let intersections = curve.intersect_with(&curve).unwrap();
assert_eq!(intersections.len(), curve.points.len());
}
#[test]
fn test_empty_curves() {
let curve1 = Curve::new(BTreeSet::new());
let curve2 = Curve::new(BTreeSet::new());
let intersections = curve1.intersect_with(&curve2).unwrap();
assert!(intersections.is_empty());
}
}
mod test_derivative_at {
use super::*;
#[test]
fn test_linear_derivative() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let derivative = curve
.derivative_at(&Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(derivative[0], dec!(1.0));
}
#[test]
fn test_quadratic_derivative() {
let curve = create_test_curve();
let derivative = curve
.derivative_at(&Point2D::new(dec!(1.0), dec!(1.0)))
.unwrap();
assert_eq!(derivative[0], dec!(2.0));
let derivative2 = curve
.derivative_at(&Point2D::new(dec!(2.0), dec!(4.0)))
.unwrap();
assert_eq!(derivative2[0], dec!(4.0));
}
#[test]
fn test_out_of_range() {
let curve = create_test_curve();
let result = curve.derivative_at(&Point2D::new(dec!(10.0), dec!(0.0)));
assert!(result.is_err());
}
#[test]
fn test_at_endpoint() {
let curve = create_test_curve();
let derivative = curve
.derivative_at(&Point2D::new(dec!(0.0), dec!(0.0)))
.unwrap();
assert!(derivative[0] == dec!(0.0));
}
#[test]
fn test_vertical_line() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(1.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let result = curve.derivative_at(&Point2D::new(dec!(1.0), dec!(0.5)));
assert!(result.is_err());
}
}
mod test_extrema {
use super::*;
#[test]
fn test_find_extrema() {
let curve = create_test_curve();
let (min, max) = curve.extrema().unwrap();
assert_eq!(min.y, dec!(0.0));
assert_eq!(max.y, dec!(9.0));
}
#[test]
fn test_empty_curve() {
let curve = Curve::new(BTreeSet::new());
let result = curve.extrema();
assert!(result.is_err());
}
#[test]
fn test_single_point() {
let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
dec!(1.0),
dec!(1.0),
)]));
let (min, max) = curve.extrema().unwrap();
assert_eq!(min, max);
}
#[test]
fn test_flat_curve() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let (min, max) = curve.extrema().unwrap();
assert_eq!(min.y, max.y);
}
#[test]
fn test_multiple_extrema() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(0.0)),
]));
let (min, max) = curve.extrema().unwrap();
assert_eq!(min.y, dec!(0.0));
assert_eq!(max.y, dec!(1.0));
}
}
mod test_measure_under {
use super::*;
#[test]
fn test_area_under_linear() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let area = curve.measure_under(&dec!(0.0)).unwrap();
assert_eq!(area, dec!(0.5));
}
#[test]
fn test_area_empty_curve() {
let curve = Curve::new(BTreeSet::new());
let area = curve.measure_under(&dec!(0.0)).unwrap();
assert_eq!(area, dec!(0.0));
}
#[test]
fn test_area_single_point() {
let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
dec!(1.0),
dec!(1.0),
)]));
let area = curve.measure_under(&dec!(0.0)).unwrap();
assert_eq!(area, dec!(0.0));
}
#[test]
fn test_area_with_base_value() {
let curve = create_test_curve();
let area1 = curve.measure_under(&dec!(0.0)).unwrap();
let area2 = curve.measure_under(&dec!(1.0)).unwrap();
assert!(area1 > area2);
}
#[test]
fn test_negative_area() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(-1.0)),
Point2D::new(dec!(1.0), dec!(-2.0)),
]));
let area = curve.measure_under(&dec!(0.0)).unwrap();
assert!(area > dec!(0.0));
}
}
}
#[cfg(test)]
mod tests_curve_serde {
use super::*;
use rust_decimal_macros::dec;
fn create_test_curve() -> Curve {
let mut points = BTreeSet::new();
points.insert(Point2D {
x: dec!(1.0),
y: dec!(2.0),
});
points.insert(Point2D {
x: dec!(3.0),
y: dec!(4.0),
});
points.insert(Point2D {
x: dec!(5.0),
y: dec!(6.0),
});
Curve {
points,
x_range: (dec!(1.0), dec!(5.0)),
}
}
#[test]
fn test_basic_serialization() {
let curve = create_test_curve();
let serialized = serde_json::to_string(&curve).unwrap();
let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
assert_eq!(curve.points, deserialized.points);
assert_eq!(curve.x_range, deserialized.x_range);
}
#[test]
fn test_pretty_print() {
let curve = create_test_curve();
let serialized = serde_json::to_string_pretty(&curve).unwrap();
assert!(serialized.contains('\n'));
assert!(serialized.contains(" "));
let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
assert_eq!(curve.points, deserialized.points);
}
#[test]
fn test_empty_curve() {
let curve = Curve {
points: BTreeSet::new(),
x_range: (dec!(0.0), dec!(0.0)),
};
let serialized = serde_json::to_string(&curve).unwrap();
let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
assert!(deserialized.points.is_empty());
assert_eq!(deserialized.x_range, (dec!(0.0), dec!(0.0)));
}
#[test]
fn test_curve_with_negative_values() {
let mut points = BTreeSet::new();
points.insert(Point2D {
x: dec!(-1.0),
y: dec!(-2.0),
});
points.insert(Point2D {
x: dec!(-3.0),
y: dec!(-4.0),
});
let curve = Curve {
points,
x_range: (dec!(-3.0), dec!(-1.0)),
};
let serialized = serde_json::to_string(&curve).unwrap();
let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
assert_eq!(curve.points, deserialized.points);
assert_eq!(curve.x_range, deserialized.x_range);
}
#[test]
fn test_curve_with_high_precision() {
let mut points = BTreeSet::new();
points.insert(Point2D {
x: dec!(1.12345678901234567890),
y: dec!(2.12345678901234567890),
});
points.insert(Point2D {
x: dec!(3.12345678901234567890),
y: dec!(4.12345678901234567890),
});
let curve = Curve {
points,
x_range: (dec!(1.12345678901234567890), dec!(3.12345678901234567890)),
};
let serialized = serde_json::to_string(&curve).unwrap();
let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
assert_eq!(curve.points, deserialized.points);
assert_eq!(curve.x_range, deserialized.x_range);
}
#[test]
fn test_invalid_json() {
let json_str = r#"{"points": []}"#;
let result = serde_json::from_str::<Curve>(json_str);
assert!(result.is_err());
let json_str = r#"{"points": [1, 2, 3], "x_range": [0, 1]}"#;
let result = serde_json::from_str::<Curve>(json_str);
assert!(result.is_err());
let json_str = r#"{"points": [], "x_range": "invalid"}"#;
let result = serde_json::from_str::<Curve>(json_str);
assert!(result.is_err());
}
#[test]
fn test_json_structure() {
let curve = create_test_curve();
let serialized = serde_json::to_string(&curve).unwrap();
let json: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert!(json.is_object());
assert!(json.get("points").is_some());
assert!(json.get("x_range").is_some());
assert!(json.get("points").unwrap().is_array());
let x_range = json.get("x_range").unwrap().as_array().unwrap();
assert_eq!(x_range.len(), 2);
}
#[test]
fn test_multiple_curves() {
let curve1 = create_test_curve();
let mut curve2 = create_test_curve();
curve2.x_range = (dec!(6.0), dec!(10.0));
let curves = vec![curve1, curve2];
let serialized = serde_json::to_string(&curves).unwrap();
let deserialized: Vec<Curve> = serde_json::from_str(&serialized).unwrap();
assert_eq!(curves.len(), deserialized.len());
assert_eq!(curves[0].points, deserialized[0].points);
assert_eq!(curves[1].points, deserialized[1].points);
}
#[test]
fn test_ordering_preservation() {
let curve = create_test_curve();
let serialized = serde_json::to_string(&curve).unwrap();
let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
let original_points: Vec<_> = curve.points.into_iter().collect();
let deserialized_points: Vec<_> = deserialized.points.into_iter().collect();
assert_eq!(original_points, deserialized_points);
}
#[test]
fn test_curve_with_extremes() {
let mut points = BTreeSet::new();
points.insert(Point2D {
x: Decimal::MAX,
y: Decimal::MAX,
});
points.insert(Point2D {
x: Decimal::MIN,
y: Decimal::MIN,
});
let curve = Curve {
points,
x_range: (Decimal::MIN, Decimal::MAX),
};
let serialized = serde_json::to_string(&curve).unwrap();
let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
assert_eq!(curve.points, deserialized.points);
assert_eq!(curve.x_range, deserialized.x_range);
}
}
#[cfg(test)]
mod tests_curve_display_and_default {
use crate::curves::{Curve, Point2D};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
#[test]
fn test_curve_display() {
let point1 = Point2D::new(dec!(1.0), dec!(2.0));
let point2 = Point2D::new(dec!(3.0), dec!(4.0));
let mut points = BTreeSet::new();
points.insert(point1);
points.insert(point2);
let curve = Curve::new(points);
let display_string = format!("{curve}");
assert!(display_string.contains("(x: 1.0, y: 2.0)"));
assert!(display_string.contains("(x: 3.0, y: 4.0)"));
}
#[test]
fn test_curve_default() {
let curve = Curve::default();
assert!(curve.points.is_empty());
assert_eq!(curve.x_range, (Decimal::ZERO, Decimal::ZERO));
}
}
#[cfg(test)]
mod tests_curve_len_and_geometric {
use crate::curves::{Curve, Point2D};
use crate::error::CurveError;
use crate::geometrics::{ConstructionMethod, ConstructionParams, GeometricObject};
use optionstratlib_core::utils::Len;
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
#[test]
fn test_curve_len() {
let curve = Curve::default();
assert_eq!(curve.len(), 0);
assert!(curve.is_empty());
let mut points = BTreeSet::new();
points.insert(Point2D::new(dec!(1.0), dec!(2.0)));
let curve_with_point = Curve::new(points);
assert_eq!(curve_with_point.len(), 1);
assert!(!curve_with_point.is_empty());
}
#[test]
fn test_curve_get_points() {
let mut points = BTreeSet::new();
points.insert(Point2D::new(dec!(1.0), dec!(2.0)));
points.insert(Point2D::new(dec!(3.0), dec!(4.0)));
let curve = Curve::new(points);
let retrieved_points = curve.get_points();
assert_eq!(retrieved_points.len(), 2);
assert!(
retrieved_points
.iter()
.any(|p| p.x == dec!(1.0) && p.y == dec!(2.0))
);
assert!(
retrieved_points
.iter()
.any(|p| p.x == dec!(3.0) && p.y == dec!(4.0))
);
}
#[test]
fn test_construct_method_error() {
let result = Curve::construct(ConstructionMethod::Parametric {
f: Box::new(|_| Err(CurveError::generator(std::io::Error::other("Test error")))),
params: ConstructionParams::D2 {
t_start: dec!(0.0),
t_end: dec!(1.0),
steps: 10,
},
});
assert!(result.is_err());
match result {
Err(CurveError::Generator(source)) => {
assert!(source.to_string().contains("Test error"));
}
_ => panic!("Expected Generator"),
}
let result = Curve::construct(ConstructionMethod::Parametric {
f: Box::new(|t| Ok(Point2D::new(t, t * dec!(2.0)))),
params: ConstructionParams::D3 {
x_start: dec!(0.0),
x_end: dec!(1.0),
y_start: dec!(0.0),
y_end: dec!(1.0),
x_steps: 10,
y_steps: 10,
},
});
assert!(result.is_err());
match result {
Err(CurveError::ConstructionError(msg)) => {
assert_eq!(msg, "Invalid parameters");
}
_ => panic!("Expected ConstructionError"),
}
}
}
#[cfg(test)]
mod tests_interpolation_edge_cases {
use crate::curves::{Curve, Point2D};
use crate::geometrics::{AxisOperations, Interpolate, InterpolationType};
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
use tracing::info;
#[test]
fn test_cubic_interpolation_edge_cases() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let start_result = curve.interpolate(dec!(0.25), InterpolationType::Cubic);
assert!(start_result.is_ok());
let end_result = curve.interpolate(dec!(2.75), InterpolationType::Cubic);
assert!(end_result.is_ok());
}
#[test]
fn test_spline_interpolation_edge_cases() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
let exact_result = curve.interpolate(dec!(1.0), InterpolationType::Spline);
assert!(exact_result.is_ok());
let exact_point = exact_result.unwrap();
assert_eq!(exact_point.x, dec!(1.0));
assert_eq!(exact_point.y, dec!(1.0));
let mid_result = curve.interpolate(dec!(1.5), InterpolationType::Spline);
assert!(mid_result.is_ok());
}
#[test]
fn test_axis_operations() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
]));
let indices = curve.get_index_values();
assert_eq!(indices, vec![dec!(0.0), dec!(1.0), dec!(2.0)]);
let values = curve.get_values(dec!(1.0));
assert_eq!(values.len(), 1);
assert_eq!(*values[0], dec!(1.0));
let closest = curve.get_closest_point(&dec!(0.9)).unwrap();
assert_eq!(closest.x, dec!(1.0));
assert_eq!(closest.y, dec!(1.0));
info!("{:?}", curve);
let point = curve.get_point(&dec!(1.0));
assert!(point.is_some());
assert_eq!(point.unwrap().y, dec!(1.0));
let non_existent = curve.get_point(&dec!(1.5));
assert!(non_existent.is_none());
}
}
#[cfg(test)]
mod tests_axis_merge_and_transformations {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_merge_axis_index() {
let curve1 = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let curve2 = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(1.0), dec!(2.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
]));
let merged = curve1.merge_axis_index(&curve2);
assert_eq!(merged.len(), 1);
assert!(merged.contains(&dec!(1.0)));
}
#[test]
fn test_translate_with_negative_values() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let result = curve.translate(vec![&dec!(-2.0), &dec!(-3.0)]).unwrap();
assert_eq!(result.points.len(), 2);
let translated_points: Vec<_> = result.points.iter().collect();
assert_eq!(translated_points[0].x, dec!(-2.0));
assert_eq!(translated_points[0].y, dec!(-3.0));
assert_eq!(translated_points[1].x, dec!(-1.0));
assert_eq!(translated_points[1].y, dec!(-2.0));
}
#[test]
fn test_intersect_with_empty_curves() {
let curve1 = Curve::new(BTreeSet::new());
let curve2 = Curve::new(BTreeSet::new());
let result = curve1.intersect_with(&curve2).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_derivative_edge_cases() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let result = curve
.derivative_at(&Point2D::new(dec!(0.0), dec!(0.0)))
.unwrap();
assert_eq!(result[0], dec!(0.0));
let result = curve
.derivative_at(&Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(result[0], dec!(1.0));
}
#[test]
fn test_derivative_vertical_line() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(1.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let result = curve.derivative_at(&Point2D::new(dec!(1.0), dec!(0.5)));
assert!(result.is_err());
}
#[test]
fn test_extrema_single_point() {
let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
dec!(1.0),
dec!(1.0),
)]));
let (min, max) = curve.extrema().unwrap();
assert_eq!(min, max);
assert_eq!(min.x, dec!(1.0));
assert_eq!(min.y, dec!(1.0));
}
#[test]
fn test_extrema_flat_curve() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]));
let (min, max) = curve.extrema().unwrap();
assert_eq!(min.y, max.y);
assert_eq!(min.y, dec!(1.0));
}
#[test]
fn test_measure_under_single_point() {
let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
dec!(1.0),
dec!(1.0),
)]));
let area = curve.measure_under(&dec!(0.0)).unwrap();
assert_eq!(area, dec!(0.0));
}
#[test]
fn test_measure_under_negative_area() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(-1.0)),
Point2D::new(dec!(1.0), dec!(-2.0)),
]));
let area = curve.measure_under(&dec!(0.0)).unwrap();
assert!(area > dec!(0.0));
assert_eq!(area, dec!(1.5)); }
#[test]
fn test_extrema_multiple_extrema() {
let curve = Curve::new(BTreeSet::from_iter(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!(3.0)),
]));
let (min, max) = curve.extrema().unwrap();
assert_eq!(min.y, dec!(0.0));
assert_eq!(max.y, dec!(3.0));
}
}
#[cfg(test)]
mod tests_duplicate_abscissa {
use crate::curves::{Curve, Point2D};
use crate::error::InterpolationError;
use crate::geometrics::{AxisOperations, Interpolate, InterpolationType};
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
fn stacked_curve() -> Curve {
Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(3.0)),
Point2D::new(dec!(2.0), dec!(2.0)),
Point2D::new(dec!(3.0), dec!(5.0)),
]))
}
#[test]
fn test_curve_new_stacked_abscissa_keeps_both_points() {
let curve = stacked_curve();
assert_eq!(curve.points.len(), 5);
assert_eq!(curve.x_range, (dec!(0.0), dec!(3.0)));
}
#[test]
fn test_curve_get_values_stacked_abscissa_returns_every_ordinate() {
let curve = stacked_curve();
let values: Vec<_> = curve.get_values(dec!(1.0)).into_iter().copied().collect();
assert_eq!(values, vec![dec!(1.0), dec!(3.0)]);
}
#[test]
fn test_curve_get_point_stacked_abscissa_returns_lowest_ordinate() {
let curve = stacked_curve();
let point = curve.get_point(&dec!(1.0)).expect("x = 1 is present");
assert_eq!(*point, Point2D::new(dec!(1.0), dec!(1.0)));
assert!(curve.contains_point(&dec!(1.0)));
}
#[test]
fn test_curve_linear_interpolate_stacked_abscissa_is_degenerate() {
let curve = stacked_curve();
assert!(matches!(
curve.interpolate(dec!(1.0), InterpolationType::Linear),
Err(InterpolationError::DegenerateInterval)
));
}
#[test]
fn test_curve_cubic_interpolate_stacked_abscissa_is_degenerate() {
let curve = stacked_curve();
assert!(matches!(
curve.interpolate(dec!(1.0), InterpolationType::Cubic),
Err(InterpolationError::DegenerateInterval)
));
}
#[test]
fn test_curve_spline_interpolate_stacked_abscissa_is_degenerate() {
let curve = stacked_curve();
assert!(matches!(
curve.interpolate(dec!(1.0), InterpolationType::Spline),
Err(InterpolationError::DegenerateInterval)
));
}
#[test]
fn test_curve_linear_interpolate_away_from_stack_reads_its_own_side() {
let curve = stacked_curve();
let left = curve
.interpolate(dec!(0.5), InterpolationType::Linear)
.expect("0.5 brackets between (0, 0) and the lowest ordinate at x = 1");
assert_eq!(left, Point2D::new(dec!(0.5), dec!(0.5)));
let right = curve
.interpolate(dec!(1.5), InterpolationType::Linear)
.expect("1.5 brackets between the highest ordinate at x = 1 and (2, 2)");
assert_eq!(right, Point2D::new(dec!(1.5), dec!(2.5)));
}
#[test]
fn test_curve_interpolate_single_ordinate_returns_the_stored_point() {
let curve = Curve::new(BTreeSet::from_iter(vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(2.0), dec!(4.0)),
Point2D::new(dec!(3.0), dec!(9.0)),
]));
for interpolation in [
InterpolationType::Linear,
InterpolationType::Cubic,
InterpolationType::Spline,
] {
let point = curve
.interpolate(dec!(2.0), interpolation)
.expect("x = 2 is a stored abscissa");
assert_eq!(point, Point2D::new(dec!(2.0), dec!(4.0)));
}
}
}
#[cfg(test)]
mod tests_coefficient_of_variation {
use super::*;
use crate::error::MetricsError;
use crate::geometrics::MetricsExtractor;
fn line(values: impl IntoIterator<Item = i64>) -> Curve {
Curve::new(
values
.into_iter()
.enumerate()
.map(|(x, y)| Point2D::new(Decimal::from(x), Decimal::from(y)))
.collect(),
)
}
#[test]
fn test_risk_metrics_coefficient_of_variation_is_std_dev_over_mean() {
let curve = line(1..=5);
let metrics = curve.compute_risk_metrics().unwrap();
assert_eq!(
metrics.coefficient_of_variation,
dec!(0.4714045207910316829338962414)
);
let basic = curve.compute_basic_metrics().unwrap();
assert_eq!(
metrics.coefficient_of_variation,
d_div(basic.std_dev, basic.mean, "test").unwrap()
);
}
#[test]
fn test_risk_metrics_coefficient_of_variation_follows_the_sign_of_the_mean() {
let metrics = line((1..=5).map(|y| -y)).compute_risk_metrics().unwrap();
assert_eq!(
metrics.coefficient_of_variation,
dec!(-0.4714045207910316829338962414)
);
}
#[test]
fn test_risk_metrics_zero_mean_is_an_error() {
assert!(matches!(
line([-1, 0, 1]).compute_risk_metrics(),
Err(MetricsError::ZeroMean {
metric: "coefficient of variation"
})
));
}
}
#[cfg(test)]
mod tests_risk_volatility {
use super::*;
use crate::geometrics::MetricsExtractor;
fn line(values: impl IntoIterator<Item = i64>) -> Curve {
Curve::new(
values
.into_iter()
.enumerate()
.map(|(x, y)| Point2D::new(Decimal::from(x), Decimal::from(y)))
.collect(),
)
}
#[test]
fn test_risk_metrics_volatility_is_the_population_std_dev() {
let metrics = line(1..=5).compute_risk_metrics().unwrap();
assert_eq!(metrics.volatility, dec!(1.4142135623730950488016887242));
assert_eq!(metrics.value_at_risk, dec!(0.6736186898962586447212220487));
assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
assert_eq!(
metrics.coefficient_of_variation,
dec!(0.4714045207910316829338962414)
);
assert_eq!(metrics.sharpe_ratio, dec!(2.1213203435596425732025330863));
}
#[test]
fn test_risk_metrics_expected_shortfall_sees_the_tail() {
let metrics = line([1, 10, 10, 10, 10, 10, 10, 10, 10, 10])
.compute_risk_metrics()
.unwrap();
assert_eq!(metrics.volatility, dec!(2.7));
assert_eq!(metrics.value_at_risk, dec!(4.6585));
assert_eq!(metrics.expected_shortfall, dec!(1));
assert_eq!(metrics.sharpe_ratio, dec!(3.3703703703703703703703703704));
}
#[test]
fn test_risk_metrics_volatility_matches_basic_std_dev() {
let curve = Curve::new(
(0..=20i64)
.map(|x| Point2D::new(Decimal::from(x), Decimal::from(x * x % 7)))
.collect(),
);
assert_eq!(
curve.compute_risk_metrics().unwrap().volatility,
curve.compute_basic_metrics().unwrap().std_dev
);
}
}
#[cfg(test)]
mod tests_bracket_matches_scan {
use super::*;
fn scan_bracket(curve: &Curve, x: Decimal) -> Result<(usize, usize), InterpolationError> {
let points: Vec<&Point2D> = curve.points.iter().collect();
if points.len() < 2 {
return Err(InterpolationError::EmptyData);
}
let (first, last) = (points[0], points[points.len() - 1]);
if x < first.x || x > last.x {
return Err(InterpolationError::OutOfRange {
target: x.to_string(),
});
}
for (i, pair) in points.windows(2).enumerate() {
if pair[0].x <= x && x <= pair[1].x {
return Ok((i, i + 1));
}
}
Err(InterpolationError::DegenerateInterval)
}
fn scan_bilinear_cell(curve: &Curve, i: usize) -> [usize; 4] {
let far = (i + 2).min(curve.len() - 2);
[i, i + 1, far, far + 1]
}
fn scan_cubic_window(curve: &Curve, i: usize) -> [usize; 4] {
let len = curve.len();
let first = if i == 0 {
0
} else if i == len - 2 {
len - 4
} else {
i - 1
};
[first, first + 1, first + 2, first + 3]
}
fn nth(curve: &Curve, index: usize) -> &Point2D {
curve.points.iter().nth(index).unwrap()
}
fn same_points(curve: &Curve, got: [&Point2D; 4], want: [usize; 4]) -> bool {
got.iter()
.zip(want)
.all(|(point, index)| *point == nth(curve, index))
}
fn check(curve: &Curve, x: Decimal) {
let scanned = scan_bracket(curve, x);
let fast = curve.find_bracket_points(x);
assert_eq!(
format!("{fast:?}"),
format!("{scanned:?}"),
"find_bracket_points at {x} on {curve:?}"
);
let pair = curve.bracket(x);
match &scanned {
Ok((i, j)) => {
let (left, right) = pair.unwrap();
assert_eq!((left, right), (nth(curve, *i), nth(curve, *j)));
if curve.len() >= 4 {
let cell = curve.bilinear_cell(x).unwrap();
assert!(same_points(curve, cell, scan_bilinear_cell(curve, *i)));
let window = curve.cubic_window(x).unwrap();
assert!(same_points(curve, window, scan_cubic_window(curve, *i)));
}
}
Err(e) => assert_eq!(format!("{:?}", pair.unwrap_err()), format!("{e:?}")),
}
let filtered: Vec<&Point2D> = curve.points.iter().filter(|p| p.x == x).collect();
assert_eq!(curve.points_at(x).collect::<Vec<_>>(), filtered);
assert_eq!(curve.contains_point(&x), !filtered.is_empty());
assert_eq!(curve.get_point(&x), filtered.first().copied());
assert_eq!(
curve.get_values(x),
filtered.iter().map(|p| &p.y).collect::<Vec<_>>()
);
let exact = match filtered[..] {
[] => Ok(None),
[only] => Ok(Some(*only)),
_ => Err(InterpolationError::DegenerateInterval),
};
assert_eq!(
format!("{:?}", curve.exact_point_at(x)),
format!("{exact:?}")
);
}
fn probes(curve: &Curve) -> Vec<Decimal> {
let mut xs = vec![dec!(-1000), dec!(1000)];
let abscissas: Vec<Decimal> = curve.points.iter().map(|p| p.x).collect();
for pair in abscissas.windows(2) {
xs.push((pair[0] + pair[1]) / dec!(2));
}
for x in abscissas {
xs.extend([x, x - dec!(0.001), x + dec!(0.001)]);
}
xs
}
fn curve_of(points: &[(i64, i64)]) -> Curve {
Curve::new(
points
.iter()
.map(|&(x, y)| Point2D::new(Decimal::from(x), Decimal::from(y)))
.collect(),
)
}
#[test]
fn edge_curves() {
let cases: Vec<Vec<(i64, i64)>> = vec![
vec![],
vec![(1, 1)],
vec![(1, 1), (1, 2)],
vec![(0, 0), (1, 1)],
vec![(0, 0), (1, 1), (2, 4), (3, 9)],
vec![(0, 0), (0, 5), (1, 1), (2, 4), (3, 9)],
vec![(0, 0), (1, 1), (1, 3), (1, 7), (2, 4), (3, 9)],
vec![(0, 0), (1, 1), (2, 4), (3, 9), (3, 10)],
vec![(0, 0), (0, 1), (0, 2), (0, 3)],
vec![(-3, 2), (-1, 0), (4, 4), (5, -1), (9, 3), (12, 0)],
];
for points in cases {
let curve = curve_of(&points);
for x in probes(&curve) {
check(&curve, x);
}
}
}
#[test]
fn decimal_scales_compare_by_value() {
let curve = Curve::new(
[
Point2D::new(dec!(0), dec!(0)),
Point2D::new(dec!(1.0), dec!(1)),
Point2D::new(dec!(2.00), dec!(4)),
Point2D::new(dec!(3), dec!(9)),
]
.into_iter()
.collect(),
);
for x in [
dec!(1),
dec!(1.000),
dec!(2),
dec!(2.0),
dec!(0.0),
dec!(3.00),
] {
check(&curve, x);
}
}
#[test]
fn random_curves_with_stacked_abscissas() {
let mut state: u64 = 0x2545_f491_4f6c_dd1d;
let mut next = |bound: u64| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state % bound
};
for _ in 0..2_000 {
let len = next(14) as usize;
let points: Vec<(i64, i64)> = (0..len)
.map(|_| (next(10) as i64 - 3, next(6) as i64 - 2))
.collect();
let curve = curve_of(&points);
for x in probes(&curve) {
check(&curve, x);
}
}
}
}
#[cfg(test)]
mod tests_spline_matches_legacy {
use super::*;
fn legacy_point_at(
curve: &Curve,
index: usize,
kind: fn(String) -> InterpolationError,
) -> Result<&Point2D, InterpolationError> {
curve.points.iter().nth(index).ok_or_else(|| {
kind(format!(
"point index {index} is out of bounds for a curve of {} points",
curve.points.len()
))
})
}
fn legacy_exact_point_at(
curve: &Curve,
x: Decimal,
) -> Result<Option<Point2D>, InterpolationError> {
let mut at_x = curve.points.iter().filter(|p| p.x == x);
let Some(point) = at_x.next() else {
return Ok(None);
};
if at_x.next().is_some() {
return Err(InterpolationError::DegenerateInterval);
}
Ok(Some(*point))
}
#[rustfmt::skip]
fn legacy_spline(curve: &Curve, x: Decimal) -> Result<Point2D, InterpolationError> {
let len = curve.len();
if len < 3 {
return Err(InterpolationError::Spline(
"Need at least three points for spline interpolation".to_string(),
));
}
let first = legacy_point_at(curve, 0, InterpolationError::Spline)?;
let spline = InterpolationError::Spline;
let last_index = shift_index(len, -1, spline)?;
let last = legacy_point_at(curve, last_index, spline)?;
if x < first.x || x > last.x {
return Err(InterpolationError::Spline(
"x is outside the range of points".to_string(),
));
}
if let Some(point) = legacy_exact_point_at(curve, x)? {
return Ok(point);
}
let n = len;
let pts: Vec<&Point2D> = curve.points.iter().collect();
let at = |index: usize| -> Result<&Point2D, InterpolationError> {
pts.get(index).copied().ok_or_else(|| {
InterpolationError::Spline(format!(
"point index {index} is out of bounds for a curve of {n} points"
))
})
};
let mut a = vec![Decimal::ZERO];
let mut b = vec![Decimal::ONE];
let mut c = vec![Decimal::ZERO];
let mut r = vec![Decimal::ZERO];
for i in 1..last_index {
let prev = at(shift_index(i, -1, spline)?)?;
let curr = at(i)?;
let next = at(shift_index(i, 1, spline)?)?;
let hi = d_sub(curr.x, prev.x, "Curve::spline_interpolate::hi")
.map_err(interp_err(InterpolationError::Spline))?;
let hi1 = d_sub(next.x, curr.x, "Curve::spline_interpolate::hi1")
.map_err(interp_err(InterpolationError::Spline))?;
if hi.is_zero() || hi1.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let band = d_add(hi, hi1, "Curve::spline_interpolate::band")
.map_err(interp_err(InterpolationError::Spline))?;
let diag = d_mul(dec!(2), band, "Curve::spline_interpolate::diag")
.map_err(interp_err(InterpolationError::Spline))?;
let rise_next = d_sub(next.y, curr.y, "Curve::spline_interpolate::rise_next")
.map_err(interp_err(InterpolationError::Spline))?;
let slope_next = d_div(rise_next, hi1, "Curve::spline_interpolate::slope_next")
.map_err(interp_err(InterpolationError::Spline))?;
let rise_prev = d_sub(curr.y, prev.y, "Curve::spline_interpolate::rise_prev")
.map_err(interp_err(InterpolationError::Spline))?;
let slope_prev = d_div(rise_prev, hi, "Curve::spline_interpolate::slope_prev")
.map_err(interp_err(InterpolationError::Spline))?;
let curvature = d_sub(
slope_next,
slope_prev,
"Curve::spline_interpolate::curvature",
)
.map_err(interp_err(InterpolationError::Spline))?;
let rhs = d_mul(dec!(6), curvature, "Curve::spline_interpolate::rhs")
.map_err(interp_err(InterpolationError::Spline))?;
a.push(hi);
b.push(diag);
c.push(hi1);
r.push(rhs);
}
a.push(Decimal::ZERO);
b.push(Decimal::ONE);
c.push(Decimal::ZERO);
r.push(Decimal::ZERO);
let mut m = vec![Decimal::ZERO; n];
for i in 1..last_index {
let prev = shift_index(i, -1, spline)?;
let a_i = band_at(&a, i, "a")?;
let b_prev = band_at(&b, prev, "b")?;
if b_prev.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let w = d_div(a_i, b_prev, "Curve::spline_interpolate::w")
.map_err(interp_err(InterpolationError::Spline))?;
let c_prev = band_at(&c, prev, "c")?;
let wc = d_mul(w, c_prev, "Curve::spline_interpolate::wc")
.map_err(interp_err(InterpolationError::Spline))?;
let b_i = band_at(&b, i, "b")?;
*band_at_mut(&mut b, i, "b")? = d_sub(b_i, wc, "Curve::spline_interpolate::b_i")
.map_err(interp_err(InterpolationError::Spline))?;
let r_prev = band_at(&r, prev, "r")?;
let wr = d_mul(w, r_prev, "Curve::spline_interpolate::wr")
.map_err(interp_err(InterpolationError::Spline))?;
let r_i = band_at(&r, i, "r")?;
*band_at_mut(&mut r, i, "r")? = d_sub(r_i, wr, "Curve::spline_interpolate::r_i")
.map_err(interp_err(InterpolationError::Spline))?;
}
let b_last = band_at(&b, last_index, "b")?;
if b_last.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let r_last = band_at(&r, last_index, "r")?;
*band_at_mut(&mut m, last_index, "m")? =
d_div(r_last, b_last, "Curve::spline_interpolate::m_last")
.map_err(interp_err(InterpolationError::Spline))?;
for i in (1..last_index).rev() {
let c_i = band_at(&c, i, "c")?;
let m_next = band_at(&m, shift_index(i, 1, spline)?, "m")?;
let cm = d_mul(c_i, m_next, "Curve::spline_interpolate::cm")
.map_err(interp_err(InterpolationError::Spline))?;
let r_i = band_at(&r, i, "r")?;
let numerator = d_sub(r_i, cm, "Curve::spline_interpolate::m_numerator")
.map_err(interp_err(InterpolationError::Spline))?;
let b_i = band_at(&b, i, "b")?;
if b_i.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
*band_at_mut(&mut m, i, "m")? = d_div(numerator, b_i, "Curve::spline_interpolate::m_i")
.map_err(interp_err(InterpolationError::Spline))?;
}
let mut segment = None;
for i in 0..last_index {
if at(i)?.x <= x && x <= at(shift_index(i, 1, spline)?)?.x {
segment = Some(i);
break;
}
}
let segment = segment.ok_or_else(|| {
InterpolationError::Spline("Could not find valid segment for interpolation".to_string())
})?;
let left = at(segment)?;
let right = at(shift_index(segment, 1, spline)?)?;
let h = d_sub(right.x, left.x, "Curve::spline_interpolate::h")
.map_err(interp_err(InterpolationError::Spline))?;
if h.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let dx = d_sub(right.x, x, "Curve::spline_interpolate::dx")
.map_err(interp_err(InterpolationError::Spline))?;
let dx1 = d_sub(x, left.x, "Curve::spline_interpolate::dx1")
.map_err(interp_err(InterpolationError::Spline))?;
let m_left = band_at(&m, segment, "m")?;
let m_right = band_at(&m, shift_index(segment, 1, spline)?, "m")?;
let six_h = d_mul(dec!(6), h, "Curve::spline_interpolate::six_h")
.map_err(interp_err(InterpolationError::Spline))?;
let left_cube = cube_scaled(m_left, dx, six_h, "Curve::spline_interpolate::left_cube")?;
let right_cube = cube_scaled(m_right, dx1, six_h, "Curve::spline_interpolate::right_cube")?;
let left_linear = linear_term(left.y, m_left, h, dx, "Curve::spline_interpolate::left")?;
let right_linear =
linear_term(right.y, m_right, h, dx1, "Curve::spline_interpolate::right")?;
let y = d_sum_iter(
[left_cube, right_cube, left_linear, right_linear],
"Curve::spline_interpolate::y",
)
.map_err(interp_err(InterpolationError::Spline))?;
Ok(Point2D::new(x, y))
}
fn check(curve: &Curve) {
let spline = CurveSpline::new(curve);
let mut xs = vec![dec!(-1000), dec!(1000), dec!(0.123456789)];
let knots: Vec<Decimal> = curve.points.iter().map(|p| p.x).collect();
for pair in knots.windows(2) {
xs.push((pair[0] + pair[1]) / dec!(2));
xs.push(pair[0] + (pair[1] - pair[0]) / dec!(3));
}
for x in knots {
xs.extend([x, x - dec!(0.001), x + dec!(0.001)]);
}
for x in xs {
let legacy = format!("{:?}", legacy_spline(curve, x));
assert_eq!(
format!("{:?}", curve.spline_interpolate(x)),
legacy,
"spline_interpolate at {x} on {curve:?}"
);
assert_eq!(
format!("{:?}", spline.interpolate(x)),
legacy,
"CurveSpline at {x} on {curve:?}"
);
}
}
fn curve_of(points: &[(Decimal, Decimal)]) -> Curve {
Curve::new(points.iter().map(|&(x, y)| Point2D::new(x, y)).collect())
}
#[test]
fn edge_curves() {
let d = |v: i64| Decimal::from(v);
let cases: Vec<Vec<(Decimal, Decimal)>> = vec![
vec![],
vec![(d(1), d(1))],
vec![(d(0), d(0)), (d(1), d(1))],
vec![(d(0), d(0)), (d(1), d(1)), (d(2), d(4))],
vec![(d(0), d(0)), (d(1), d(1)), (d(1), d(3)), (d(2), d(4))],
vec![(d(0), d(0)), (d(0), d(2)), (d(1), d(1)), (d(2), d(4))],
vec![(d(0), d(0)), (d(1), d(1)), (d(2), d(4)), (d(2), d(5))],
(0..9)
.map(|i| {
let x = dec!(10) / dec!(7) * d(i);
(x, (x - dec!(5)) / dec!(7) * (x - dec!(5)) / dec!(3))
})
.collect(),
vec![
(d(-3), d(2)),
(d(-1), d(0)),
(dec!(-0.999), d(1)),
(d(4), d(4)),
(d(9), d(-3)),
],
];
for points in cases {
check(&curve_of(&points));
}
}
#[test]
fn overflowing_solve_reports_the_same_error() {
let curve = curve_of(&[
(dec!(0), dec!(0)),
(dec!(0.0000000000000000000000000001), Decimal::MAX),
(dec!(1), Decimal::MIN),
(dec!(2), dec!(1)),
]);
assert!(legacy_spline(&curve, dec!(0.5)).is_err());
check(&curve);
}
#[test]
fn random_curves() {
let mut state: u64 = 0xd1b5_4a32_d192_ed03;
let mut next = |bound: u64| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state % bound
};
for _ in 0..300 {
let len = next(16) as usize;
let points: Vec<(Decimal, Decimal)> = (0..len)
.map(|_| {
(
Decimal::new(next(400) as i64 - 100, 1),
Decimal::new(next(20_000) as i64 - 10_000, 3),
)
})
.collect();
check(&curve_of(&points));
}
}
#[test]
fn merge_resampling_matches_per_read_splines() {
let a = curve_of(
&(0..12)
.map(|i| (Decimal::from(i), Decimal::from(i * i % 7)))
.collect::<Vec<_>>(),
);
let b = curve_of(
&(0..9)
.map(|i| {
(
dec!(0.5) + Decimal::from(i) * dec!(1.3),
Decimal::from(i % 4),
)
})
.collect::<Vec<_>>(),
);
let (ra, rb) = a
.merge_axis_interpolate(&b, InterpolationType::Spline)
.unwrap();
for (resampled, source) in [(&ra, &a), (&rb, &b)] {
for p in &resampled.points {
let want = match source.get_point(&p.x) {
Some(point) => *point,
None => legacy_spline(source, p.x).unwrap(),
};
assert_eq!(*p, want);
}
}
}
}
#[cfg(test)]
mod tests_intersect_matches_all_pairs {
use super::*;
fn check(a: &Curve, b: &Curve) {
for (left, right) in [(a, b), (b, a), (a, a)] {
let fast = left.intersect_with(right);
let all = intersect_all_pairs(left, right, Decimal::new(1, 6));
assert_eq!(
format!("{fast:?}"),
format!("{all:?}"),
"{left:?} with {right:?}"
);
}
}
fn curve_of(points: &[(Decimal, Decimal)]) -> Curve {
Curve::new(points.iter().map(|&(x, y)| Point2D::new(x, y)).collect())
}
#[test]
fn samples_within_tolerance_are_matched_in_pair_order() {
let tiny = dec!(0.0000005);
let a = curve_of(&[
(dec!(0), dec!(1)),
(dec!(1), dec!(2)),
(dec!(1), dec!(5)),
(dec!(2), dec!(3)),
]);
let b = curve_of(&[
(dec!(0) + tiny, dec!(1) - tiny),
(dec!(1) - tiny, dec!(2)),
(dec!(1), dec!(2) + tiny),
(dec!(1) + dec!(0.000001), dec!(5)),
(dec!(2.000001), dec!(3)),
]);
check(&a, &b);
let found = match a.intersect_with(&b) {
Ok(found) => found,
Err(e) => panic!("intersect_with failed: {e:?}"),
};
assert_eq!(
found,
vec![
Point2D::new(dec!(0), dec!(1)),
Point2D::new(dec!(1), dec!(2)),
Point2D::new(dec!(1), dec!(2)),
]
);
}
#[test]
fn crossings_between_samples_are_not_reported() {
let rising = curve_of(&[(dec!(0), dec!(0)), (dec!(2), dec!(2))]);
let falling = curve_of(&[(dec!(0), dec!(2)), (dec!(2), dec!(0))]);
assert!(matches!(rising.intersect_with(&falling), Ok(found) if found.is_empty()));
}
#[test]
fn empty_and_disjoint_curves() {
let empty = Curve::default();
let a = curve_of(&[(dec!(0), dec!(0)), (dec!(1), dec!(1))]);
let far = curve_of(&[(dec!(10), dec!(0)), (dec!(11), dec!(1))]);
check(&empty, &a);
check(&a, &far);
check(&empty, &empty);
}
#[test]
fn overflowing_abscissa_difference_reports_the_same_pair() {
let a = curve_of(&[
(dec!(-70000000000000000000000000000), dec!(0)),
(dec!(0), dec!(0)),
]);
let b = curve_of(&[
(dec!(0), dec!(0)),
(dec!(70000000000000000000000000000), dec!(1)),
]);
assert!(a.intersect_with(&b).is_err());
check(&a, &b);
let c = curve_of(&[
(dec!(0), dec!(-70000000000000000000000000000)),
(dec!(1), dec!(0)),
]);
let d = curve_of(&[
(dec!(0), dec!(70000000000000000000000000000)),
(dec!(1), dec!(0)),
]);
assert!(c.intersect_with(&d).is_err());
check(&c, &d);
}
#[test]
fn random_curves_with_stacks_and_near_misses() {
let mut state: u64 = 0xa076_1d64_78bd_642f;
let mut next = |bound: u64| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state % bound
};
let curve = |next: &mut dyn FnMut(u64) -> u64| {
let len = next(30) as usize;
let points: Vec<(Decimal, Decimal)> = (0..len)
.map(|_| {
(
Decimal::new(next(12) as i64, 7) * dec!(5),
Decimal::new(next(4) as i64, 7) * dec!(5),
)
})
.collect();
curve_of(&points)
};
for _ in 0..1_000 {
let a = curve(&mut next);
let b = curve(&mut next);
check(&a, &b);
}
}
}