options_math/
lib.rs

1#[macro_use]
2extern crate derive_new;
3
4use chrono::prelude::*;
5use itertools::Itertools;
6use std::collections::HashMap;
7
8#[derive(PartialEq, Clone, Copy, Debug)]
9pub enum OptionKind {
10    Call,
11    Put,
12}
13
14pub type Cents = i64;
15
16pub type Percentage = f64;
17
18#[derive(new, Clone, Copy, Debug)]
19pub struct OptionContract {
20    expires_at: NaiveDateTime,
21    strike: Cents,
22    kind: OptionKind,
23    bid: Cents,
24    ask: Cents,
25}
26
27impl OptionContract {
28    /**
29     * Mark price
30     */
31    pub fn mark(self) -> Cents {
32        return (self.ask + self.bid) / 2;
33    }
34}
35
36#[derive(Clone, Copy, Debug)]
37struct OptionStrike {
38    price: Cents,
39    put: OptionContract,
40    call: OptionContract,
41    delta_k: Cents,
42}
43
44impl OptionStrike {
45    /**
46     * Difference between the price of the call and put
47     */
48    pub fn call_put_difference(self) -> Cents {
49        return self.call.mark() - self.put.mark();
50    }
51}
52
53#[derive(Clone, Debug)]
54pub struct OptionsByExpiryDate {
55    expires_at: NaiveDateTime,
56    calls: Vec<OptionContract>,
57    puts: Vec<OptionContract>,
58}
59
60impl OptionsByExpiryDate {
61    /**
62     * Gets options grouped and sorted by their strike price.
63     */
64    fn get_strikes(&self) -> Vec<OptionStrike> {
65        let mut all_options: Vec<OptionContract> = self
66            .calls
67            .clone()
68            .into_iter()
69            .chain(self.puts.clone().into_iter())
70            // filter out zero bids
71            .filter(|o| o.bid != 0)
72            .collect();
73        all_options.sort_unstable_by_key(|o| o.strike);
74
75        let mut options_by_strike: Vec<OptionStrike> = all_options
76            .into_iter()
77            .group_by(|o| o.strike)
78            .into_iter()
79            .flat_map(|(strike, options)| -> Option<OptionStrike> {
80                let options_for_strike: Vec<OptionContract> = options.collect();
81                let call = options_for_strike
82                    .clone()
83                    .into_iter()
84                    .find(|o| o.kind == OptionKind::Call);
85                let put = options_for_strike
86                    .clone()
87                    .into_iter()
88                    .find(|o| o.kind == OptionKind::Put);
89
90                return match (call, put) {
91                    (Some(c), Some(p)) => Some(OptionStrike {
92                        price: strike,
93                        call: c,
94                        put: p,
95                        delta_k: 0,
96                    }),
97                    _ => None,
98                };
99            })
100            .collect();
101        options_by_strike.sort_unstable_by_key(|s| s.price);
102
103        let mut delta_ks: HashMap<Cents, Cents> = HashMap::new();
104        for w in options_by_strike.windows(3) {
105            match (w.get(0), w.get(1), w.get(2)) {
106                (Some(prev), Some(curr), Some(next)) => {
107                    // Interval between strike prices – half the difference between the strike on either side of Ki:
108                    let delta_k = (next.price - prev.price) / 2;
109                    delta_ks.insert(curr.price, delta_k);
110                }
111                _ => {}
112            };
113        }
114
115        return options_by_strike
116            .into_iter()
117            .map(|mut s| -> OptionStrike {
118                s.delta_k = *delta_ks.get(&s.price).unwrap_or(&0);
119                return s;
120            })
121            .collect();
122    }
123
124    /**
125     * Computes the number of minutes until the option's expiration.
126     */
127    pub fn minutes_to_expiration(&self, now: NaiveDateTime) -> Percentage {
128        return self.expires_at.signed_duration_since(now).num_minutes() as f64;
129    }
130
131    /**
132     * Computes the time to the option's expiration as a percentage of the remaining year.
133     */
134    pub fn time_to_expiration(&self, now: NaiveDateTime) -> Percentage {
135        return self.minutes_to_expiration(now) / 525600.0;
136    }
137
138    /**
139     * Computes the implied forward price.
140     */
141    pub fn forward_price(&self, risk_free_rate: f64, now: NaiveDateTime) -> Cents {
142        let interest = (risk_free_rate * self.time_to_expiration(now)).exp();
143        let mut strikes = self.get_strikes();
144        // we want to find the ATM option
145        strikes.sort_unstable_by_key(|k| k.call_put_difference().abs());
146        let atm = strikes.first();
147        return atm
148            .map(|strike| -> Cents {
149                strike.price + (interest * strike.call_put_difference() as f64) as Cents
150            })
151            .unwrap_or(0);
152    }
153
154    /**
155     * \sigma^2 from the VIX whitepaper
156     */
157    pub fn variance(&self, risk_free_rate: f64, now: NaiveDateTime) -> Percentage {
158        let t = self.time_to_expiration(now);
159        let risk_free_interest = (risk_free_rate * t).exp();
160        let strikes = self.get_strikes();
161        let fp = self.forward_price(risk_free_rate, now);
162
163        let (mut below_and_k, above): (Vec<OptionStrike>, Vec<OptionStrike>) =
164            strikes.into_iter().partition(|x| (*x).price < fp);
165
166        // The highest below the forward price is K
167        below_and_k.sort_unstable_by_key(|k| -k.price);
168        let k = below_and_k.get(0);
169        let k_0 = k.map(|s| s.price).unwrap_or(0);
170
171        let below = below_and_k.get(1..).unwrap_or(&[]);
172
173        // find all out of the money options + the atm option
174        let selected_options = below
175            .into_iter()
176            .map(|s| (s.put, s.delta_k))
177            .chain(above.into_iter().map(|s| (s.call, s.delta_k)))
178            .chain(
179                k.into_iter()
180                    .flat_map(|s| vec![(s.call, s.delta_k), (s.put, s.delta_k)]),
181            )
182            .collect::<Vec<(OptionContract, Cents)>>();
183
184        let contributions: f64 = selected_options
185            .into_iter()
186            .map(|(option, delta_k)| -> f64 {
187                let strike_dollars = option.strike as f64 / 100.0;
188                return (delta_k as f64 / 100.0) / (strike_dollars * strike_dollars)
189                    * (option.mark() as f64 / 100.0)
190                    * risk_free_interest;
191            })
192            .sum();
193
194        let a = fp as f64 / k_0 as f64 - 1.0;
195        return (2.0 * contributions - a * a) / t;
196    }
197}
198
199pub fn group_options_by_expiry(
200    options: &[OptionContract],
201) -> HashMap<NaiveDateTime, OptionsByExpiryDate> {
202    let mut options_by_expiry: HashMap<NaiveDateTime, OptionsByExpiryDate> = HashMap::new();
203
204    for (expires_at, options_for_expiry) in
205        options.into_iter().group_by(|o| o.expires_at).into_iter()
206    {
207        let (calls, puts) = options_for_expiry.partition(|o| o.kind == OptionKind::Call);
208        options_by_expiry.insert(
209            expires_at,
210            OptionsByExpiryDate {
211                expires_at: expires_at,
212                calls: calls,
213                puts: puts,
214            },
215        );
216    }
217    return options_by_expiry;
218}
219
220pub fn compute_vix(
221    near_term: &OptionsByExpiryDate,
222    next_term: &OptionsByExpiryDate,
223    near_term_risk_free_rate: f64,
224    next_term_risk_free_rate: f64,
225    now: NaiveDateTime,
226) -> Percentage {
227    let t1 = near_term.time_to_expiration(now);
228    let n_t1 = near_term.minutes_to_expiration(now);
229    let s1_sq = near_term.variance(near_term_risk_free_rate, now);
230    let t2 = next_term.time_to_expiration(now);
231    let n_t2 = next_term.minutes_to_expiration(now);
232    let s2_sq = next_term.variance(next_term_risk_free_rate, now);
233    let n_30 = (30 * 24 * 60) as f64;
234    let n_365 = (365 * 24 * 60) as f64;
235
236    return ((t1 * s1_sq * (n_t2 - n_30) / (n_t2 - n_t1)
237        + t2 * s2_sq * (n_30 - n_t1) / (n_t2 - n_t1))
238        * n_365
239        / n_30)
240        .powf(0.5)
241        * 100.0;
242}
243
244#[cfg(test)]
245mod tests {
246    #[test]
247    fn it_works() {
248        assert_eq!(2 + 2, 4);
249    }
250}