In the last issue of the Deep Dive into Showcase series, we covered Implied Volatility (IV). It took advantage of multiple Snapshot Indexer HTTPS and advanced Algorithm Lens, which allows for per-user configuration of calculation parameters. This showcase also focuses on Algorithm Lens, a project that uses multiple Snapshot Indexers and performs advanced calculations.
https://github.com/horizonx-tech/chainsight-showcase/tree/main/volatility_index_spx
Before explaining each component, we will explain the Volatility Index (VIX), which is the subject of this article.
What is Volatility Index?
The Volatility Index is the market's expectation of the strength (volatility) of the S&P 500 (SPX) over the short term. The VIX is calculated from the price of the SPX index option near its maturity date and represents the market's expectation for volatility over the next 30 days. It is considered an index that reflects market risk and investor sentiment and is called a "investor fear gauge" Generally, the higher the value of the VIX, the stronger the sense of uncertainty about the future.
In this Showcase Algorithm Lens, we are calculating the VIX for SP500, and although the inputs and flow required to calculate the VIX is complex, it can be broken down into four steps
-
Calculation of Near-term and Next-term of the Option to be calculated
-
Calculation of Interest Rate
-
Calculation of variance of Near-term and Next-term options
Calculation of Strike Price, Filtering of ATM and OTM options
-
Calculation of VIX
Source: https://cdn.cboe.com/api/global/us_indices/governance/VIX_Methodology.pdf
We now turn our attention to the Near-term and Next-term calculations, where two maturity dates are selected based on 30 days later, and the corresponding options are included in the calculation.
Source: https://financestu.com/vix-formula/#1_Options_Used_in_the_Calculation
These two types of maturity dates are called Near-term and Next-term. Depending on the date of calculation, the maturity date (and options belonging to it) corresponding to this Near-term and Next-term will change. Snapshot Indexer HTTPS takes into account the changes in the calculation target that may change due to the execution time of this process.
Variable request parameters in Snapshot Indexer HTTPS
As a quick recap of the previous point, Snapshot Indexer HTTPS periodically makes requests to a "static" URL generated from a configuration specified in the manifest and stores the data. In this Showcase, we need option data for the maturity dates corresponding to Near-term and Next-term, depending on the execution time. This means that at least two different Snapshot Indexers are required. If the maturity date information is required in the request parameters to retrieve this data, then a single Snapshot Indexer will hold the optional data for the specified maturity date. The maturity dates of the options used in the VIX calculation are shifted in accordance with the execution time, so new maturity date option data is required each time. Building a new Snapshot Indexer for this purpose each time would be very costly. To address these cases, Snapshot Indexer HTTPS provides a mechanism to "dynamically" set request parameters.
Set datasource.queries.type=dynamic
in the manifest to set "dynamic" parameters.
datasource:
url: https://query1.finance.yahoo.com/v7/finance/options/%5ESPX
# ex: https://query1.finance.yahoo.com/v7/finance/options/%5ESPX?straddle=false&date=1704672000
headers:
Content-Type: application/json
queries:
type: dynamic
Adding this setting to the code generates the template code for dynamically calculating parameters.
use std::collections::BTreeMap;
pub fn get_query_parameters() -> BTreeMap<String, String> {
BTreeMap::from([
("param1".to_string(), "value1".to_string()),
("param2".to_string(), "value2".to_string()),
])
}
By implementing the logic so that the Map structure has key and value, you can incorporate your own dynamic parameter generation. This time, the maturity date to be calculated must be specified in the query parameter. For this purpose, the maturity date should be calculated for each execution as follows.
const SECS_FOR_DAY: u64 = 24 * 60 * 60;
const SECS_FOR_WEEK: u64 = SECS_FOR_DAY * 7;
pub fn get_query_parameters() -> BTreeMap<String, String> {
let now_secs = ic_cdk::api::time() / (1000 * 1000000);
let current_day = now_secs / SECS_FOR_DAY * SECS_FOR_DAY;
let rounded_epoch = current_day / SECS_FOR_WEEK * SECS_FOR_WEEK;
let fri = rounded_epoch + SECS_FOR_DAY;
let near_term = if current_day - rounded_epoch < 6 {
fri + SECS_FOR_WEEK * 4
} else {
fri + SECS_FOR_WEEK * 5
};
BTreeMap::from([
("straddle".to_string(), "false".to_string()),
("date".to_string(), near_term.to_string()),
])
}
Now, in order to calculate the Volatility Index, only two Snapshot Indexers (Near-term and Next-term) can be used to obtain the options to be calculated.
The final project is build from
- Snapshot Indexer HTTPS x 2 for the Option data mentioned above
- Snapshot Indexer HTTPS to accumulate U.S. Treasury Bond Yield data to calculate interest rate
- Algorithm Lens to calculate VIX
and components for termination processing, such as Relayer for propagation.
Below are all of the Showcase resources described in this article.
https://github.com/horizonx-tech/chainsight-showcase/tree/main/volatility_index_spx
This article focused on Snapshot Indexer HTTPS, which supports variable request parameters. Another feature of this Showcase is the Algorithm Lens, which implements heavy VIX calculations. Although not limited to Internet Computer, WASM modules under the blockchain are often forced to be implemented in a no_std environment, and the VIX calculation logic is also implemented in no_std, which is one point to note. And although the code implemented is canister code running on Internet Computer / Chainsight Platform, it can be written in pure Rust, so you can write easy-to-understand module splits and unit tests, and these can also be used as a reference. Some of the code is reproduced below.
// src/logics/lens_vix_spx/src/calc/variance.rs
#[derive(Clone, Debug, PartialEq)]
pub struct Option {
pub strike_price: f64,
pub bid: f64,
pub ask: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ParamVariance {
pub options: Vec<Option>,
pub time_to_expiration: f64, // T
pub risk_free_rate: f64, // R
pub forward_price: f64,
pub k_0: f64,
}
pub fn variance_per_term(p: ParamVariance) -> f64 {
let mut options_for_contribution = vec![];
let last_idx = p.options.len() - 1;
for (idx, option) in p.options.iter().enumerate() {
let delta_k = match idx {
idx if idx == last_idx => {
option.strike_price - p.options[idx - 1].strike_price
}
idx if idx == 0 => {
p.options[idx + 1].strike_price - option.strike_price
},
_ => {
(p.options[idx + 1].strike_price - p.options[idx - 1].strike_price) / 2.0
}
};
let mid_price = (option.bid + option.ask) / 2.0;
options_for_contribution.push(OptionForContribution {
strike_price: option.strike_price,
mid_price,
delta_k,
});
}
let left = variance_left_part(options_for_contribution, p.risk_free_rate, p.time_to_expiration);
let right = variance_right_part(p.forward_price, p.k_0, p.time_to_expiration);
left - right
}
fn variance_left_part(options: Vec<OptionForContribution>, risk_free_rate: f64, time_to_expiration: f64) -> f64 {
let sum_contributions = calculate_sum_contributions(options, risk_free_rate, time_to_expiration);
sum_contributions * 2.0 / time_to_expiration
}
fn variance_right_part(f: f64, k_0: f64, t: f64) -> f64 {
(f / k_0 - 1.0).powi(2) / t
}
#[derive(Clone, Debug, PartialEq)]
pub struct OptionForContribution {
pub strike_price: f64,
pub mid_price: f64,
pub delta_k: f64,
}
fn calculate_sum_contributions(options: Vec<OptionForContribution>, risk_free_rate: f64, time_to_expiration: f64) -> f64 {
let mut sum_contributions = 0.0;
for option in options {
sum_contributions += contribution_per_option(option, risk_free_rate, time_to_expiration);
}
sum_contributions
}
fn contribution_per_option(option: OptionForContribution, risk_free_rate: f64, time_to_expiration: f64) -> f64 {
let numerator = option.delta_k * (risk_free_rate * time_to_expiration).exp() * option.mid_price;
numerator / option.strike_price.powf(2.0)
}
#[cfg(test)]
mod tests {
use crate::calc::k::{calculate_f, ParamF, find_closest_less_than_f};
use super::*;
// https://cdn.cboe.com/api/global/us_indices/governance/VIX_Methodology.pdf
#[test]
fn test_contribution_per_option() {
// 1370 Put
let mid_price = (0.05 + 0.35) / 2.0; // (call + put) / 2
let delta_k = 1375.0 - 1370.0; // NOTE: use ki because this option is the lowest
assert_eq!(
contribution_per_option(
OptionForContribution {
strike_price: 1370.0,
mid_price,
delta_k
},
0.00031664, // risk_free_rate
34484.0 / 525600.0, // time_to_expiration
),
0.0000005328045045527672
)
}
#[test]
fn test_variance_right_part_near_term() {
let t1 = 34484.0 / 525600.0;
let r1 = 0.00031664;
let f = calculate_f(ParamF {
strike_price: 1965.0,
call_price: 21.05,
put_price: 23.15,
risk_free_rate: r1,
time_to_expiration: t1,
});
let list = vec![
1955.0,
1960.0,
1965.0,
1970.0
];
let k_0 = list.get(find_closest_less_than_f(f, list.clone()).unwrap()).unwrap();
assert_eq!(
variance_right_part(f, *k_0, t1),
0.0000333663350403073
);
}
#[test]
fn test_variance_right_part_next_term() {
let t2 = 44954.0 / 525600.0;
let r2 = 0.00028797;
let f = calculate_f(ParamF {
strike_price: 1960.0,
call_price: 27.30,
put_price: 24.90,
risk_free_rate: r2,
time_to_expiration: t2,
});
let list = vec![
1955.0,
1960.0,
1965.0,
1970.0
];
let k_0 = list.get(find_closest_less_than_f(f, list.clone()).unwrap()).unwrap();
assert_eq!(
variance_right_part(f, *k_0, t2),
0.000017531486804492088
);
}
}
// src/logics/lens_vix_spx/src/calc/vix.rs
pub struct ParamVixPerTerm {
pub variance: f64,
pub t: f64,
pub minites_until_t: f64,
}
pub struct ParamVix {
pub near: ParamVixPerTerm,
pub next: ParamVixPerTerm,
}
pub const N_30: f64 = 30.0 * 24.0 * 60.0; // 43200
pub const N_365: f64 = 365.0 * 24.0 * 60.0; // 525600
pub fn calculate_vix(p: ParamVix) -> f64 {
let ParamVix { near, next } = p;
let contribution_near = near.t * near.variance
* ((next.minites_until_t - N_30) / (next.minites_until_t - near.minites_until_t));
let contribution_next = next.t * next.variance
* ((N_30 - near.minites_until_t) / (next.minites_until_t - near.minites_until_t));
let inner_square_root = (contribution_near + contribution_next) * (N_365 / N_30);
100.0 * inner_square_root.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
// https://cdn.cboe.com/api/global/us_indices/governance/VIX_Methodology.pdf
#[test]
fn test_calculate_vix_from_approximation() {
assert_eq!(
calculate_vix(ParamVix {
near: ParamVixPerTerm {
variance: 0.019233906,
t: 0.0656088,
minites_until_t: 34484.0,
},
next: ParamVixPerTerm {
variance: 0.019423884,
t: 0.0855289,
minites_until_t: 44954.0,
}
}),
13.927840480842871
)
}
#[test]
fn test_calculate_vix_from_measured() {
assert_eq!(
calculate_vix(ParamVix {
near: ParamVixPerTerm {
variance: 0.019233906397055467,
t: 34484.0 / 525600.0,
minites_until_t: 34484.0,
},
next: ParamVixPerTerm {
variance: 0.01942388426632579,
t: 44954.0 / 525600.0,
minites_until_t: 44954.0,
}
}),
13.927842342097524
)
}
}
This time, we introduced the VIX project Showcase centered on the Snapshot Indexer HTTPS, which can update acquired data with variable parameters. Stay tuned for the next installment!
Follow us on Twitter and Medium for updates! We look forward to seeing you there!
Written by Megared@Chainsight