fts_core/models/cost.rs
1mod constant;
2mod curve;
3
4use crate::models::{AuthId, BidderId, uuid_wrapper};
5pub use constant::{Constant, RawConstant};
6pub use curve::{Curve, Point};
7use fxhash::FxBuildHasher;
8use indexmap::IndexMap;
9use serde::{Deserialize, Serialize};
10use std::hash::Hash;
11use thiserror::Error;
12use time::OffsetDateTime;
13use utoipa::ToSchema;
14
15// A simple newtype for a Uuid
16uuid_wrapper!(CostId);
17
18/// Controls whether cost group details should be included in API responses
19///
20/// Since cost groups can be large, this enum allows API endpoints to optionally
21/// exclude group details from responses to reduce payload size.
22#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
23#[serde(rename_all = "lowercase")]
24pub enum GroupDisplay {
25 /// Exclude group details from the response
26 Exclude,
27 /// Include group details in the response
28 Include,
29}
30
31impl Default for GroupDisplay {
32 fn default() -> Self {
33 Self::Exclude
34 }
35}
36
37/// The utility-specification of the cost
38///
39/// CostData represents either:
40/// - A non-increasing, piecewise-linear demand curve assigning a cost to each quantity in its domain, or
41/// - A simple, "flat" demand curve assining a constant cost to each quantity in its domain.
42///
43/// This is the core component that defines how a bidder values different trade outcomes.
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
45#[serde(untagged, try_from = "RawCostData", into = "RawCostData")]
46// TODO: Utoipa doesn't fully support all the Serde annotations,
47// so we injected `untagged` (which Serde will ignore given the presence of
48// of `try_from` and `into`), then inline the actual fields. This appears
49// to correctly generate the OpenAPI schema, but we should revisit.
50pub enum CostData {
51 /// A piecewise linear demand curve defined by points
52 Curve(#[schema(inline)] Curve),
53 /// A constant constraint enforcing a specific trade quantity at a price
54 Constant(#[schema(inline)] Constant),
55}
56
57/// An error type for the ways in which the provided utility function may be invalid.
58#[derive(Error, Debug)]
59pub enum ValidationError {
60 /// Error when a curve's definition is invalid
61 #[error("invalid demand curve: {0}")]
62 Curve(#[from] curve::ValidationError),
63 /// Error when a constant curve's definition is invalid
64 #[error("invalid constant curve: {0}")]
65 Constraint(#[from] constant::ValidationError),
66}
67
68/// The "DTO" type for the utility
69///
70/// This enum represents the raw data formats accepted in API requests for defining costs.
71#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
72#[serde(untagged)]
73pub enum RawCostData {
74 /// A sequence of points defining a piecewise linear demand curve
75 Curve(Vec<Point>),
76 /// A raw constant constraint definition
77 Constant(RawConstant),
78}
79
80impl TryFrom<RawCostData> for CostData {
81 type Error = ValidationError;
82
83 fn try_from(value: RawCostData) -> Result<Self, Self::Error> {
84 match value {
85 RawCostData::Curve(curve) => Ok(CostData::Curve(curve.try_into()?)),
86 RawCostData::Constant(constant) => Ok(CostData::Constant(constant.try_into()?)),
87 }
88 }
89}
90
91impl From<CostData> for RawCostData {
92 fn from(value: CostData) -> Self {
93 match value {
94 CostData::Curve(curve) => RawCostData::Curve(curve.into()),
95 CostData::Constant(constant) => RawCostData::Constant(constant.into()),
96 }
97 }
98}
99
100/// A record of the cost's data at the time it was updated or defined
101///
102/// This provides historical versioning of cost data, allowing the system
103/// to track changes to cost definitions over time.
104#[derive(Serialize, Deserialize, PartialEq, ToSchema, Debug)]
105pub struct CostHistoryRecord {
106 /// The cost data, or None if the cost was deactivated
107 pub data: Option<CostData>,
108 /// The timestamp when this version was created
109 #[serde(with = "time::serde::rfc3339")]
110 pub version: OffsetDateTime,
111}
112
113/// A full description of a cost
114///
115/// A CostRecord combines all the information needed to define a cost:
116/// - Who owns it (bidder_id)
117/// - Which auths it applies to (group)
118/// - The utility function (data)
119#[derive(Serialize, Deserialize, PartialEq, Debug, ToSchema)]
120pub struct CostRecord {
121 /// The responsible bidder's id
122 pub bidder_id: BidderId,
123
124 /// A unique id for the cost
125 pub cost_id: CostId,
126
127 /// The group associated to the cost. Because it is not always required, some endpoints may omit its definition.
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 #[schema(value_type = Option<std::collections::HashMap<AuthId, f64>>)]
130 pub group: Option<Group>,
131
132 /// The utility for the cost
133 pub data: Option<CostData>,
134
135 /// The "last-modified-or-created" time as recorded by the system
136 #[serde(with = "time::serde::rfc3339")]
137 pub version: OffsetDateTime,
138}
139
140/// A group is a sparse collection of authorizations
141///
142/// Groups define which auths a particular cost applies to, with weights determining the
143/// relative contribution of each auth to the group's overall trade. This allows bidders
144/// to express substitution preferences between different portfolios.
145pub type Group = IndexMap<AuthId, f64, FxBuildHasher>;
146
147impl CostRecord {
148 /// Converts this cost record into a solver-compatible format.
149 ///
150 /// This method transforms the cost record into the appropriate solver structures,
151 /// applying the time scale to rate-based constraints as needed.
152 pub fn into_solver(self, scale: f64) -> Option<(fts_solver::Group<AuthId>, fts_solver::Cost)> {
153 let group = self.group.unwrap_or_default().into_iter().collect();
154
155 if let Some(data) = self.data {
156 Some((
157 group,
158 match data {
159 CostData::Curve(curve) => {
160 let curve = curve.as_solver(scale);
161 if curve.points.len() == 1 {
162 // We can convert a pathological curve into a constraint
163 let point = curve.points.first().unwrap();
164 // assert_eq!(point.quantity, 0.0);
165 let constant = fts_solver::Constant {
166 quantity: (point.quantity, point.quantity),
167 price: point.price,
168 };
169 fts_solver::Cost::Constant(constant)
170 } else {
171 fts_solver::Cost::PiecewiseLinearCurve(curve)
172 }
173 }
174 CostData::Constant(constant) => {
175 fts_solver::Cost::Constant(constant.as_solver(scale))
176 }
177 },
178 ))
179 } else {
180 None
181 }
182 }
183}