Skip to main content

acta_maker_sdk/
spreads.rs

1//! Cash spreads use quote atoms for collateral and payoff; prices use 1e9.
2
3use 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
48/// Put spreads allow a zero lower strike; calls require a positive lower strike.
49pub 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/// Both long legs are valued by the caller's model with one expiry/volatility context.
224#[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}