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}