fts_core/models/
auction.rs

1use indexmap::IndexMap;
2use rustc_hash::FxBuildHasher;
3use serde::{Deserialize, Deserializer, Serialize};
4use time::{Duration, OffsetDateTime};
5use utoipa::ToSchema;
6
7use super::{AuthId, AuthRecord, BidderId, CostRecord, ProductId};
8
9/// Configuration for scheduling and running a batch auction.
10///
11/// Flow trading uses batch auctions to clear markets at regular intervals.
12/// This structure defines the time parameters for scheduling these auctions.
13#[derive(Deserialize, ToSchema)]
14pub struct AuctionSolveRequest {
15    /// The starting time of the batch; if omitted, defaults to the last batch's ending time
16    #[serde(default, with = "time::serde::rfc3339::option")]
17    pub from: Option<OffsetDateTime>,
18
19    /// The ending time of the batch
20    #[serde(with = "time::serde::rfc3339")]
21    pub thru: OffsetDateTime,
22
23    /// Optionally divide the (from, thru) interval into smaller batches with the provided duration
24    ///
25    /// This allows for scheduling multiple consecutive sub-auctions within the given time range.
26    #[serde(default, deserialize_with = "optional_humantime")]
27    pub by: Option<Duration>,
28}
29
30/// A simple struct for the external identification of an auction.
31///
32/// Provides the time interval of an auction for external reference.
33#[derive(Serialize, ToSchema)]
34pub struct AuctionMetaData {
35    /// The starting time of the auction interval
36    #[serde(with = "time::serde::rfc3339")]
37    pub from: OffsetDateTime,
38    /// The ending time of the auction interval
39    #[serde(with = "time::serde::rfc3339")]
40    pub thru: OffsetDateTime,
41}
42
43/// A struct containing the raw auth and cost records for the stated auction interval.
44///
45/// This structure collects all the inputs needed to solve an auction for a specific time interval.
46/// It contains all auths and costs that are active during the interval, as well as timing information
47/// needed to scale rate-based quantities appropriately.
48pub struct RawAuctionInput<AuctionId> {
49    /// An internal auction id
50    pub id: AuctionId,
51    /// The start time for this batch
52    pub from: OffsetDateTime,
53    /// The end time for this batch
54    pub thru: OffsetDateTime,
55    /// All appropriate auth records for the interval
56    pub auths: Vec<AuthRecord>,
57    /// All appropriate cost records for the interval
58    pub costs: Vec<CostRecord>,
59    /// The reference time that all rate-based quantities are defined with respect to
60    pub trade_duration: Duration,
61}
62
63impl<T> RawAuctionInput<T> {
64    /// Convert the auction data to a format the solver understands
65    pub fn into_solver(
66        self,
67    ) -> IndexMap<BidderId, fts_solver::Submission<AuthId, ProductId>, FxBuildHasher> {
68        // Convert the auction into a format the solver understands
69        // First, we aggregate the auths by the bidder
70        let mut auths_by_bidder = IndexMap::<BidderId, Vec<AuthRecord>, FxBuildHasher>::default();
71
72        for record in self.auths {
73            auths_by_bidder
74                .entry(record.bidder_id)
75                .or_default()
76                .push(record);
77        }
78
79        // Same story for costs
80        let mut costs_by_bidder = IndexMap::<BidderId, Vec<CostRecord>, FxBuildHasher>::default();
81
82        for record in self.costs {
83            costs_by_bidder
84                .entry(record.bidder_id)
85                .or_default()
86                .push(record);
87        }
88
89        let scale = (self.thru - self.from) / self.trade_duration;
90
91        // Now we produce a list of submissions
92        let submissions_by_bidder = auths_by_bidder
93            .into_iter()
94            .filter_map(|(bidder_id, auths)| {
95                let mut portfolios = Vec::new();
96                let mut curves = Vec::new();
97
98                for auth in auths {
99                    let id = auth.auth_id.clone();
100                    if let Some((portfolio, curve)) = auth.into_solver(scale) {
101                        portfolios.push((id, portfolio));
102                        curves.push(curve);
103                    }
104                }
105
106                if let Some(costs) = costs_by_bidder.swap_remove(&bidder_id) {
107                    for cost in costs {
108                        if let Some(curve) = cost.into_solver(scale) {
109                            curves.push(curve);
110                        }
111                    }
112                }
113
114                fts_solver::Submission::new(portfolios, curves)
115                    .ok()
116                    .map(|submission| (bidder_id, submission))
117            })
118            .collect();
119
120        submissions_by_bidder
121    }
122}
123
124// TODO: this is probably unnecessary with the right sequence of invocations,
125// but it is easy enough to maintain.
126
127/// Deserializes a human-readable duration string into an Option<Duration>.
128///
129/// This helper function allows duration values to be specified in a human-friendly format
130/// (e.g., "1h", "30m", "1d") in the API requests.
131fn optional_humantime<'de, D: Deserializer<'de>>(
132    deserializer: D,
133) -> Result<Option<Duration>, D::Error> {
134    // Extract the string, if present
135    let value: Option<&str> = Deserialize::deserialize(deserializer)?;
136
137    if let Some(value) = value {
138        // If the string is present, try parsing.
139        // Note that humantime parses into std::time::Duration, so we also need to do a conversion
140        let delta = humantime::parse_duration(value).map_err(serde::de::Error::custom)?;
141
142        let sec: i64 = delta
143            .as_secs()
144            .try_into()
145            .map_err(serde::de::Error::custom)?;
146        let ns: i32 = delta
147            .subsec_nanos()
148            .try_into()
149            .map_err(serde::de::Error::custom)?;
150
151        Ok(Some(Duration::new(sec, ns)))
152    } else {
153        Ok(None)
154    }
155}