Metadata-Version: 2.4
Name: qsmile
Version: 0.0.2
Summary: Volatility Smile Modelling
Project-URL: Homepage, https://github.com/markrichardson/qsmile
Project-URL: Repository, https://github.com/markrichardson/qsmile
Project-URL: Issues, https://github.com/markrichardson/qsmile/issues
Author: Mark Richardson
License: MIT
License-File: LICENSE.md
Keywords: ci,configuration,ruff,templates
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.11
Requires-Dist: cvxpy<2.0,>=1.6.0
Requires-Dist: numpy<3.0,>=2.4.0
Requires-Dist: pandas<3.1,>=2.0
Requires-Dist: scipy<2.0,>=1.14.0
Provides-Extra: plot
Requires-Dist: matplotlib<4.0,>=3.9.0; extra == 'plot'
Description-Content-Type: text/markdown

<div align="center">

# qsmile

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Python versions](https://img.shields.io/badge/Python-3.11%20•%203.12%20•%203.13-blue?logo=python)](https://www.python.org/)
[![PyPI - Version](https://img.shields.io/pypi/v/qsmile.svg)](https://pypi.org/project/qsmile/)

![Github](https://img.shields.io/badge/GitHub-181717?style=flat&logo=github)
![Linux](https://img.shields.io/badge/Linux-FCC624?style=flat&logo=linux&logoColor=white)
![macOS](https://img.shields.io/badge/macOS-000000?style=flat&logo=apple&logoColor=white)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg?logo=ruff)](https://github.com/astral-sh/ruff)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)

[![CI](https://github.com/markrichardson/qsmile/actions/workflows/rhiza_ci.yml/badge.svg?event=push)](https://github.com/markrichardson/qsmile/actions/workflows/rhiza_ci.yml)
[![MARIMO](https://github.com/markrichardson/qsmile/actions/workflows/rhiza_marimo.yml/badge.svg?event=push)](https://github.com/markrichardson/qsmile/actions/workflows/rhiza_marimo.yml)
[![CodeFactor](https://www.codefactor.io/repository/github/markrichardson/qsmile/badge)](https://www.codefactor.io/repository/github/markrichardson/qsmile)

**Volatility smile modelling for option trading**

</div>

---

## Overview

**qsmile** is a Python library for fitting parametric volatility smile models to option chain data. It provides bid/ask-aware data containers, Black76 pricing, forward/discount-factor calibration, and least-squares SVI calibration out of the box.

### Key capabilities

- **Bid/ask option prices** — `OptionChain` stores bid/ask call and put prices, and automatically calibrates the forward and discount factor from put-call parity using quasi-delta weighted least squares.
- **Coordinate transforms** — `VolData` is a unified container with `.transform(x, y)` to freely convert between any combination of X-coordinates (Strike, Moneyness, Log-Moneyness, Standardised) and Y-coordinates (Price, Volatility, Variance, Total Variance) via composable, invertible maps.
- **SVI fitting** — Fit the SVI raw parameterisation to `VolData`:

$$w(k) = a + b\left(\rho(k - m) + \sqrt{(k - m)^2 + \sigma^2}\right)$$

where $k = \ln(K/F)$ is log-moneyness and $w$ is total implied variance.

- **Black76 pricing** — Vectorised call/put pricing and explicit closed-form implied vol inversion via `black76_call`, `black76_put`, and `black76_implied_vol`. The inverter uses the inverse-Gaussian quantile representation of [Schadner (2026)](https://arxiv.org/abs/2604.24480), which recovers implied vol to machine precision in a single non-iterative evaluation.
- **Plotting** — All chain types have a `.plot()` method for bid/ask error-bar charts (requires `qsmile[plot]`).

---

## Installation

```bash
pip install qsmile            # core
pip install "qsmile[plot]"    # with matplotlib plotting
```

For development:

```bash
git clone https://github.com/markrichardson/qsmile.git
cd qsmile
make install
```

---

## Quick Start

### From bid/ask prices (full pipeline)

```python +RHIZA_SKIP
import numpy as np
import pandas as pd
from qsmile import OptionChain, SmileMetadata, StrikeArray, SVIModel, VolData, XCoord, YCoord, fit

# Bid/ask prices — forward and DF are calibrated automatically
strikes = np.array([80, 90, 95, 100, 105, 110, 120], dtype=float)
idx = pd.Index(strikes, dtype=np.float64)
sa = StrikeArray()
sa.set(("call", "bid"), pd.Series([20.5, 11.8, 7.5, 4.2, 2.0, 0.8, 0.1], index=idx))
sa.set(("call", "ask"), pd.Series([21.5, 12.4, 8.0, 4.6, 2.3, 1.0, 0.2], index=idx))
sa.set(("put", "bid"), pd.Series([0.1, 0.6, 1.5, 3.1, 5.8, 9.6, 18.8], index=idx))
sa.set(("put", "ask"), pd.Series([0.2, 0.8, 1.8, 3.5, 6.2, 10.2, 19.6], index=idx))

prices = OptionChain(
    strikedata=sa,
    metadata=SmileMetadata(date=pd.Timestamp("2024-01-01"), expiry=pd.Timestamp("2024-07-01")),
)
print(prices.metadata.forward)          # Calibrated forward
print(prices.metadata.discount_factor)  # Calibrated discount factor

# Enter the coordinate transform framework
sd = prices.to_vols()                                      # (FixedStrike, Volatility)
sd_unit = sd.transform(XCoord.StandardisedStrike, YCoord.TotalVariance)  # → unitised

# Fit SVI directly from VolData
result = fit(sd, model=SVIModel)
print(result.model)    # Fitted SVIModel
print(result.rmse)     # Root mean square error
```

### From mid implied vols

```python +RHIZA_SKIP
import numpy as np
import pandas as pd
from qsmile import SmileMetadata, SVIModel, VolData, fit

meta = SmileMetadata(
    date=pd.Timestamp("2024-01-01"),
    expiry=pd.Timestamp("2024-07-01"),
    forward=100.0,
)

sd = VolData.from_mid_vols(
    strikes=np.array([80, 90, 100, 110, 120], dtype=float),
    ivs=np.array([0.28, 0.22, 0.18, 0.17, 0.19]),
    metadata=meta,
)

result = fit(sd, model=SVIModel)
print(result.model)    # Fitted SVIModel
print(result.rmse)     # Root mean square error
```

---

## API Reference

### Data containers

| Class | Description |
|---|---|
| `OptionChain` | Bid/ask call and put prices with automatic forward/DF calibration |
| `VolData` | Unified coordinate-labelled container with `.transform(x, y)` and `.from_mid_vols()` factory |

### Coordinate transforms

```
OptionChain ─── .to_vols() ──→ VolData ─── .transform(x, y) ──→ VolData
VolData.from_mid_vols(...)         ──→ VolData ───────────────────────────┘
```

| Coordinate type | Values |
|---|---|
| X-coordinates | `FixedStrike`, `MoneynessStrike`, `LogMoneynessStrike`, `StandardisedStrike` |
| Y-coordinates | `Price`, `Volatility`, `Variance`, `TotalVariance` |

### Smile fitting

| Function / Class | Description |
|---|---|
| `fit(chain, model)` | Fit any `SmileModel` to `VolData` — generic entry point |
| `SmileModel` | Abstract base dataclass for pluggable smile models (native coords, bounds, evaluate, etc.) |
| `SmileResult` | Fitted result with `.model`, `.residuals`, `.rmse`, `.success` |
| `SVIModel` | SVI model and parameter values `(a, b, rho, m, sigma)` with `.evaluate(k)` and `.implied_vol(k, T)` |
| `SABRModel` | SABR model `(alpha, beta, rho, nu)` with Hagan (2002) lognormal implied vol `.evaluate(k)` |

### Black76 pricing

| Function | Description |
|---|---|
| `black76_call(F, K, D, σ, T)` | Vectorised Black76 call price |
| `black76_put(F, K, D, σ, T)` | Vectorised Black76 put price |
| `black76_implied_vol(price, F, K, D, T)` | Closed-form implied vol inversion via the inverse-Gaussian quantile (Schadner, 2026) |

---

## Development

```bash
make install   # Set up environment
make test      # Run tests with coverage
make fmt       # Format and lint
make marimo    # Launch interactive notebooks
```

---

## License

MIT — see [LICENSE](LICENSE) for details.

