fts_core/models/curve/
constant.rs

1use crate::models::Point;
2
3/// A representation of a flat demand curve supporting interval, half-line, and full-line trading domains
4///
5/// A constant curve represents a fixed price for trades within a specified rate interval.
6/// This can be used to express indifference to (potentially unbounded) trade rates at a specific price.
7///
8/// The sign convention follows flow trading standards:
9/// - min_rate ≤ 0 (non-positive): maximum selling rate
10/// - max_rate ≥ 0 (non-negative): maximum buying rate
11#[derive(Clone, Debug)]
12#[cfg_attr(
13    feature = "serde",
14    derive(serde::Serialize, serde::Deserialize),
15    serde(try_from = "ConstantCurveDto", into = "ConstantCurveDto")
16)]
17pub struct ConstantCurve {
18    min_rate: f64,
19    max_rate: f64,
20    price: f64,
21}
22
23impl ConstantCurve {
24    /// Creates a new constant constraint without validation
25    ///
26    /// # Safety
27    /// This function is unsafe because it bypasses validation of rates and price.
28    /// It should only be used when the caller can guarantee the values are valid.
29    pub unsafe fn new_unchecked(min_rate: f64, max_rate: f64, price: f64) -> Self {
30        Self {
31            min_rate: min_rate,
32            max_rate: max_rate,
33            price: price,
34        }
35    }
36
37    /// Creates a new constant curve with validation
38    pub fn new(
39        min_rate: Option<f64>,
40        max_rate: Option<f64>,
41        price: f64,
42    ) -> Result<Self, ConstantCurveError> {
43        let dto = ConstantCurveDto {
44            min_rate,
45            max_rate,
46            price,
47        };
48        Self::try_from(dto)
49    }
50
51    /// Return the domain of the demand curve (min and max rates)
52    pub fn domain(&self) -> (f64, f64) {
53        (self.min_rate, self.max_rate)
54    }
55
56    /// Returns the curve as a vector of points
57    ///
58    /// For a constant curve, this returns one or two points:
59    /// - If min_rate equals max_rate: returns a single point
60    /// - Otherwise: returns two points at the min and max rates, both with the same price
61    pub fn points(self) -> Vec<Point> {
62        let mut response = Vec::with_capacity(2);
63        response.push(Point {
64            rate: self.min_rate,
65            price: self.price,
66        });
67        if self.min_rate != self.max_rate {
68            response.push(Point {
69                rate: self.max_rate,
70                price: self.price,
71            });
72        }
73        response
74    }
75}
76
77/// A DTO to ensure that we always validate when we deserialize from an untrusted source
78#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(inline))]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80#[derive(Debug)]
81pub struct ConstantCurveDto {
82    /// The minimum rate (nonpositive), defaulting to negative infinity if None
83    pub min_rate: Option<f64>,
84    /// The maximum rate (nonnegative), defaulting to positive infinity if None
85    pub max_rate: Option<f64>,
86    /// The (finite) price
87    pub price: f64,
88}
89
90impl Into<ConstantCurveDto> for ConstantCurve {
91    fn into(self) -> ConstantCurveDto {
92        ConstantCurveDto {
93            min_rate: if self.min_rate.is_finite() {
94                Some(self.min_rate)
95            } else {
96                None
97            },
98            max_rate: if self.max_rate.is_finite() {
99                Some(self.max_rate)
100            } else {
101                None
102            },
103            price: self.price,
104        }
105    }
106}
107
108impl TryFrom<ConstantCurveDto> for ConstantCurve {
109    type Error = ConstantCurveError;
110
111    fn try_from(value: ConstantCurveDto) -> Result<Self, Self::Error> {
112        let min_rate = value.min_rate.unwrap_or(f64::NEG_INFINITY);
113        let max_rate = value.max_rate.unwrap_or(f64::INFINITY);
114        let price = value.price;
115
116        if min_rate.is_nan() || max_rate.is_nan() || price.is_nan() {
117            return Err(ConstantCurveError::NaN);
118        }
119        if price.is_infinite() {
120            return Err(ConstantCurveError::InfinitePrice);
121        }
122        if !(min_rate <= 0.0 && 0.0 <= max_rate) {
123            return Err(ConstantCurveError::ZeroTrade);
124        }
125
126        Ok(Self {
127            min_rate,
128            max_rate,
129            price,
130        })
131    }
132}
133
134/// Errors that can occur when creating or validating a ConstantCurve
135#[derive(Debug, PartialEq, thiserror::Error)]
136pub enum ConstantCurveError {
137    /// Error when any coordinate value is NaN
138    #[error("NaN value encountered")]
139    NaN,
140    /// Error when the curve's domain does not include rate=0
141    #[error("Domain excludes rate=0")]
142    ZeroTrade,
143    /// Error when the price is infinite
144    #[error("Price cannot be infinite")]
145    InfinitePrice,
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_domain_contains_zero() {
154        // Valid: spans zero
155        let result = ConstantCurve::new(Some(-5.0), Some(5.0), 10.0);
156        assert!(result.is_ok());
157
158        // Valid: min_rate exactly 0
159        let result = ConstantCurve::new(Some(0.0), Some(5.0), 10.0);
160        assert!(result.is_ok());
161
162        // Valid: max_rate exactly 0
163        let result = ConstantCurve::new(Some(-5.0), Some(0.0), 10.0);
164        assert!(result.is_ok());
165
166        // Valid: both None (infinite domain)
167        let result = ConstantCurve::new(None, None, 10.0);
168        assert!(result.is_ok());
169
170        // Valid: min_rate None, max_rate positive
171        let result = ConstantCurve::new(None, Some(5.0), 10.0);
172        assert!(result.is_ok());
173
174        // Valid: min_rate negative, max_rate None
175        let result = ConstantCurve::new(Some(-5.0), None, 10.0);
176        assert!(result.is_ok());
177    }
178
179    #[test]
180    fn test_neg_infinity() {
181        let (min, max) = ConstantCurve::new(None, Some(5.0), 10.0).unwrap().domain();
182        assert!(min.is_infinite() && min < 0.0 && max == 5.0);
183    }
184
185    #[test]
186    fn test_pos_infinity() {
187        let (min, max) = ConstantCurve::new(Some(0.0), None, 10.0).unwrap().domain();
188        assert!(min == 0.0 && max.is_infinite() && max > 0.0);
189    }
190
191    #[test]
192    fn test_full_infinity() {
193        let (min, max) = ConstantCurve::new(None, None, 10.0).unwrap().domain();
194        assert!(min.is_infinite() && min < 0.0 && max.is_infinite() && max > 0.0);
195    }
196
197    #[test]
198    fn test_infinite_price() {
199        assert_eq!(
200            ConstantCurve::new(None, None, f64::INFINITY).unwrap_err(),
201            ConstantCurveError::InfinitePrice
202        );
203    }
204
205    #[test]
206    fn test_nans() {
207        assert_eq!(
208            ConstantCurve::new(Some(f64::NAN), None, 10.0).unwrap_err(),
209            ConstantCurveError::NaN
210        );
211        assert_eq!(
212            ConstantCurve::new(None, Some(f64::NAN), 10.0).unwrap_err(),
213            ConstantCurveError::NaN
214        );
215        assert_eq!(
216            ConstantCurve::new(None, None, f64::NAN).unwrap_err(),
217            ConstantCurveError::NaN
218        );
219    }
220
221    #[test]
222    fn test_bad_domain_reversed() {
223        assert_eq!(
224            ConstantCurve::new(Some(1.0), Some(-1.0), 10.0).unwrap_err(),
225            ConstantCurveError::ZeroTrade
226        );
227    }
228
229    #[test]
230    fn test_bad_domain_strict_positive() {
231        assert_eq!(
232            ConstantCurve::new(Some(1.0), Some(3.0), 10.0).unwrap_err(),
233            ConstantCurveError::ZeroTrade
234        );
235    }
236
237    #[test]
238    fn test_bad_domain_strict_negative() {
239        assert_eq!(
240            ConstantCurve::new(Some(-3.0), Some(-1.0), 10.0).unwrap_err(),
241            ConstantCurveError::ZeroTrade
242        );
243    }
244}