Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions app/hurst.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def _(mo):

We want to construct a mechanism to estimate the Hurst exponent via OHLC data because it is widely available from data providers and easily constructed as an online signal during trading.

In order to evaluate results against known solutions, we consider the Weiner process as generator of timeseries.
In order to evaluate results against known solutions, we consider the Wiener process as generator of timeseries.

We use the **WeinerProcess** from the stochastic process library and sample one path over a time horizon of 1 (day) with a time step every second.
We use the **WienerProcess** from the stochastic process library and sample one path over a time horizon of 1 (day) with a time step every second.
""")
return

Expand All @@ -56,24 +56,24 @@ def _(mo):

@app.cell
def _(regenerate_btn):
from quantflow.sp.weiner import WeinerProcess
from quantflow.sp.wiener import WienerProcess
from quantflow.utils import plot
from quantflow.utils.dates import start_of_day

regenerate_btn

weiner = WeinerProcess(sigma=2.0)
weiner_paths = weiner.sample(n=1, time_horizon=1, time_steps=24*60*60)
weiner_df = weiner_paths.as_datetime_df(start=start_of_day(), unit="d").reset_index()
wiener = WienerProcess(sigma=2.0)
wiener_paths = wiener.sample(n=1, time_horizon=1, time_steps=24*60*60)
wiener_df = wiener_paths.as_datetime_df(start=start_of_day(), unit="d").reset_index()

plot.plot_lines(
weiner_df,
x=weiner_df.columns[0],
y=weiner_df.columns[1],
title="Weiner Process Path",
labels={"value": "Value", "variable": "Path", weiner_df.columns[0]: "Date"},
wiener_df,
x=wiener_df.columns[0],
y=wiener_df.columns[1],
title="Wiener Process Path",
labels={"value": "Value", "variable": "Path", wiener_df.columns[0]: "Date"},
)
return plot, start_of_day, weiner_df, weiner_paths
return plot, start_of_day, wiener_df, wiener_paths


@app.cell
Expand All @@ -90,14 +90,14 @@ def _(mo):
### Realized Variance

At this point we estimate the standard deviation using the **realized variance** along the path (we use the **scaled** flag so that the standard deviation is scaled by the square-root of time step, in this way it removes the dependency on the time step size).
The value should be close to the **sigma** of the WeinerProcess defined above.
The value should be close to the **sigma** of the WienerProcess defined above.
""")
return


@app.cell
def _(weiner_paths):
float(weiner_paths.paths_std(scaled=True)[0])
def _(wiener_paths):
float(wiener_paths.paths_std(scaled=True)[0])
return


Expand All @@ -110,15 +110,15 @@ def _(mo):


@app.cell
def _(weiner_paths):
weiner_paths.hurst_exponent()
def _(wiener_paths):
wiener_paths.hurst_exponent()
return


@app.cell
def _(mo):
mo.md(r"""
As expected, the Hurst exponent should be close to 0.5, since we have calculated the exponent from the paths of a Weiner process.
As expected, the Hurst exponent should be close to 0.5, since we have calculated the exponent from the paths of a Wiener process.
""")
return

Expand All @@ -143,7 +143,7 @@ def _(mo):


