fts_core/models/cost/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 /// Converts this constant curve to the solver's representation
190 ///
191 /// Applies the given time scale to rate-based values to produce
192 /// quantity-based values for the solver
193 pub fn as_solver(&self, scale: f64) -> fts_solver::Constant {
194 fts_solver::Constant {
195 quantity: (self.min_rate * scale, self.max_rate * scale),
196 price: self.price,
197 }
198 }
199}