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}