fts_core/models/
auth.rs

1use crate::models::{BidderId, Bound, ProductId, uuid_wrapper};
2use fxhash::FxBuildHasher;
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use std::hash::Hash;
6use thiserror::Error;
7use time::OffsetDateTime;
8use utoipa::ToSchema;
9
10uuid_wrapper!(AuthId);
11
12/// The supported constraints for an authorization.
13///
14/// An AuthData defines the trading constraints for an authorization:
15/// - Rate constraints limit how fast a portfolio can be traded (in units per time)
16/// - Trade constraints limit the total accumulated trade amount over time
17///
18/// The rate constraints must allow the possibility of zero trade (min_rate ≤ 0 ≤ max_rate).
19///
20/// The trade constraints do not have this restriction, but instead, at time of
21/// specification, they *should* allow for the currently traded amount of the auth.
22/// If they do not, the trade constraint is implicitly expanded to include 0 at
23/// each auction, which may not be desired.
24#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
25#[serde(try_from = "RawAuthorization", into = "RawAuthorization")]
26pub struct AuthData {
27    /// The fastest rate at which the portfolio may sell (non-positive)
28    #[schema(value_type = Option<f64>)]
29    pub min_rate: f64,
30
31    /// The fastest rate at which the portfolio may buy (non-negative)
32    #[schema(value_type = Option<f64>)]
33    pub max_rate: f64,
34
35    /// A minimum amount of trade to preserve (always enforced against the authorization's contemporaneous amount of trade)
36    #[schema(value_type = Option<f64>)]
37    pub min_trade: f64,
38
39    /// A maximum amount of trade to preserve (always enforced against the authorization's contemporaneous amount of trade)
40    #[schema(value_type = Option<f64>)]
41    pub max_trade: f64,
42}
43
44impl AuthData {
45    /// Creates a new AuthData with the specified constraints.
46    pub fn new(
47        min_rate: f64,
48        max_rate: f64,
49        min_trade: f64,
50        max_trade: f64,
51    ) -> Result<Self, ValidationError> {
52        if min_rate.is_nan() || max_rate.is_nan() {
53            return Err(ValidationError::NAN);
54        }
55        if !(min_rate <= 0.0 && 0.0 <= max_rate) {
56            return Err(ValidationError::ZERORATE);
57        }
58
59        if min_trade.is_nan() || max_trade.is_nan() {
60            return Err(ValidationError::NAN);
61        }
62        if min_trade > max_trade {
63            return Err(ValidationError::INFEASIBLETRADE);
64        }
65
66        Ok(Self {
67            min_rate,
68            max_rate,
69            min_trade,
70            max_trade,
71        })
72    }
73}
74
75/// An enumeration of the ways authorization data may be invalid
76#[derive(Debug, Error)]
77pub enum ValidationError {
78    /// Error when any constraint value is NaN
79    #[error("NaN value encountered")]
80    NAN,
81    /// Error when rate constraints don't allow zero trade
82    #[error("Rate restriction must allow for 0")]
83    ZERORATE,
84    /// Error when min_trade > max_trade
85    #[error("Trade restriction is infeasible")]
86    INFEASIBLETRADE,
87}
88
89/// The "DTO" type for AuthData. Omitted values default to the appropriately signed infinity.
90///
91/// This provides a user-friendly interface for specifying auth constraints, allowing
92/// missing values to default to appropriate infinities.
93#[derive(Serialize, Deserialize)]
94pub struct RawAuthorization {
95    pub min_rate: Bound,
96    pub max_rate: Bound,
97    pub min_trade: Bound,
98    pub max_trade: Bound,
99}
100
101impl TryFrom<RawAuthorization> for AuthData {
102    type Error = ValidationError;
103
104    fn try_from(value: RawAuthorization) -> Result<Self, Self::Error> {
105        AuthData::new(
106            value.min_rate.or_neg_inf(),
107            value.max_rate.or_pos_inf(),
108            value.min_trade.or_neg_inf(),
109            value.max_trade.or_pos_inf(),
110        )
111    }
112}
113
114impl From<AuthData> for RawAuthorization {
115    fn from(value: AuthData) -> Self {
116        Self {
117            min_rate: value.min_rate.into(),
118            max_rate: value.max_rate.into(),
119            min_trade: value.min_trade.into(),
120            max_trade: value.max_trade.into(),
121        }
122    }
123}
124
125/// A record of the authorization's data at the time it was updated or defined
126///
127/// This provides historical versioning of auth constraints, allowing the system
128/// to track changes to auth parameters over time.
129#[derive(Serialize, Deserialize, PartialEq, ToSchema, Debug)]
130pub struct AuthHistoryRecord {
131    /// The authorization constraints, or None if the auth was deactivated
132    pub data: Option<AuthData>,
133    /// The timestamp when this version was created
134    #[serde(with = "time::serde::rfc3339")]
135    pub version: OffsetDateTime,
136}
137
138/// A full description of an authorization
139///
140/// An AuthRecord combines all the information needed to define an authorization:
141/// - Who owns it (bidder_id)
142/// - What it trades (portfolio)
143/// - How it can be traded (data)
144/// - The current accumulated trade (trade)
145#[derive(Serialize, Deserialize, PartialEq, ToSchema, Debug)]
146pub struct AuthRecord {
147    /// The responsible bidder's id
148    pub bidder_id: BidderId,
149
150    /// A unique id for the auth
151    pub auth_id: AuthId,
152
153    /// The portfolio associated to the auth. Due to the expected size, this portfolio may be omitted from certain endpoints.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    #[schema(value_type = Option<std::collections::HashMap<ProductId, f64>>)]
156    pub portfolio: Option<Portfolio>,
157
158    /// The constraint data for the authorization
159    pub data: Option<AuthData>,
160
161    /// The "last-modified-or-created" time as recorded by the system
162    #[serde(with = "time::serde::rfc3339")]
163    pub version: OffsetDateTime,
164
165    /// The amount of cumulative trade associated to this authorization, as-of the request time
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub trade: Option<f64>,
168}
169
170/// A portfolio is a weighted bundle of products
171///
172/// Portfolios define what combination of products an auth trades, with weights determining
173/// the relative proportions. Positive weights indicate buying the product, negative weights
174/// indicate selling.
175pub type Portfolio = IndexMap<ProductId, f64, FxBuildHasher>;
176
177impl AuthRecord {
178    /// Converts this auth record into a solver-compatible format.
179    ///
180    /// This method applies the time scale to rate-based constraints and computes
181    /// the appropriate trade bounds for the auction, taking into account the
182    /// current accumulated trade.
183    pub fn into_solver(self, scale: f64) -> Option<(AuthId, fts_solver::Auth<ProductId>)> {
184        let trade = self.trade.unwrap_or_default();
185        if let Some(data) = self.data {
186            let min_trade = (data.min_trade - trade).max(data.min_rate * scale).min(0.0);
187            let max_trade = (data.max_trade - trade).min(data.max_rate * scale).max(0.0);
188            let portfolio = self
189                .portfolio
190                .unwrap_or_default()
191                .into_iter()
192                .collect::<fts_solver::Portfolio<_>>();
193
194            if portfolio.len() == 0 {
195                None
196            } else {
197                Some((
198                    self.auth_id,
199                    fts_solver::Auth {
200                        min_trade,
201                        max_trade,
202                        portfolio,
203                    },
204                ))
205            }
206        } else {
207            None
208        }
209    }
210}