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}