Option-Implied Moments & Diagnostics in Pure R — a faithful port of the Python package vilkovgr/qmoms with identical function signatures, output keys, and defaults.
⚖️ Scientific intent:
rqmoms
aims for numerical identity with the reference Python implementation for a given IV surface and parameter set. A dev-only Python reference workflow (ignored in builds) can be used locally to double-check results.
Installation
Soon, installation should be possible from CRAN
# From source (development)
install.packages("rqmoms")
util then it can be installed from github with:
# install.packages("devtools")
devtools::install_github("sstoeckl/rqmoms")
rqmoms
imports: dplyr
, tibble
, purrr
, cli
, stats
, vctrs
, pracma
.
Packaged example data
This package ships two datasets (loaded automatically with library(rqmoms)
):
-
qmoms_surface
— IV surface snapshots (columns:id
,date
,days
,mnes
,impl_volatility
, optional:delta
,best_bid
,best_offer
,open_interest
,prem
) -
qmoms_zerocd
— zero curve bydate
anddays
(in decimal; e.g.0.02
= 2% p.a.)
They are derived from the Python repo’s data/surface.csv
and data/zerocd.csv
.
library(rqmoms)
str(qmoms_surface)
> Classes 'tbl_df', 'tbl' and 'data.frame': 234 obs. of 7 variables:
> $ id : int 10078 10078 10078 10078 10078 10078 10078 10078 10078 10078 ...
> $ date : Date, format: "1996-01-04" "1996-01-04" ...
> $ days : int 30 30 30 30 30 30 30 30 30 30 ...
> $ mnes : num 0.843 0.853 0.879 0.886 0.912 ...
> $ prem : num 0.191 0.0277 0.1657 0.0372 0.1442 ...
> $ impl_volatility: num 0.847 0.778 0.84 0.777 0.834 ...
> $ delta : num 80 -20 75 -25 70 -30 65 -35 -40 60 ...
head(qmoms_zerocd)
> date days rate
> 1 1996-01-02 9 5.763067
> 2 1996-01-02 15 5.745902
> 3 1996-01-02 50 5.673317
> 4 1996-01-02 78 5.608884
> 5 1996-01-02 169 5.473762
> 6 1996-01-02 260 5.352667
Quick start — single surface (mirrors Python qmoms_compute()
)
params <- rq_default_params()
# pick one (id, date, days)
one <- subset(qmoms_surface,
id == qmoms_surface$id[1] & days == qmoms_surface$days[1])
# rate from the zero curve (interpolated at this date/maturity)
r30 <- get_rate_for_maturity(qmoms_zerocd,
date = one$date[1],
days = one$days[1])
res <- qmoms_compute(
mnes = one$mnes,
vol = one$impl_volatility,
days = one$days[1],
rate = r30,
params = params,
output = "list"
)
str(res)
> List of 17
> $ nopt : int 234
> $ smfiv : num 6.39
> $ mfiv_bkm : num 3.1
> $ mfiv_bjn : num 3.92
> $ smfivd : num 0.0076
> $ mfivd_bkm : num 0.0115
> $ mfivd_bjn : num 0.01
> $ mfis : num -0.0476
> $ mfik : num 3.28
> $ cvix_sigma2: num 1.94
> $ cvix_mnes20: num 1.2
> $ rix : num 0.00151
> $ rixnorm : num 0.131
> $ tlm_sigma2 : num NA
> $ tlm_delta20: num NA
> $ slopedn : num NA
> $ slopeup : num -2.29e-15
Whole-dataframe — grouped compute (mirrors Python README)
First, merge/interpolate rates into the surface:
suppressMessages(library(dplyr))
# exact-maturity rate interpolation by date
surf_r <- get_rate_for_maturity(qmoms_zerocd, df_surf = qmoms_surface)
A) dplyr group_map()
res_bygroup <- surf_r |>
dplyr::group_by(id, date, days) |>
dplyr::group_map(~{
# .x: rows of the group (no keys); .y: one-row tibble of keys
tibble::as_tibble(
c(
list(id = .y$id[[1]], date = .y$date[[1]], days = .y$days[[1]]),
qmoms_compute(
mnes = .x$mnes,
vol = .x$impl_volatility,
days = .y$days[[1]],
rate = .x$rate[1],
params = params,
output = "list"
)
)
)
}) |>
dplyr::bind_rows()
dplyr::glimpse(res_bygroup)
> Rows: 9
> Columns: 20
> $ id <int> 10078, 10078, 10078, 10078, 10078, 10078, 10078, 10078, 10…
> $ date <date> 1996-01-04, 1996-01-05, 1996-01-08, 1996-01-09, 1996-01-1…
> $ days <int> 30, 30, 30, 30, 30, 30, 30, 30, 30
> $ nopt <int> 26, 26, 26, 26, 26, 26, 26, 26, 26
> $ smfiv <dbl> 6.079506, 6.118586, 6.155363, 6.197620, 6.356862, 6.012891…
> $ mfiv_bkm <dbl> 3.039818, 3.041774, 3.046623, 3.066407, 3.084455, 3.030018…
> $ mfiv_bjn <dbl> 3.809570, 3.818995, 3.830234, 3.855023, 3.901564, 3.788134…
> $ smfivd <dbl> 0.003832652, 0.003139576, 0.003262268, 0.006018649, 0.0040…
> $ mfivd_bkm <dbl> 0.004951385, 0.003880698, 0.004068641, 0.007911383, 0.0051…
> $ mfivd_bjn <dbl> 0.004538752, 0.003611914, 0.003775383, 0.007208670, 0.0047…
> $ mfis <dbl> -0.090644976, -0.024247430, -0.015743917, -0.109163284, 0.…
> $ mfik <dbl> 3.262624, 3.129021, 3.101145, 3.287541, 3.001450, 3.364392…
> $ cvix_sigma2 <dbl> 1.899215, 1.900327, 1.903911, 1.915603, 1.926735, 1.893552…
> $ cvix_mnes20 <dbl> 1.197292, 1.194654, 1.194702, 1.202204, 1.198877, 1.198188…
> $ rix <dbl> 0.0004126325, 0.0002687845, 0.0002932578, 0.0007027131, 0.…
> $ rixnorm <dbl> 0.08333678, 0.06926188, 0.07207760, 0.08882305, 0.07299096…
> $ tlm_sigma2 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA
> $ tlm_delta20 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA
> $ slopedn <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA
> $ slopeup <dbl> -1.637620e-15, -3.249709e-15, 1.608695e-15, 2.615529e-15, …
B) Convenience wrapper qmoms_compute_bygroup()
If you prefer, you can call the wrapper on each group.
res_bygroup2 <- surf_r |>
dplyr::group_by(id, date, days) |>
dplyr::group_map(~{
qmoms_compute_bygroup(
list(.x, params),
id = .y$id[[1]],
date = .y$date[[1]],
days = .y$days[[1]],
rate = .x$rate[1]
)
}) |>
dplyr::bind_rows()
# Same result (up to column order)
all.equal(
dplyr::arrange(res_bygroup, id, date, days),
dplyr::arrange(res_bygroup2, id, date, days)
)
> [1] TRUE
C) Column mapping example (mirrors Python column/dtype mapping)
# rename columns
df_alt <- surf_r |>
dplyr::rename(ID = id, Date = date, Days = days, Rate = rate, M = mnes, IV = impl_volatility)
# mapping for the wrapper
map <- list(id = "ID", date = "Date", days = "Days", rate = "Rate", mnes = "M", impl_volatility = "IV")
# one group through the wrapper
gs <- df_alt |>
dplyr::group_by(ID, Date, Days) |>
dplyr::group_split()
ex_mapped <- qmoms_compute_bygroup(
list(gs[[1]], params),
id = dplyr::first(gs[[1]]$ID),
date = dplyr::first(gs[[1]]$Date),
days = dplyr::first(gs[[1]]$Days),
rate = dplyr::first(gs[[1]]$Rate),
cols_map = map
)
ex_mapped
> # A tibble: 1 × 20
> id date days nopt smfiv mfiv_bkm mfiv_bjn smfivd mfivd_bkm
> <int> <date> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
> 1 10078 1996-01-04 30 26 6.08 3.04 3.81 0.00383 0.00495
> # ℹ 11 more variables: mfivd_bjn <dbl>, mfis <dbl>, mfik <dbl>,
> # cvix_sigma2 <dbl>, cvix_mnes20 <dbl>, rix <dbl>, rixnorm <dbl>,
> # tlm_sigma2 <dbl>, tlm_delta20 <dbl>, slopedn <dbl>, slopeup <dbl>
Parameters — defaults & customization
Defaults mirror Python (grid, filters, CVIX/TLM windows, slope windows):
rq_default_params()
> $atmfwd
> [1] FALSE
>
> $grid
> $grid$number_points
> [1] 500
>
> $grid$grid_limit
> [1] 2
>
>
> $filter
> $filter$mnes_lim
> [1] 0 1000
>
> $filter$delta_put_lim
> [1] -0.499 0.000
>
> $filter$delta_call_lim
> [1] 0.0 0.5
>
> $filter$best_bid_zero
> [1] -1
>
> $filter$open_int_zero
> [1] -1
>
> $filter$min_price
> [1] 0
>
>
> $semivars
> $semivars$compute
> [1] TRUE
>
>
> $mfismfik
> $mfismfik$compute
> [1] TRUE
>
>
> $cvix
> $cvix$compute
> [1] TRUE
>
> $cvix$abs_dev
> $cvix$abs_dev[[1]]
> [1] 0.2
>
>
> $cvix$vol_dev
> $cvix$vol_dev[[1]]
> [1] 2
>
>
>
> $rix
> $rix$compute
> [1] TRUE
>
>
> $tlm
> $tlm$compute
> [1] TRUE
>
> $tlm$delta_lim
> $tlm$delta_lim[[1]]
> [1] 20
>
>
> $tlm$vol_lim
> $tlm$vol_lim[[1]]
> [1] 2
>
>
>
> $slope
> $slope$compute
> [1] TRUE
>
> $slope$deltaP_lim
> [1] -0.50 -0.05
>
> $slope$deltaC_lim
> [1] 0.05 0.50
Change what’s computed and tweak windows
params2 <- rq_default_params()
# Example: turn off TLM, adjust slope & CVIX windows
params2 <- utils::modifyList(params2, list(
tlm = list(compute = FALSE),
slope = list(compute = TRUE, deltaP_lim = c(-0.4, -0.1), deltaC_lim = c(0.05, 0.5)),
cvix = list(compute = TRUE, abs_dev = list(0.1, 0.2, 0.3), vol_dev = list(1.5, 2))
))
one <- subset(qmoms_surface, id == qmoms_surface$id[1] & days == qmoms_surface$days[1])
r30 <- get_rate_for_maturity(qmoms_zerocd, date = one$date[1], days = one$days[1])
res_custom <- qmoms_compute(one$mnes, one$impl_volatility, one$days[1], r30, params2, output = "list")
str(res_custom[names(res_custom)[startsWith(names(res_custom), "cvix_")]])
> List of 2
> $ cvix_sigma2: num 1.94
> $ cvix_mnes20: num 1.2
Dev: Python parity vignette
A vignette “Python parity: R vs Python outputs” reproduces the grouped examples and prints side-by-side diffs for all metrics.
-
Coming soon:
vignettes/python-parity.Rmd
- It will appear on the pkgdown site under Articles → Python parity.
(The vignette is optional and only for developers who keep a local clone of the Python repo in python/qmoms_src
with a private virtualenv. The R package itself is pure R.)
What does qmoms_compute()
return?
Keys match the Python package exactly:
Key | Meaning (brief) |
---|---|
nopt |
Number of options used |
smfiv |
Martin’s model-free implied variance |
mfiv_bkm |
BKM (Bakshi–Kapadia–Madan) variance kernel |
mfiv_bjn |
Alternative variance kernel |
smfivd |
Semi-variance (downside) |
mfivd_bkm |
Semi-variance (BKM kernel) |
mfivd_bjn |
Semi-variance (BJN kernel) |
mfis |
Implied skewness (BKM) |
mfik |
Implied kurtosis (BKM) |
cvix_* |
Curvature window integrals (by vol window sigmaX or abs moneyness window mnesY ) |
rix , rixnorm
|
Difference & normalized difference between BKM and BJN downside integrals |
tlm_* |
Tail loss measure for delta/vol windows |
slopedn /slopeup
|
OLS slope of IV on Δ in put/call windows (call side reported with a negative sign) |
Citation & Credits
This package is a pure-R re-implementation of vilkovgr/qmoms. If you use rqmoms
, please also cite the original Python package and related research by the authors.