Skip to main content

Probabilistic price forecasting via stochastic differential equations (GBM/OU/neural), with AIC model selection, Monte Carlo quantiles, and fan charts.

Project description

neural-sde

Probabilistic price forecasting via stochastic differential equations. Fit a GBM or OU (mean-reverting) model to a price series, generate Monte Carlo forecasts with quantified uncertainty, and answer questions like "what's the probability this is up 10% in 30 days?" — without pretending to know the future.

This library produces probabilistic forecasts, not point predictions. forecast() returns a distribution over future paths, not a single "the price will be X" answer. Treat every number it gives you — quantiles, prob_above(), the fan chart — as a statement about uncertainty, not a promise. Financial markets are not stationary; a model fit on last year's regime can be a poor guide to next month's.

Install

pip install -e .                 # core: numpy, scipy, matplotlib
pip install -e ".[torch,pandas]" # + neural SDE path and pandas Series input
pip install -e ".[demo]"         # + yfinance, to run demo_forecast.py on real data

Requires Python 3.9+. See requirements.txt / pyproject.toml for pinned dependency ranges.

Quickstart

from neural_sde.highlevel import fit, forecast

model = fit(prices)                        # auto: AIC picks GBM vs OU
fc = forecast(model, horizon=30)           # antithetic Monte Carlo
print(fc.prob_above(prices[-1] * 1.10))    # P(+10% by day 30)
print(fc.quantiles([0.05, 0.5, 0.95]))     # 5th/50th/95th percentile price
fc.plot("forecast.png")                    # fan chart: history -> forecast cone

prices is any 1-D array-like or pandas Series of price levels (not returns). fit(model="neural") routes to a torch-based NeuralSDETrainer when torch is installed — see Neural path below before using it for anything besides diffusion/volatility. The GBM/OU parametric paths use exact (unbiased) transition densities and are the production-quality default.

On regimes

fit() estimates parameters from whatever window of history you give it. A GBM fit on a 6-month bull run will forecast that drift forward; an OU fit on a period of active mean-reversion will forecast reversion. Neither model knows the regime changed the day after your training window ends. Re-fit periodically, and sanity-check model.summary() (drift/vol/theta) against what you'd expect for the asset before trusting the forecast.

Neural path

fit(model="neural") learns state-dependent drift f(x,t) and diffusion g(x,t) functions instead of GBM/OU's parametric forms.

Diffusion recovery is solid — verified within 0.4-14% of the true value on synthetic GBM/OU calibration tests, and it's a genuine capability the parametric paths don't have: state-dependent volatility, not a single constant sigma. Useful for anything that wants to react to changing vol/regime (option pricing, vol-aware risk sizing).

Drift recovery is not usable at daily sampling frequency, and this isn't a bug that more engineering fixes. Recovering a state-dependent drift function from a single daily-sampled price path hits a hard signal-to-noise limit: SNR ≈ mu * sqrt(dt) / sigma, a few percent for typical parameters. A 5-seed-averaged sweep from 2,000 to 20,000 daily observations (up to 79 simulated years) improved median drift error only from ~267% to ~135% — consistent with the expected slow 1/sqrt(n) convergence, and nowhere near a usable tolerance. Extrapolating that rate, closing the gap would need on the order of 1,000+ years of daily data. The parametric GBM/OU drift estimate doesn't have this problem: it fits one global scalar averaged over the entire series rather than attempting state resolution, which sidesteps the issue instead of solving it.

Practical guidance: use the neural path for diffusion/volatility modeling. For anything that needs a drift estimate — hedging deltas, regime signals, mean-reversion speed — use the parametric GBM/OU drift, not the neural path's drift(x,t).

Beyond forecasting

Three thin layers on top of fit/forecast, each designed around what's actually reliable (see Neural path above) rather than requiring drift accuracy the library can't currently deliver.

Signals

from neural_sde.highlevel import fit, forecast, signal

fc = forecast(fit(prices), horizon=30)
sig = signal(fc, prob_threshold=0.6)   # P(favorable direction) >= 60% to act
print(sig.summary())                    # direction, size, stop/target levels
sig.exit_triggered(current_price)       # True once price crosses stop or target

A thin wrapper, not a new estimation step — augments a strategy you already have rather than replacing it. Direction and confidence come straight from prob_above/prob_below; size is proportional to the forecast's implied Sharpe ratio; stop/target come from the forecast's own quantile bounds.

Options pricing

from neural_sde.pricing import price_european_call

result = price_european_call(fit(prices), K=110, T=30/252, r=0.04)
print(result.summary())   # price, std error, delta, gamma

Risk-neutral Monte Carlo — by the fundamental theorem of asset pricing, an option's value only depends on the underlying's diffusion under the risk-neutral measure; the drift is always the risk-free rate, never the asset's real-world drift estimate. That's what makes this usable with model="neural" for genuinely state-dependent volatility even though the neural path's real-world drift isn't reliable — pricing never touches that estimate in the first place. With model="gbm", this reduces to exactly Black-Scholes (verified in test_pricing.py against the closed-form formula); OU and neural extend beyond it.

