fts_core/models/
auction.rs

1use fxhash::FxBuildHasher;
2use indexmap::IndexMap;
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> Into<Vec<fts_solver::Submission<AuthId, ProductId>>> for RawAuctionInput<T> {
64    fn into(self) -> Vec<fts_solver::Submission<AuthId, ProductId>> {
65        // Convert the auction into a format the solver understands
66        // First, we aggregate the auths by the bidder
67        let mut auths_by_bidder = IndexMap::<BidderId, Vec<AuthRecord>, FxBuildHasher>::default();
68
69        for record in self.auths {
70            auths_by_bidder
71                .entry(record.bidder_id)
72                .or_default()
73                .push(record);
74        }
75
76        // Same story for costs
77        let mut costs_by_bidder = IndexMap::<BidderId, Vec<CostRecord>, FxBuildHasher>::default();
78
79        for record in self.costs {
80            costs_by_bidder
81                .entry(record.bidder_id)
82                .or_default()
83                .push(record);
84        }
85
86        let scale = (self.thru - self.from) / self.trade_duration;
87
88        // Now we produce a list of submissions
89
90        costs_by_bidder
91            .into_iter()
92            .filter_map(|(bidder_id, costs)| {
93                // If the bidder has no auths, then the costs are no-op.
94                if let Some(auths) = auths_by_bidder.swap_remove(&bidder_id) {
95                    // Otherwise, we scale the rate-based definitions into quantity-based ones
96                    let auths = auths
97                        .into_iter()
98                        .filter_map(|record| record.into_solver(scale));
99
100                    let costs = costs
101                        .into_iter()
102                        .filter_map(|record| record.into_solver(scale));
103
104                    // Having done all that, we now have a submission
105                    fts_solver::Submission::new(auths, costs)
106                } else {
107                    None
108                }
109            })
110            .collect()
111    }
112}
113
114// TODO: this is probably unnecessary with the right sequence of invocations,
115// but it is easy enough to maintain.
116
117/// Deserializes a human-readable duration string into an Option<Duration>.
118///
119/// This helper function allows duration values to be specified in a human-friendly format
120/// (e.g., "1h", "30m", "1d") in the API requests.
121fn optional_humantime<'de, D: Deserializer<'de>>(
122    deserializer: D,
123) -> Result<Option<Duration>, D::Error> {
124    // Extract the string, if present
125    let value: Option<&str> = Deserialize::deserialize(deserializer)?;
126
127    if let Some(value) = value {
128        // If the string is present, try parsing.
129        // Note that humantime parses into std::time::Duration, so we also need to do a conversion
130        let delta = humantime::parse_duration(value).map_err(serde::de::Error::custom)?;
131
132        let sec: i64 = delta
133            .as_secs()
134            .try_into()
135            .map_err(serde::de::Error::custom)?;
136        let ns: i32 = delta
137            .subsec_nanos()
138            .try_into()
139            .map_err(serde::de::Error::custom)?;
140
141        Ok(Some(Duration::new(sec, ns)))
142    } else {
143        Ok(None)
144    }
145}