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