1use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[repr(u8)]
7pub enum SpreadType {
8 CallSpread = 2,
9 PutSpread = 3,
10}
11
12impl TryFrom<u8> for SpreadType {
13 type Error = SpreadError;
14 fn try_from(value: u8) -> Result<Self, Self::Error> {
15 match value {
16 2 => Ok(Self::CallSpread),
17 3 => Ok(Self::PutSpread),
18 _ => Err(SpreadError::InvalidType),
19 }
20 }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
24pub enum SpreadError {
25 #[error("taker_buy_unsupported")]
26 TakerBuyUnsupported,
27 #[error("expected CallSpread or PutSpread")]
28 InvalidType,
29 #[error("spread requires lower < upper")]
30 InvalidStrikes,
31 #[error("spread requires nonzero quantity and 0 < premium < width")]
32 InvalidPremium,
33 #[error("spread amount overflow or unsupported decimals (expected 0..=18)")]
34 Overflow,
35 #[error("invalid spread fee basis points")]
36 InvalidFee,
37 #[error("collateral is below the required liability")]
38 InsufficientCollateral,
39 #[error("invalid pricing inputs or long-leg values")]
40 InvalidPricing,
41}
42
43pub fn width(lower: u64, upper: u64) -> Result<u64, SpreadError> {
44 upper
45 .checked_sub(lower)
46 .filter(|v| *v > 0)
47 .ok_or(SpreadError::InvalidStrikes)
48}
49
50pub fn validate_strikes(kind: SpreadType, lower: u64, upper: u64) -> Result<u64, SpreadError> {
52 if kind == SpreadType::CallSpread && lower == 0 {
53 return Err(SpreadError::InvalidStrikes);
54 }
55 width(lower, upper)
56}
57
58pub fn validate_premium(
59 kind: SpreadType,
60 lower: u64,
61 upper: u64,
62 quantity: u64,
63 price: u64,
64) -> Result<(), SpreadError> {
65 let width = validate_strikes(kind, lower, upper)?;
66 if quantity == 0 || price == 0 || price >= width {
67 return Err(SpreadError::InvalidPremium);
68 }
69 Ok(())
70}
71
72pub fn quote_amount(
73 price: u64,
74 quantity: u64,
75 quote_decimals: u8,
76 underlying_decimals: u8,
77 ceil: bool,
78) -> Result<u64, SpreadError> {
79 if quote_decimals > 18 || underlying_decimals > 18 {
80 return Err(SpreadError::Overflow);
81 }
82 let quote_scale = 10u128.pow(u32::from(quote_decimals));
83 let denominator = 1_000_000_000u128 * 10u128.pow(u32::from(underlying_decimals));
84 let numerator = u128::from(price) * u128::from(quantity);
85 let result = if quote_scale <= denominator {
86 let divisor = denominator / quote_scale;
87 numerator / divisor + u128::from(ceil && numerator % divisor != 0)
88 } else {
89 numerator
90 .checked_mul(quote_scale / denominator)
91 .ok_or(SpreadError::Overflow)?
92 };
93 u64::try_from(result).map_err(|_| SpreadError::Overflow)
94}
95
96pub fn collateral(
97 lower: u64,
98 upper: u64,
99 quantity: u64,
100 quote_decimals: u8,
101 underlying_decimals: u8,
102) -> Result<u64, SpreadError> {
103 quote_amount(
104 width(lower, upper)?,
105 quantity,
106 quote_decimals,
107 underlying_decimals,
108 true,
109 )
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct SpreadTerms {
114 pub kind: SpreadType,
115 pub lower_strike: u64,
116 pub upper_strike: u64,
117 pub quantity: u64,
118 pub quote_decimals: u8,
119 pub underlying_decimals: u8,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct SpreadPremium {
124 pub collateral: u64,
125 pub gross_premium: u64,
126 pub protocol_fee: u64,
127 pub net_premium: u64,
128 pub prefunded_quote: u64,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct SpreadSettlement {
133 pub maker_payoff: u64,
134 pub taker_return: u64,
135}
136
137impl SpreadTerms {
138 pub fn collateral(self) -> Result<u64, SpreadError> {
139 collateral(
140 self.lower_strike,
141 self.upper_strike,
142 self.quantity,
143 self.quote_decimals,
144 self.underlying_decimals,
145 )
146 }
147
148 pub fn payoff(self, settlement_price: u64) -> Result<u64, SpreadError> {
149 let width = width(self.lower_strike, self.upper_strike)?;
150 let intrinsic = match self.kind {
151 SpreadType::CallSpread => settlement_price.saturating_sub(self.lower_strike),
152 SpreadType::PutSpread => self.upper_strike.saturating_sub(settlement_price),
153 }
154 .min(width);
155 self.quote(intrinsic, true)
156 }
157
158 pub fn settle(
159 self,
160 settlement_price: u64,
161 actual_collateral: u64,
162 ) -> Result<SpreadSettlement, SpreadError> {
163 if actual_collateral < self.collateral()? {
164 return Err(SpreadError::InsufficientCollateral);
165 }
166 let maker_payoff = self.payoff(settlement_price)?;
167 let taker_return = actual_collateral
168 .checked_sub(maker_payoff)
169 .ok_or(SpreadError::InsufficientCollateral)?;
170 Ok(SpreadSettlement {
171 maker_payoff,
172 taker_return,
173 })
174 }
175
176 pub fn premium(
177 self,
178 price: u64,
179 premium_bps: u16,
180 volume_bps: u16,
181 ) -> Result<SpreadPremium, SpreadError> {
182 validate_premium(
183 self.kind,
184 self.lower_strike,
185 self.upper_strike,
186 self.quantity,
187 price,
188 )?;
189 if premium_bps > 10_000 || volume_bps > 10_000 {
190 return Err(SpreadError::InvalidFee);
191 }
192 let collateral = self.collateral()?;
193 let gross_premium = self.quote(price, false)?;
194 let exposure = self.quote(width(self.lower_strike, self.upper_strike)?, false)?;
195 if gross_premium == 0 || gross_premium >= collateral || exposure == 0 {
196 return Err(SpreadError::InvalidPremium);
197 }
198 let fee = |amount: u64, bps: u16| -> Result<u64, SpreadError> {
199 u64::try_from(u128::from(amount) * u128::from(bps) / 10_000)
200 .map_err(|_| SpreadError::Overflow)
201 };
202 let premium_fee = gross_premium - self.quote(price - fee(price, premium_bps)?, false)?;
203 let protocol_fee = premium_fee.min(fee(exposure, volume_bps)?);
204 let net_premium = gross_premium - protocol_fee;
205 Ok(SpreadPremium {
206 collateral,
207 gross_premium,
208 protocol_fee,
209 net_premium,
210 prefunded_quote: collateral - net_premium,
211 })
212 }
213
214 fn quote(self, price: u64, ceil: bool) -> Result<u64, SpreadError> {
215 quote_amount(
216 price,
217 self.quantity,
218 self.quote_decimals,
219 self.underlying_decimals,
220 ceil,
221 )
222 }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq)]
227pub struct SpreadPricingContext {
228 pub time_to_expiry_years: f64,
229 pub volatility: f64,
230}
231
232pub fn price_spread(
233 kind: SpreadType,
234 lower_strike: f64,
235 upper_strike: f64,
236 context: SpreadPricingContext,
237 mut long_leg_value: impl FnMut(bool, f64, SpreadPricingContext) -> f64,
238) -> Result<f64, SpreadError> {
239 if !lower_strike.is_finite()
240 || !upper_strike.is_finite()
241 || lower_strike < 0.0
242 || (kind == SpreadType::CallSpread && lower_strike == 0.0)
243 || upper_strike <= lower_strike
244 || !context.time_to_expiry_years.is_finite()
245 || context.time_to_expiry_years < 0.0
246 || !context.volatility.is_finite()
247 || context.volatility < 0.0
248 {
249 return Err(SpreadError::InvalidPricing);
250 }
251 let is_put = kind == SpreadType::PutSpread;
252 let low = if is_put && lower_strike == 0.0 {
253 0.0
254 } else {
255 long_leg_value(is_put, lower_strike, context)
256 };
257 let high = long_leg_value(is_put, upper_strike, context);
258 let price = if is_put { high - low } else { low - high };
259 if !low.is_finite()
260 || !high.is_finite()
261 || low < 0.0
262 || high < 0.0
263 || !price.is_finite()
264 || price < 0.0
265 || price > upper_strike - lower_strike
266 {
267 return Err(SpreadError::InvalidPricing);
268 }
269 Ok(price)
270}