JAX-native framework for generating reproducible market scenarios, benchmarking portfolio strategies against them, and publishing results as versioned datasets with a hosted Leaderboard
Project description
QuantScenarioBench
A JAX-native Python framework for generating reproducible stochastic market scenarios and benchmarking portfolio strategies against them, with built-in export to Parquet and the Hugging Face Hub.
from quantscenariobench.api import simulate
from quantscenariobench.interface import TimeGrid
from quantscenariobench.models import Heston
import jax.numpy as jnp
model = Heston(mu=0.0, kappa=2.0, theta=0.04, xi=0.3, rho=-0.7, v0=0.04, S0=100.0)
tg = TimeGrid(jnp.linspace(0.0, 1.0, 253)) # 252 daily steps, 1 year
scenario = simulate(model, tg, n_paths=10_000, seed=42)
print(scenario.observation.shape) # (10000, 253) — asset price paths
print(scenario.latent_state.shape) # (10000, 253) — variance paths
QuantScenarioBench is more than a published dataset — it's an end-to-end, Hugging Face-native pipeline:
- Scenario generation — reproducible price paths from established stochastic volatility models (
quantscenariobench.api,.models) - Benchmark Core — a shared interface, baselines, and metrics for scoring portfolio strategies against those Scenarios (
quantscenariobench.benchmark) - Evaluation Results — a versioned, JSON-native record of each benchmark run, derived from
BenchmarkResult(quantscenariobench.benchmark.evaluation) - Leaderboard aggregation — a ranked strategy × Benchmark Dataset table built from every published Evaluation Result
- Leaderboard Space — a hosted, sortable, filterable Hugging Face Space that renders that ranked table live, so anyone can browse results without running any code (
spaces/leaderboard/, see Leaderboard Space) - Hugging Face-native workflow throughout — datasets, dataset cards, and Evaluation Results all publish to and load from the Hub with the same functions used for local storage
Why QuantScenarioBench?
Quantitative finance research routinely benchmarks models against each other, but reproducible, openly published path datasets — and a standardized way to score strategies against them — are rare. QuantScenarioBench is useful for model benchmarking, evaluation, stress testing, and general experimentation with stochastic volatility models, and makes it straightforward to:
- Generate large batches of price paths from established stochastic volatility models with a single call
- Compare models on a shared schema — every
Scenariohas the same fields regardless of the model used - Export results to Parquet or publish directly to the Hugging Face Hub
- Reproduce any result exactly — the same
(model, time_grid, n_paths, seed)always produces bit-identical paths on the same computational backend - Benchmark portfolio strategies against generated Scenarios with a shared
run_benchmark()pipeline and standardized performance metrics - Publish, aggregate, and browse benchmark runs as versioned Evaluation Results, ranked into a Leaderboard table you can query yourself or browse live on the hosted Leaderboard Space
Installation
pip install quantscenariobench
Requirements: Python ≥ 3.11. Core dependencies — jax, diffrax, equinox, pyarrow, scipy (Optimizer Solver Layer for GMV's long-only path and CVaR Optimization), and huggingface_hub (Hugging Face Hub publishing, both datasets and Evaluation Results) — install automatically; there is no separate opt-in step for Hugging Face support.
For development (running the test suite, loading datasets in examples):
pip install "quantscenariobench[dev]" # adds pytest, pandas, datasets
Quick Start
1. Generate a scenario
import quantscenariobench # enables JAX float64 globally
from quantscenariobench.api import simulate
from quantscenariobench.interface import TimeGrid
from quantscenariobench.models import BlackScholes, Heston, RoughBergomi
import jax.numpy as jnp
tg = TimeGrid(jnp.linspace(0.0, 1.0, 253)) # daily steps over 1 year
# Black-Scholes (no latent state)
bs = BlackScholes(mu=0.0, sigma=0.2, S0=100.0)
s = simulate(bs, tg, n_paths=50_000, seed=0)
print(s.observation.shape) # (50000, 253)
print(s.latent_state.shape) # (50000, 0) — empty for GBM
# Heston stochastic volatility
heston = Heston(mu=0.0, kappa=2.0, theta=0.04, xi=0.3, rho=-0.7, v0=0.04, S0=100.0)
s = simulate(heston, tg, n_paths=50_000, seed=0)
print(s.observation.shape) # (50000, 253) — asset price paths
print(s.latent_state.shape) # (50000, 253) — variance paths
# Rough Bergomi (non-Markovian, Hurst H=0.1)
rb = RoughBergomi(H=0.1, eta=1.5, rho=-0.7, xi0=0.04, S0=100.0, mu=0.0)
s = simulate(rb, tg, n_paths=50_000, seed=0)
2. Export to Parquet
from quantscenariobench.export import export_parquet
export_parquet([s], "my_dataset.parquet")
Every exported file follows the same 12-column schema regardless of model:
| Column | Type | Description |
|---|---|---|
observation |
list<float64> |
Asset price path (one row per path) |
latent_state |
list<float64> |
Latent state path (empty for Black-Scholes) |
seed |
int64 |
Integer PRNG seed |
prng_key_info |
string |
JAX PRNGKey derivation description |
model_name |
string |
Market model class name |
model_version |
string |
Model specification version |
parameters |
string |
JSON-encoded model parameters |
time_grid |
string |
JSON-encoded time points |
n_paths |
int64 |
Number of simulation paths |
library_version |
string |
quantscenariobench version |
dataset_version |
string |
Dataset version identifier |
generated_at |
string |
UTC ISO-8601 generation timestamp |
3. Publish to Hugging Face Hub
from quantscenariobench.export import publish_to_hub
url = publish_to_hub([s], "my-org/my-dataset", token="hf_...")
print(url) # https://huggingface.co/datasets/my-org/my-dataset
publish_to_hub writes the Parquet file and a dataset card (README.md) in a single call, then uploads both to the Hub.
4. Replay paths deterministically
scenario, dW = simulate(heston, tg, n_paths=1000, seed=7, return_randomness=True)
# Later, reproduce the exact same paths from the saved increments
replayed = simulate(heston, tg, n_paths=1000, seed=7, randomness=dW)
Available Models
Black-Scholes (BlackScholes)
Geometric Brownian Motion:
$$dS_t = \mu,S_t,dt + \sigma,S_t,dW_t$$
| Parameter | Description |
|---|---|
mu |
Drift (use 0.0 for risk-neutral) |
sigma |
Constant volatility |
S0 |
Initial asset price |
Heston (Heston)
Stochastic volatility with mean-reverting variance:
$$dS_t = \mu,S_t,dt + \sqrt{v_t},S_t,dW^S_t$$ $$dv_t = \kappa(\theta - v_t),dt + \xi,\sqrt{v_t},dW^v_t, \quad \text{Corr}(dW^S, dW^v) = \rho$$
| Parameter | Description |
|---|---|
mu |
Asset drift |
kappa |
Variance mean-reversion speed |
theta |
Long-run variance |
xi |
Vol-of-vol |
rho |
Asset–variance correlation (leverage effect) |
v0 |
Initial variance |
S0 |
Initial asset price |
The Feller condition 2κθ ≥ ξ² ensures the variance process stays positive. Violation emits a QuantScenarioBenchValidationWarning.
Rough Bergomi (RoughBergomi)
Non-Markovian stochastic volatility driven by fractional Brownian motion:
$$V_t = \xi_0 \exp!\left(\eta,W^H_t - \tfrac{1}{2}\eta^2 t^{2H}\right), \quad dS_t = \mu,S_t,dt + \sqrt{V_t},S_t,dW^S_t$$
where $W^H_t$ is a Riemann–Liouville fractional Brownian motion with Hurst exponent $H$, discretised via the Volterra representation.
| Parameter | Description |
|---|---|
H |
Hurst exponent; H < 0.5 for rough volatility (empirically H ≈ 0.1) |
eta |
Vol-of-vol amplitude |
rho |
Correlation between asset BM and fBM driver |
xi0 |
Initial variance level |
S0 |
Initial asset price |
mu |
Asset drift |
Correlated Multi-Asset Scenarios
Independently-simulated assets have near-zero expected correlation, which makes GMV/CVaR/HRP-style strategies degenerate toward Equal Weight. simulate_correlated_basket() simulates N single-asset models with Brownian increments correlated by a validated N×N correlation matrix ρ, giving benchmark strategies genuine cross-asset structure to exploit:
import jax.numpy as jnp
from quantscenariobench.api import simulate_correlated_basket
from quantscenariobench.interface import TimeGrid
from quantscenariobench.models import BlackScholes
models = [BlackScholes(mu=0.05, sigma=0.2, S0=100.0), BlackScholes(mu=0.03, sigma=0.15, S0=50.0)]
rho = jnp.array([[1.0, 0.7], [0.7, 1.0]])
scenarios, basket_metadata = simulate_correlated_basket(models, TimeGrid(jnp.linspace(0.0, 1.0, 253)), n_paths=50_000, seed=1, rho=rho)
scenariosis exactlylist[Scenario]— the same schemacompose_returns/export_parquet/publish_to_hubalready accept, unchanged.basket_metadata(aBasketMetadata) additively records ρ, the basket seed, and constituent identifiers; pass it toexport_parquet(scenarios, path, basket_metadata=basket_metadata)to have it survive export/Hub-publish/reload.- ρ must be symmetric, unit-diagonal, and positive semi-definite — validated before any simulation runs. ρ = identity reproduces N independent draws bit-identically (a documented seed-derivation rule:
jax.random.split(PRNGKey(seed), N)gives each asset's sub-key). - Correlation is applied to each asset's price-driving noise; a model's own internal driver (e.g. Heston's price/variance correlation) is unaffected.
BlackScholes/Hestonconstituents are supported in v1;RoughBergomi(non-Markovian) raisesNotImplementedError.
Scenario Realism Diagnostics
quantscenariobench.diagnostics measures — rather than just asserts — how well a Scenario's simulated paths reproduce the stylized facts of real asset returns (Cont, 2001): heavy tails, volatility clustering, absence of linear autocorrelation, the leverage effect, and aggregational Gaussianity.
from quantscenariobench.diagnostics import realism_report
report = realism_report(scenario) # the full n_paths ensemble, vectorized — no per-path loop
print(report.excess_kurtosis) # DiagnosticStat(mean=..., std=..., in_band=..., reference_low=..., reference_high=...)
print(report.squared_return_acf_lag1) # volatility clustering
print(report.leverage_correlation) # negative for equity-like scenarios (e.g. Heston with rho < 0)
- Every field is a
DiagnosticStat(cross-path mean/std, a literature reference band, and an in-band flag) — computed against a fixed, documented range, never fetched or computed from live market data. realism_report()never rejects or filters a scenario: Black-Scholes correctly failing the volatility-clustering band is a reported finding, not an error.RealismReportis JSON-serializable (RealismReport.from_dict/dataclasses.asdict) and embeds additively into a Hub dataset card:generate_dataset_card(scenario, realism_report=report).
Reproducibility
Identical (model, time_grid, n_paths, seed) inputs produce bit-identical paths on the same computational backend (CPU / GPU / TPU). Cross-backend bit-identity is not guaranteed due to floating-point differences across JAX backends. The seed, prng_key_info, and library_version metadata fields document full provenance for every batch.
All simulations run in float64 (enabled automatically on import quantscenariobench).
Pre-built Benchmark Samples
Three lightweight demo samples are published on Hugging Face. They use a single fixed configuration (10,000 paths, daily steps over 1 year, seed 42) and are sized for quick loading, not research-scale use. They are useful for inspecting the output schema and doing quick cross-model comparisons without generating anything locally.
| Model | Sample | Paths | Steps |
|---|---|---|---|
| Black-Scholes | QuantScenarioBench/qsb-black-scholes | 10,000 | 253 (daily, 1 yr) |
| Heston | QuantScenarioBench/qsb-heston | 10,000 | 253 (daily, 1 yr) |
| Rough Bergomi | QuantScenarioBench/qsb-rough-bergomi | 10,000 | 253 (daily, 1 yr) |
All three use the same time grid (linspace(0, 1, 253)), seed (42), and initial spot (S0=100, mu=0) for direct cross-model comparison.
from datasets import load_dataset
ds = load_dataset("QuantScenarioBench/qsb-heston", split="train")
print(ds.column_names)
# ['observation', 'latent_state', 'seed', 'prng_key_info', 'model_name',
# 'model_version', 'parameters', 'time_grid', 'n_paths',
# 'library_version', 'dataset_version', 'generated_at']
For research use, generate your own dataset with your chosen horizon, time grid, parameters, and number of paths using simulate(), then export it with export_parquet() or publish_to_hub().
Benchmark Core
quantscenariobench.benchmark scores portfolio strategies against Scenarios using a shared pipeline, mirroring the Market Model layer's interface-plus-conformance-suite design.
from quantscenariobench.benchmark.returns import derive_returns, compose_returns
from quantscenariobench.benchmark.strategies import EqualWeight, GlobalMinimumVariance, CVaROptimization
from quantscenariobench.benchmark.runner import run_benchmark
# One Scenario per asset, all sharing the same TimeGrid (a single path per asset)
returns = compose_returns([scenario_a, scenario_b, scenario_c]) # shape (t, n_assets)
historical_returns, evaluation_returns = returns[:126], returns[126:]
strategy = EqualWeight()
result = run_benchmark(strategy, historical_returns, evaluation_returns)
print(result.metrics)
# {'sharpe_ratio': ..., 'sortino_ratio': ..., 'max_drawdown': ..., 'final_wealth_factor': ...}
- Portfolio Optimizer Interface (
quantscenariobench.benchmark.interface) —BaselineStrategy(allocate(historical_returns)) andForecastOptimizer(allocate(historical_returns, forecast)), bothequinox.ModuleABCs, plus a validatedPortfolioWeightstype (long-only, sums to 1). - Traditional baselines (
quantscenariobench.benchmark.strategies) —EqualWeight,GlobalMinimumVariance(long_only=...),CVaROptimization(confidence_level=...),HierarchicalRiskParity(linkage_method=...)(covariance-robust, no matrix inversion — López de Prado's tree-clustering/quasi-diagonalization/recursive-bisection algorithm). - Metrics (
quantscenariobench.benchmark.metrics) —sharpe_ratio,sortino_ratio,max_drawdown,final_wealth_factor, assembled inDEFAULT_METRICS; all purejax.numpyfunctions with defined sentinel behavior on degenerate input (e.g. zero variance). run_benchmark()(quantscenariobench.benchmark.runner) fits the strategy once (static buy-and-hold), applies its weights acrossevaluation_returns, and returns a JSON-serializableBenchmarkResult(strategy_name,strategy_parameters,metrics,asset_scenario_ids,time_grid_reference,library_version,generated_at).- A conformance test suite (
quantscenariobench.benchmark.testing) verifies a customBaselineStrategy/ForecastOptimizerimplementation against the interface, with zero changes torun_benchmark().
Metric Conventions
- Risk-free rate is 0 for every ratio metric (Sharpe, Sortino, annualized Sharpe) unless a metric's name says otherwise.
- No metric annualizes by default.
sharpe_ratio,sortino_ratio, and every otherDEFAULT_METRICSentry report their raw, un-annualized value. - Annualization is opt-in and uses one documented convention:
periods_per_year=252, applied only by metrics whose name says_annualized_<N>(e.g.annualized_sharpe(252)→sharpe_ratio_annualized_252) or that otherwise takeperiods_per_yearas an explicit parameter (e.g.calmar_ratio). These are never silently added toDEFAULT_METRICS— always opt-in. - Compounding for drawdown/Calmar/wealth-factor metrics (
max_drawdown,calmar_ratio,final_wealth_factor) is viacumprod(1 + returns), with wealth(0) implicit at 1.0.
Periodic Rebalancing & Transaction Costs
By default run_benchmark() fits a strategy once and holds its weights unchanged (buy-and-hold). Pass a RebalanceSchedule to refit periodically instead:
from quantscenariobench.benchmark.interface import ProportionalCost, RebalanceSchedule
from quantscenariobench.benchmark.metrics import turnover
result = run_benchmark(
strategy, historical_returns, evaluation_returns,
rebalance_schedule=RebalanceSchedule(k=21), # refit every 21 evaluation steps
cost_model=ProportionalCost(one_way_bps=10), # 10 bps one-way transaction cost, opt-in
metrics=(*DEFAULT_METRICS, turnover),
)
- Between rebalances, weights drift with relative asset performance (not reset every step) — the literature-default convention.
cost_model=None(the default) means no transaction costs and is bit-identical to a run with no cost model at all;ProportionalCost(0)is a distinct, explicit zero-cost configuration with the same numeric result.- A minimal
PolicyStrategy(allocate_sequence(observed_returns), called once per rebalance date) lets a time-varying policy participate in the same pipeline asBaselineStrategy/ForecastOptimizer. - A cost sensitivity sweep (mirroring the paper's bps grid) is a plain loop over the shipped API — no bespoke helper:
for bps in (0, 5, 10):
result = run_benchmark(
strategy, historical_returns, evaluation_returns,
rebalance_schedule=RebalanceSchedule(k=21), cost_model=ProportionalCost(bps),
)
- The active
rebalance_schedule/cost_modelare recorded onBenchmarkResult/EvaluationResult(additive fields) and join the Leaderboard aggregation key, so results at differentbpsnever collapse into the same row.
Distributional Evaluation
simulate() already generates tens of thousands of i.i.d. paths per Scenario; run_benchmark_distributional() reuses that ensemble — no re-simulation — to score a strategy over R independent path draws and report mean ± std, a confidence interval, and (via compare_strategies) a paired significance test against another strategy, matching arXiv:2510.03129's reporting standard:
from quantscenariobench.benchmark.runner import run_benchmark_distributional
from quantscenariobench.benchmark.evaluation import compare_strategies
result = run_benchmark_distributional(
strategy, [scenario_a, scenario_b, scenario_c], n_historical=30,
n_repeats=32, seed=0, # R=32 by default; reuses n_paths already simulated
)
print(result.metrics) # per-metric mean, unchanged scalar shape
print(result.metrics_distribution["sharpe_ratio"])
# {'mean': 0.040, 'std': 0.155, 'ci_low': -0.013, 'ci_high': 0.092, 'n_repeats': 32, 'values': [...]}
Reproducing arXiv:2510.03129 Table 1's mean±std reporting style from actual output on two strategies over the same R=32 draws:
| Strategy | Sharpe (mean ± std) | 95% CI |
|---|---|---|
EqualWeight |
0.040 ± 0.155 | [-0.013, 0.092] |
GlobalMinimumVariance |
0.046 ± 0.169 | [-0.011, 0.105] |
comparison = compare_strategies(result_a, result_b, "sharpe_ratio")
# {'mean_difference': ..., 'p_value_ttest': ..., 'p_value_wilcoxon': ...}
compare_strategies requires result_a/result_b to come from the same R path draws — pass identical scenarios/seed/n_repeats to both run_benchmark_distributional() calls being compared; alignment is paired by construction and not re-derived after the fact. n_repeats=1 collapses to an ordinary run_benchmark() result exactly (metrics_distribution stays None).
Implementing a custom strategy
import jax.numpy as jnp
from quantscenariobench.benchmark.interface import BaselineStrategy, PortfolioWeights
class MyStrategy(BaselineStrategy):
def allocate(self, historical_returns):
n = historical_returns.shape[1]
return PortfolioWeights(jnp.full((n,), 1.0 / n), n_assets=n)
Pass it directly to run_benchmark() — no other integration required.
Evaluation Results & Leaderboard
quantscenariobench.benchmark.evaluation turns a BenchmarkResult into a versioned, publishable record, and reads any collection of them back into a ranked comparison table. This section covers the publishing/aggregation pipeline itself — for the hosted, browsable page built on top of it, see Leaderboard Space.
from quantscenariobench.benchmark.evaluation import (
to_evaluation_result,
write_evaluation_result,
publish_evaluation_results,
load_evaluation_results,
aggregate_evaluation_results,
)
# 1. Convert a BenchmarkResult (from run_benchmark()) into an EvaluationResult
evaluation_result = to_evaluation_result(result)
# 2. Store it locally — one timestamped file per run, organized by dataset/strategy
write_evaluation_result(evaluation_result, root="results")
# 3. Publish it to a shared Hugging Face dataset repo (append-only)
publish_evaluation_results([evaluation_result], "my-org/qsb-evaluation-results", token="hf_...")
# 4. Aggregate every locally stored (or downloaded) result into a leaderboard table
results = load_evaluation_results("results")
leaderboard = aggregate_evaluation_results(results)
print(leaderboard)
# [{'strategy': 'EqualWeight', 'benchmark_dataset': '...', 'sharpe_ratio': ..., ...}, ...]
EvaluationResult— a fixed, JSON-native schema (schema_version,result_id,strategy,benchmark_dataset,metricsas an ordered{name, value}list,library_version,generated_at), derived fromBenchmarkResultvia the pureto_evaluation_result()function.BenchmarkResultitself is unchanged —EvaluationResultis a separate, additive publication-layer type.- Local storage (
write_evaluation_result) writes one timestamped JSON file per run underresults/<dataset>/<strategy>/; nothing is ever overwritten. - Hugging Face publishing (
publish_evaluation_results,generate_evaluation_results_card) uploads results to a shared dataset repo and regenerates a summary README/card after every publish. - Leaderboard aggregation (
aggregate_evaluation_results,load_evaluation_results,load_evaluation_results_from_hub) is a generic reader — no strategy- or dataset-specific branching — that returns a plainlist[dict]: one row per strategy × Benchmark Dataset, the most recently generated result winning ties. It has no UI framework dependency; render it withpandas.DataFrame(leaderboard)or however you like.
Leaderboard Space
A hosted, browsable Hugging Face Space — built with Gradio — that renders the Leaderboard above as a live, sortable, filterable page. It lives at spaces/leaderboard/, alongside (not inside) the quantscenariobench package, and consumes the pipeline described in Evaluation Results & Leaderboard as an ordinary dependency: it calls aggregate_evaluation_results()/load_evaluation_results_from_hub() and renders the result. It adds no aggregation, ranking, or data-model logic of its own — every value shown comes directly from that pipeline.
- Table rendering — the current ranked Leaderboard (strategy × Benchmark Dataset rows, one column per Metric), reloaded fresh every time the page is opened, so a newly published
EvaluationResultappears without redeploying the Space. - Sorting — click any column header to reorder rows ascending or descending; this is Gradio's built-in
Dataframebehavior, not custom code. - Filtering — narrow the table by Benchmark Dataset, Strategy, or Metric, independently or combined.
- Out of scope — advanced analytics, visualizations (charts/plots), historical/trend views, and strategy-to-strategy comparison tooling are deliberately not part of this Space; it is a ranked table, not an analytics dashboard.
Running it locally
cd spaces/leaderboard
pip install -r requirements.txt
python app.py
This starts a local Gradio server rendering the Leaderboard from whichever Evaluation Results repo is configured (see below).
Configuring the Evaluation Results repo
The Space reads from the Hugging Face dataset repo named by the QSB_EVAL_RESULTS_REPO environment variable:
export QSB_EVAL_RESULTS_REPO="your-org/your-evaluation-results-repo"
If unset, it falls back to a placeholder default (quantscenariobench/evaluation-results) — the actual namespace for a shared, public Evaluation Results repo has not been finalized yet, so there is no canonical repo ID or hosted Space URL to publish here. Point QSB_EVAL_RESULTS_REPO at your own published Evaluation Results repo (see Hugging Face publishing above) to run the Space against real data.
Architecture
quantscenariobench/
├── api/ simulate() — single public entry point
├── interface/ MarketModel ABC, Scenario, TimeGrid, Metadata
├── models/ BlackScholes, Heston, RoughBergomi
├── solver/ Euler-Maruyama SDE solver (diffrax / lineax)
├── export/ export_parquet(), generate_dataset_card(), publish_to_hub()
├── testing/ Conformance suite for custom model authors
└── benchmark/
├── interface/ BaselineStrategy/ForecastOptimizer ABCs, PortfolioWeights, BenchmarkResult
├── strategies/ EqualWeight, GlobalMinimumVariance, CVaROptimization
├── metrics/ sharpe_ratio, sortino_ratio, max_drawdown, final_wealth_factor
├── returns/ derive_returns(), compose_returns()
├── solver/ scipy-backed Optimizer Solver Layer (GMV long-only, CVaR)
├── runner/ run_benchmark() — single public entry point
├── evaluation/ EvaluationResult, to_evaluation_result(), local storage,
│ HF publishing, Leaderboard aggregation
└── testing/ Conformance suite for custom strategy authors
spaces/
└── leaderboard/ Hugging Face Space (Gradio) — see Leaderboard Space above.
Sibling to quantscenariobench/, not part of the installable
package; depends on it like any other consumer.
Dependency rule: models and export import only from interface. The solver and API layers compose these. This keeps any custom model implementation minimal. The benchmark subpackage mirrors this rule one layer up: strategies/metrics/returns import only from benchmark.interface (plus benchmark.solver where noted), and only benchmark.runner composes a caller-supplied strategy — no benchmark module reaches back into models or the scenario-generation solver.
Implementing a custom model
Subclass MarketModel (an equinox.Module) and implement three methods:
import equinox as eqx
import jax.numpy as jnp
from quantscenariobench.interface import MarketModel
class MyModel(MarketModel):
sigma: float
S0: float
def _drift(self, t, state):
return jnp.zeros_like(state) # zero drift
def _diffusion(self, t, state):
return self.sigma * state # scalar diffusion
def initial_state(self):
return jnp.array(self.S0, dtype=float)
Pass the model directly to simulate() — no other integration required.
Development
git clone https://github.com/tim-nish/QuantScenarioBench
cd QuantScenarioBench
pip install -e ".[dev]"
pytest
The test suite covers closed-form price validation (Gil-Pélaez inversion for Heston; Black-Scholes formula for GBM), statistical properties (skew monotonicity in H for rBergomi), Parquet round-trips, and dataset card conformance — plus the Benchmark Core (metrics and baselines validated against hand-derived reference values, Portfolio Optimizer conformance suite), the Evaluation Results pipeline (BenchmarkResult → EvaluationResult transform, local storage, and Leaderboard aggregation), and the Leaderboard Space (spaces/leaderboard/: data loading/rendering, sorting, filtering, and deployment configuration).
The same suite runs in CI on every push and pull request (see the badge at the top of this README). Releases follow docs/RELEASE_CHECKLIST.md.
Roadmap
| Capability | Status |
|---|---|
| Scenario generation (Black-Scholes, Heston, Rough Bergomi) | Shipped — v1.0 |
| Parquet export & Hugging Face dataset publishing | Shipped — v1.0 |
Benchmark Core (Portfolio Optimizer Interface, baselines, metrics, run_benchmark()) |
Shipped — v1.1 |
| EvaluationResult pipeline (transform, local storage, HF publishing) | Shipped — v1.1 |
| Leaderboard aggregation (ranked table from published results, no UI) | Shipped — v1.1 |
| Hugging Face Space — hosted Gradio Leaderboard UI, with sorting and filtering | Shipped — v1.2 |
Context-aware metrics (MetricContext) + tail-risk (VaR/CVaR), Calmar, and concentration metrics |
Shipped — v1.3 |
Periodic rebalancing (PolicyStrategy), turnover metric, proportional transaction costs |
Shipped — v1.3 |
| Distributional evaluation across scenario paths + strategy significance tests | Shipped — v1.3 |
Correlated multi-asset scenario generation (simulate_correlated_basket) |
Shipped — v1.3 |
| Hierarchical Risk Parity baseline strategy | Shipped — v1.3 |
Scenario realism diagnostics — stylized-facts validation (realism_report) |
Shipped — v1.3 |
| Advanced analytics, visualizations, historical/trend tracking, strategy-comparison tooling | Not planned |
v1.1 shipped the data; v1.2 ships the dashboard: aggregate_evaluation_results() still returns a plain list[dict] you can put in a pandas.DataFrame, a notebook, or your own app — but as of v1.2 you can also browse the same ranked table live on the hosted Leaderboard Space, with sorting and filtering built in. That Space was the explicit v1.2 goal referenced in earlier releases' roadmaps; it is deliberately scoped to the ranked table alone — see the Leaderboard Space section's "Out of scope" note for what's intentionally not included.
Citing
If you use QuantScenarioBench in your research, please cite it. Citation
metadata lives in CITATION.cff — use GitHub's
"Cite this repository" button for ready-made BibTeX/APA, or cite the
archived release via the Zenodo DOI badge at the top of this README
(the concept DOI 10.5281/zenodo.21097248 always resolves to the latest
release).
License
MIT — see LICENSE for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quantscenariobench-1.3.0.tar.gz.
File metadata
- Download URL: quantscenariobench-1.3.0.tar.gz
- Upload date:
- Size: 791.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4659d5690c8ff3b93f06344a394a493e476bd5e97c694bca74f0bc28102ef60
|
|
| MD5 |
14132fdd44d6653f92ee07d9135a7a8b
|
|
| BLAKE2b-256 |
72fddac7e2b8f2898524dcb11261910f3f807c70a5bac9235e1a80eab9d52d2b
|
File details
Details for the file quantscenariobench-1.3.0-py3-none-any.whl.
File metadata
- Download URL: quantscenariobench-1.3.0-py3-none-any.whl
- Upload date:
- Size: 99.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a45d26b0c2612acdff27be207c068b506309b0e7e675316b4f406c53bbfaade5
|
|
| MD5 |
01ae4b1d2fb3b5b2b2fa5336d95bc517
|
|
| BLAKE2b-256 |
7a0690f9a992d02bc357ffe1b185697805b4e55391d21811078a246a97924837
|