@app.cell
def _(weiner_df):
def _(wiener_df):
import pandas as pd
import polars as pl
import math
Expand All @@ -167,7 +167,7 @@ def rstd(pdf: pl.Series, range_seconds: float) -> float:
results = []
for period in ("10s", "20s", "30s", "1m", "2m", "3m", "5m", "10m", "30m"):
ohlc = template.model_copy(update=dict(period=period))
rf = ohlc(weiner_df)
rf = ohlc(wiener_df)
ts = pd.to_timedelta(period).to_pytimedelta().total_seconds()
data = dict(period=period)
for name in ("pk", "gk", "rs"):
Expand Down Expand Up @@ -255,15 +255,15 @@ def ohlc_hurst_exponent(


@app.cell
def _(ohlc_hurst_exponent, weiner_df):
ohlc_hurst_exponent(weiner_df, series=["0"])
def _(ohlc_hurst_exponent, wiener_df):
ohlc_hurst_exponent(wiener_df, series=["0"])
return


@app.cell
def _(mo):
mo.md(r"""
The Hurst exponent should be close to 0.5, since we have calculated the exponent from the paths of a Weiner process. But it is not exactly 0.5 because the range-based estimators are not the same as the realized variance. Interestingly, the Parkinson estimator gives a Hurst exponent closer to 0.5 than the Garman-Klass and Rogers-Satchell estimators.
The Hurst exponent should be close to 0.5, since we have calculated the exponent from the paths of a Wiener process. But it is not exactly 0.5 because the range-based estimators are not the same as the realized variance. Interestingly, the Parkinson estimator gives a Hurst exponent closer to 0.5 than the Garman-Klass and Rogers-Satchell estimators.

## Mean Reverting Time Series

Expand Down
2 changes: 1 addition & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Continuous-time stochastic processes used as underlying models for option pricin

| Module | Description |
|---|---|
| [Weiner Process](sp/weiner.md) | Geometric Brownian motion (constant volatility) |
| [Wiener Process](sp/wiener.md) | Geometric Brownian motion (constant volatility) |
| [Heston Model](sp/heston.md) | Stochastic volatility with optional jump component (HestonJ) |
| [Jump Diffusion](sp/jump_diffusion.md) | Compound Poisson jump processes |
| [CIR Process](sp/cir.md) | Cox-Ingersoll-Ross mean-reverting process |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/sp/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This page gives an overview of all stochastic processes available in the library

| Process | Description |
|---|---|
| [WeinerProcess][quantflow.sp.weiner.WeinerProcess] | Standard Brownian motion |
| [WienerProcess][quantflow.sp.wiener.WienerProcess] | Standard Brownian motion |

### Mean-reverting (intensity)

Expand Down
3 changes: 0 additions & 3 deletions docs/api/sp/weiner.md

This file was deleted.

3 changes: 3 additions & 0 deletions docs/api/sp/wiener.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Wiener process

::: quantflow.sp.wiener.WienerProcess
20 changes: 10 additions & 10 deletions docs/examples/fft.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from docs.examples._utils import assets_path
from quantflow.sp.weiner import WeinerProcess
from quantflow.sp.wiener import WienerProcess
from quantflow.utils import plot

p = WeinerProcess(sigma=0.5)
p = WienerProcess(sigma=0.5)
m = p.marginal(0.2)

fig = plot.plot_characteristic(m)
fig.update_layout(title="Weiner Process Characteristic Function")
fig.write_image(assets_path("weiner_characteristic.png"))
fig.update_layout(title="Wiener Process Characteristic Function")
fig.write_image(assets_path("wiener_characteristic.png"))

fig = plot.plot_marginal_pdf(m, n=128, use_fft=True, max_frequency=20)
fig.update_layout(title="Weiner Process PDF via FFT with n=128")
fig.write_image(assets_path("weiner_fft_128.png"))
fig.update_layout(title="Wiener Process PDF via FFT with n=128")
fig.write_image(assets_path("wiener_fft_128.png"))

fig = plot.plot_marginal_pdf(m, n=128 * 8, use_fft=True, max_frequency=8 * 20)
fig.update_layout(title="Weiner Process PDF via FFT with n=1024")
fig.write_image(assets_path("weiner_fft_1024.png"))
fig.update_layout(title="Wiener Process PDF via FFT with n=1024")
fig.write_image(assets_path("wiener_fft_1024.png"))

fig = plot.plot_marginal_pdf(m, 64)
fig.update_layout(title="Weiner Process PDF via FRFT with n=64")
fig.write_image(assets_path("weiner_64.png"))
fig.update_layout(title="Wiener Process PDF via FRFT with n=64")
fig.write_image(assets_path("wiener_64.png"))
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from quantflow.options.inputs import OptionType
from quantflow.options.pricer import OptionPricer
from quantflow.sp.weiner import WeinerProcess
from quantflow.sp.wiener import WienerProcess

# Weiner process with constant volatility
# Wiener process with constant volatility
# This produces the same sensitivities as the Black-Scholes model
pricer = OptionPricer(model=WeinerProcess(sigma=0.3))
pricer = OptionPricer(model=WienerProcess(sigma=0.3))

# Price an ATM call option at time to maturity 1.0
price = pricer.price(
Expand Down
4 changes: 2 additions & 2 deletions docs/theory/convexity_correction.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ with time, reflecting the fact that geometric Brownian motion drifts upward on a
(the asset price is log-normally distributed with positive variance).

```python
from quantflow.sp.weiner import WeinerProcess
pr = WeinerProcess(sigma=0.5)
from quantflow.sp.wiener import WienerProcess
pr = WienerProcess(sigma=0.5)
-pr.characteristic_exponent(1, complex(0,-1)) # c_t at t=1
```

Expand Down
10 changes: 5 additions & 5 deletions docs/theory/inversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ which means $\delta_u$ and $\delta_x$ cannot be chosen independently — they ar
\delta_x = \frac{2\pi}{N \delta_u}
\end{equation}

As an example, let us invert the characteristic function of the Weiner process, which yields the standard normal distribution.
As an example, let us invert the characteristic function of the Wiener process, which yields the standard normal distribution.

```python
--8<-- "docs/examples/fft.py"
```

![Weiner Characteristic Function](../assets/examples/weiner_characteristic.png)
![Wiener Characteristic Function](../assets/examples/wiener_characteristic.png)

![Weiner FFT 128](../assets/examples/weiner_fft_128.png)
![Wiener FFT 128](../assets/examples/wiener_fft_128.png)

![Weiner FFT 1024](../assets/examples/weiner_fft_1024.png)
![Wiener FFT 1024](../assets/examples/wiener_fft_1024.png)


**Note** the amount of unnecessary discretization points in the frequency domain (the characteristic function is zero after 15 or so). However the space domain is poorly represented because of the FFT constraints (we have a relatively small number of points where it matters, around zero).
Expand All @@ -83,7 +83,7 @@ z &= \left(\left[e^{i j^2 \zeta/2}\right]_{j=0}^{N-1}, \left[e^{i\left(N-j\right

We can now reduce the number of points needed for the discretization and achieve higher accuracy by properly selecting the domain discretization independently.

![Weiner FRFT 64](../assets/examples/weiner_64.png)
![Wiener FRFT 64](../assets/examples/wiener_64.png)

Since one N-point FRFT will invoke three 2N-point FFT procedures, the number of operations will be approximately $6N\log{N}$ compared to $N\log{N}$ for the FFT. However, we can use fewer points as demonstrated and be more robust in delivering results.

Expand Down
2 changes: 1 addition & 1 deletion docs/theory/levy.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The independence and stationarity of the increments of the Lévy process imply t
where the characteristic exponent $\phi_{x_1,u}$ is given by the [Lévy-Khintchine formula](https://en.wikipedia.org/wiki/L%C3%A9vy_process).

There are several Lévy processes in the literature, including the [Poisson process](../api/sp/poisson.md),
the compound Poisson process, and the [Brownian motion](../api/sp/weiner.md).
the compound Poisson process, and the [Brownian motion](../api/sp/wiener.md).

## Time-Changed Lévy Processes

Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/option_pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Model sensitivities such as delta and gamma are calculated using the model dynam
The first example shows how to price an option using the Black-Scholes model and validate the results against the analytical Black formula. The implied volatility should be the same as the model volatility, and the deltas and gammas should be the same as well (within numerical precision).

```python
--8<-- "docs/examples/weiner_volatility_pricer.py"
--8<-- "docs/examples/wiener_volatility_pricer.py"
```

```json
--8<-- "docs/examples/output/weiner_volatility_pricer.out"
--8<-- "docs/examples/output/wiener_volatility_pricer.out"
```


Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ nav:
- Jump Diffusion: api/sp/jump_diffusion.md
- Ornstein-Uhlenbeck: api/sp/ou.md
- Poisson Process: api/sp/poisson.md
- Weiner Process: api/sp/weiner.md
- Wiener Process: api/sp/wiener.md
- Copulas: api/sp/copula.md
- Technical Analysis:
- api/ta/index.md
Expand Down
12 changes: 6 additions & 6 deletions notebooks/models/weiner.md → notebooks/models/wiener.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ kernelspec:
name: python3
---

# Weiner Process
# Wiener Process

In this document, we use the term Weiner process $w_t$ to indicate a Brownian motion with standard deviation given by the parameter $\sigma$; that is to say, the one-dimensional Weiner process is defined as:
In this document, we use the term Wiener process $w_t$ to indicate a Brownian motion with standard deviation given by the parameter $\sigma$; that is to say, the one-dimensional Wiener process is defined as:

1. $w_t$ is Lévy process
2. $d w_t = w_{t+dt}-w_t \sim N\left(0, \sigma dt\right)$ where $N$ is the normal distribution
Expand All @@ -24,9 +24,9 @@ The [](characteristic-exponent) of $w$ is
\end{equation}

```{code-cell}
from quantflow.sp.weiner import WeinerProcess
from quantflow.sp.wiener import WienerProcess

pr = WeinerProcess(sigma=0.5)
pr = WienerProcess(sigma=0.5)
pr
```

Expand All @@ -47,8 +47,8 @@ plot.plot_marginal_pdf(m, 128)

```{code-cell}
from quantflow.options.pricer import OptionPricer
from quantflow.sp.weiner import WeinerProcess
pricer = OptionPricer(WeinerProcess(sigma=0.2))
from quantflow.sp.wiener import WienerProcess
pricer = OptionPricer(WienerProcess(sigma=0.2))
pricer
```

Expand Down
5 changes: 4 additions & 1 deletion quantflow/sp/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ def call_option_carr_madan_alpha(self) -> float:

The choice of alpha is crucial for the numerical stability of the Carr-Madan
formula. A common choice is to set alpha to a value that ensures the integrand
decays sufficiently fast at high frequencies.
decays sufficiently fast at high frequencies. The maturity-dependent heuristic
below (high alpha for short maturities, decaying to a 0.5 floor for long ones)
has been found to work well in practice across the processes in this library;
override on a per-process basis if needed.
"""
return max(8 * np.max(np.exp(-2 * self.t)), 0.5)

Expand Down
24 changes: 14 additions & 10 deletions quantflow/sp/cir.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
from .base import IntensityProcess


class SamplingAlgorithm(str, enum.Enum):
euler = "euler"
milstein = "milstein"
implicit = "implicit"
class SamplingAlgorithm(enum.StrEnum):
EULER = enum.auto()
MILSTEIN = enum.auto()
IMPLICIT = enum.auto()


class CIR(IntensityProcess):
Expand All @@ -36,7 +36,7 @@ class CIR(IntensityProcess):
sigma: float = Field(default=1.0, gt=0, description=r"Volatility $\sigma$")
theta: float = Field(default=1.0, gt=0, description=r"Mean rate $\theta$")
sample_algo: SamplingAlgorithm = Field(
default=SamplingAlgorithm.implicit, description="Sampling algorithm"
default=SamplingAlgorithm.IMPLICIT, description="Sampling algorithm"
)

@property
Expand Down Expand Up @@ -64,14 +64,14 @@ def sample(

def sample_from_draws(self, paths: Paths, *args: Paths) -> Paths:
match self.sample_algo:
case SamplingAlgorithm.euler:
case SamplingAlgorithm.EULER:
return self.sample_euler(paths)
case SamplingAlgorithm.milstein:
case SamplingAlgorithm.MILSTEIN:
return self.sample_euler(paths, 0.25)
case SamplingAlgorithm.implicit:
case SamplingAlgorithm.IMPLICIT:
return self.sample_implicit(paths)

def sample_euler(self, draws: Paths, ic: float = 0.0) -> Paths:
def sample_euler(self, draws: Paths, milstein_coef: float = 0.0) -> Paths:
kappa = self.kappa
theta = self.theta
dt = draws.dt
Expand All @@ -83,7 +83,11 @@ def sample_euler(self, draws: Paths, ic: float = 0.0) -> Paths:
w = sdt * draws.data[t, :]
x = paths[t, :]
xplus = np.clip(x, 0, None)
dx = kappa * (theta - xplus) * dt + np.sqrt(xplus) * w + ic * (w * w - sdt2)
dx = (
kappa * (theta - xplus) * dt
+ np.sqrt(xplus) * w
+ milstein_coef * (w * w - sdt2)
)
paths[t + 1, :] = x + dx
return Paths(t=draws.t, data=paths)

Expand Down
Loading
Loading