Portfolio risk

from neural_sde.risk import portfolio_var

models = {"AAPL": fit(aapl_prices), "MSFT": fit(msft_prices)}
port = portfolio_var(models, shares={"AAPL": 100, "MSFT": -50},
                     price_histories={"AAPL": aapl_prices, "MSFT": msft_prices})
print(port.summary())   # VaR/CVaR at 95%/99%, per-position contribution

Correlated Monte Carlo VaR/CVaR across positions — samples each asset's terminal price jointly (via the historical correlation between their returns), not independently, so diversification and hedges actually show up in the portfolio-level risk number. Deliberately restricted to model="gbm" or "ou" — VaR is a real-world-measure risk metric that depends on each asset's actual drift, which is exactly the estimate the neural path can't produce reliably (see Neural path); rejects model="neural" with a clear error rather than silently using an unreliable one.

What's inside

Module Purpose
highlevel.py Public API: fit/forecast/FittedModel/Forecast/Signal/signal — start here
pricing.py Risk-neutral option pricing — see Beyond forecasting
risk.py Correlated portfolio VaR/CVaR — see Beyond forecasting
sde_core.py Core SDE theory: drift/diffusion, Ito<->Stratonovich conversion
solvers.py / torch_solvers.py Euler-Maruyama, Milstein, Heun solvers (numpy and torch backends)
multi_asset.py Correlated multi-asset GBM/OU/CIR diffusion
neural_networks.py / trainer.py Neural drift/diffusion networks and training loop — see Neural path, requires torch
likelihood.py Score-matching / contrastive-divergence loss functions
forecasting.py NeuralSDEForecaster (used internally by fit(model="neural")); its OptionPricer predates the normalization fixes in trainer.py and isn't wired up to them — use pricing.py instead
api.py Lower-level unified solver entry point

Demo

pip install -e ".[demo]"
python demo_forecast.py

With yfinance installed and network access, edit the top of the script to pull real data:

import yfinance as yf
from neural_sde.highlevel import fit, forecast

prices = yf.download("AAPL", period="2y")["Close"]
model = fit(prices)
fc = forecast(model, horizon=30)
print(fc.summary())
fc.plot("forecast.png")

Offline, the script instead demos against synthetic data with known dynamics, which doubles as a calibration test — it simulates GBM and OU paths with known parameters, fits them back, and checks:

  1. fit() recovers the true GBM parameters and AIC selects GBM.
  2. fit() recovers the true OU parameters and AIC selects OU.
  3. Monte Carlo quantiles agree with the exact lognormal distribution to <1%.
  4. The exact and euler (core-library) engines agree on the median to <3%.

forecast.png and forecast_example.png in this directory are pre-generated sample outputs from that script — regenerate them yourself with python demo_forecast.py.

Testing

pip install -e ".[dev]"
pytest test_convergence_rate.py test_adjoint_grad.py test_adjoint_verify.py \
       test_trainer_losses.py test_torch_solvers.py test_unified_entry.py \
       test_api_integration_v05.py test_edge_case_regression.py

test_signal.py, test_pricing.py, and test_risk.py (self-verifying checks for the Beyond forecasting modules) are script-style rather than pytest-discoverable — run each directly: python test_pricing.py, etc.

archive/ holds superseded tests kept for historical reference (see archive/README.md); they aren't part of the suite.

License

MIT.

Credit

Built by Kevin Downie in collaboration with Claude (Anthropic).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

neural_sde-0.7.0.tar.gz (277.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

neural_sde-0.7.0-py3-none-any.whl (290.0 kB view details)

Uploaded Python 3

File details

Details for the file neural_sde-0.7.0.tar.gz.

File metadata

  • Download URL: neural_sde-0.7.0.tar.gz
  • Upload date:
  • Size: 277.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for neural_sde-0.7.0.tar.gz
Algorithm Hash digest
SHA256 fcaea0746d1d1abf1ca59964fcf341207f0139a9a78e739490318edfd4e8e574
MD5 5d1d21cf21c51b355aab645a29777893
BLAKE2b-256 c2a2464e3f23e200ded3f2afd61c4ecddcfc6cfc0bc0b66f2afba94ed2584caf

See more details on using hashes here.

File details

Details for the file neural_sde-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: neural_sde-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 290.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for neural_sde-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 51c573807a631130d419d0ae9ef48afb0b87a0ce51487ffe67d9b34dfbc5e562
MD5 fd4730284560b8ec05a94da4e87eb47a
BLAKE2b-256 6d9f2eaf1d29d9d3a4359ae48c86348b87a38dd7c4974432c0753fc10e0720ad

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page