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 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 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 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(|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 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 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 pub fn time_to_expiration(&self, now: NaiveDateTime) -> Percentage {
135 return self.minutes_to_expiration(now) / 525600.0;
136 }
137
138 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 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 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 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 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}