fts_core/models/demand/
constant.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3use utoipa::ToSchema;
4
5use crate::models::Bound;
6
7/// A representation of a flat demand curve supporting interval, half-line, and full-line trading domains
8///
9/// A constant curve represents a fixed price for trades within a specified rate range.
10/// This can be used to express indifference to (potentially unbounded) trade rates at a specific price.
11///
12/// The sign convention follows flow trading standards:
13/// - min_rate ≤ 0 (non-positive): maximum selling rate
14/// - max_rate ≥ 0 (non-negative): maximum buying rate
15#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
16#[serde(try_from = "RawConstant", into = "RawConstant")]
17pub struct Constant {
18    /// The fastest rate at which the portfolio may sell (non-positive)
19    #[schema(value_type = Option<f64>)]
20    pub min_rate: f64,
21
22    /// The fastest rate at which the portfolio may buy (non-negative)
23    #[schema(value_type = Option<f64>)]
24    pub max_rate: f64,
25
26    /// The fixed price at which trades within the rate range are valued
27    #[schema(value_type = Option<f64>)]
28    pub price: f64,
29}
30
31/// The "DTO" type for Constant, allowing for infinite values to be represented as nulls
32///
33/// This provides a user-friendly interface for specifying constant constraints, allowing
34/// missing values to default to appropriate infinities.
35#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
36pub struct RawConstant {
37    /// The minimum rate bound, defaulting to negative infinity if null or missing
38    pub min_rate: Bound,
39
40    /// The maximum rate bound, defaulting to positive infinity if null or missing
41    pub max_rate: Bound,
42
43    /// The fixed price for trades within the rate bounds
44    pub price: f64,
45}
46
47impl TryFrom<RawConstant> for Constant {
48    type Error = ValidationError;
49
50    /// Attempts to convert from the DTO format to the internal representation,
51    /// applying validation rules and handling infinite bounds.
52    fn try_from(value: RawConstant) -> Result<Self, Self::Error> {
53        Constant::new(
54            value.min_rate.or_neg_inf(),
55            value.max_rate.or_pos_inf(),
56            value.price,
57        )
58    }
59}
60
61impl From<Constant> for RawConstant {
62    /// Converts from the internal representation to the DTO format,
63    /// mapping infinite values to null bounds.
64    fn from(value: Constant) -> Self {
65        Self {
66            min_rate: value.min_rate.into(),
67            max_rate: value.max_rate.into(),
68            price: value.price,
69        }
70    }
71}
72
73impl Constant {
74    /// Creates a new validated constant curve
75    ///
76    /// Creates a constant curve with the specified rates and price,
77    /// ensuring all values satisfy validation requirements:
78    /// - min_rate must be non-positive
79    /// - max_rate must be non-negative
80    /// - price must be finite
81    /// - Neither rate nor price can be NaN
82    pub fn new(min_rate: f64, max_rate: f64, price: f64) -> Result<Self, ValidationError> {
83        // Use the unchecked variant to provide the default values for the
84        // None's, but then painstakingly verify the validity.
85        let unvalidated = unsafe { Self::new_unchecked(min_rate, max_rate, price) };
86
87        let lerr = validate_bound(unvalidated.min_rate, -1.0);
88        let rerr = validate_bound(unvalidated.max_rate, 1.0);
89        let perr = validate_price(unvalidated.price);
90
91        match (lerr, rerr, perr) {
92            (None, None, None) => Ok(unvalidated),
93            (lerr, rerr, perr) => Err(ValidationError {
94                min_rate: lerr,
95                max_rate: rerr,
96                price: perr,
97            }),
98        }
99    }
100
101    /// Creates a new constant constraint without validation
102    ///
103    /// # Safety
104    /// This function is unsafe because it bypasses validation of rates and price.
105    /// It should only be used when the caller can guarantee the values are valid.
106    pub unsafe fn new_unchecked(min_rate: f64, max_rate: f64, price: f64) -> Self {
107        Self {
108            min_rate,
109            max_rate,
110            price,
111        }
112    }
113}
114
115/// The various ways in which a flat demand curve can be invalid
116#[derive(Debug, Error)]
117#[error("(min_rate = {min_rate:?}, max_rate = {max_rate:?}, price = {price:?})")]
118pub struct ValidationError {
119    /// Error related to the min_rate value, if any
120    min_rate: Option<BoundValidationError>,
121
122    /// Error related to the max_rate value, if any
123    max_rate: Option<BoundValidationError>,
124
125    /// Error related to the price value, if any
126    price: Option<PriceError>,
127}
128
129/// Errors specific to bound validation (min_rate and max_rate)
130#[derive(Debug, Error)]
131pub enum BoundValidationError {
132    /// The bound value is NaN, which is not allowed
133    #[error("NaN")]
134    NAN,
135
136    /// The bound value has incorrect sign (min_rate must be non-positive, max_rate must be non-negative)
137    #[error("Sign")]
138    SIGN,
139}
140
141/// Errors specific to price validation
142#[derive(Debug, Error)]
143pub enum PriceError {
144    /// The price value is NaN, which is not allowed
145    #[error("NaN")]
146    NAN,
147
148    /// The price value is infinite, which is not allowed
149    #[error("Infinity")]
150    INFINITY,
151}
152
153/// Validates that a bound value meets requirements
154///
155/// # Arguments
156/// * `x` - The bound value to validate
157/// * `sgn` - The sign to check against (1.0 for max_rate, -1.0 for min_rate)
158///
159/// # Returns
160/// None if the bound is valid, or a BoundValidationError if invalid
161fn validate_bound(x: f64, sgn: f64) -> Option<BoundValidationError> {
162    if x.is_nan() {
163        Some(BoundValidationError::NAN)
164    } else if sgn * x < 0.0 {
165        Some(BoundValidationError::SIGN)
166    } else {
167        None
168    }
169}
170
171/// Validates that a price value meets requirements
172///
173/// # Arguments
174/// * `x` - The price value to validate
175///
176/// # Returns
177/// None if the price is valid, or a PriceError if invalid
178fn validate_price(x: f64) -> Option<PriceError> {
179    if x.is_nan() {
180        Some(PriceError::NAN)
181    } else if x.is_infinite() {
182        Some(PriceError::INFINITY)
183    } else {
184        None
185    }
186}
187
188impl Constant {
189    /// Return the domain of the demand curve (min and max rates)
190    pub fn domain(&self) -> (f64, f64) {
191        (self.min_rate, self.max_rate)
192    }
193
194    /// Converts this constant curve to the solver's representation
195    ///
196    /// Applies the given time scale to rate-based values to produce
197    /// quantity-based values for the solver
198    pub fn as_solver(&self, scale: f64) -> Vec<fts_solver::Point> {
199        vec![
200            fts_solver::Point {
201                quantity: self.min_rate * scale,
202                price: self.price,
203            },
204            fts_solver::Point {
205                quantity: self.max_rate * scale,
206                price: self.price,
207            },
208        ]
209    }
210}