Skip to main content
Lacuna

Open-source quantitative research validation for finding where alpha breaks.

Stress-test your alpha before the market does.

CI   Latest release   PyPI   Python 3.11+   Rust 2024   Arrow compatible   MIT License


Lacuna is the validation and diagnostics layer between a quantitative research idea and confidence in its backtest. Bring a signal, a return stream, or an experiment history; Lacuna's job is to uncover weak evidence, leakage, instability, and unrealistic assumptions before capital is at risk.

[!IMPORTANT] v0.13.0 is the current release. It completes the planned factor-research sequence: explicit signal transformations and multi-lag stability in v0.10, validated decay inference, diagnostic portfolio projections, and event studies in v0.11, and semantics-first factor-panel ingestion in v0.12. Version 0.13 moves distribution to PyPI without changing the lacuna import API or analytical methods. Lacuna remains alpha software and its pre-1.0 APIs may evolve through documented migrations.

[!NOTE] Install the core project from PyPI as lacuna-quant; Python code continues to use import lacuna. The PyPI project named lacuna is unrelated and must not be installed beside lacuna-quant because both own the same import-package path.

Why Lacuna?

Backtest engines are good at answering what happened under these assumptions? Lacuna is being built to ask the harder follow-up questions:

  • Is the signal informative before portfolio construction amplifies it?
  • Did overlapping labels or unavailable data leak across the validation boundary?
  • Does the result survive nearby parameters, different periods, and different regimes?
  • Is the evidence still credible after research trials and transaction costs are counted?
  • Can every conclusion be reproduced from structured, machine-readable evidence?

Lacuna is not a broker, market-data vendor, strategy generator, or full event-driven backtester. It is designed to sit on top of Polars, pandas, NumPy, Arrow, and existing research or backtesting systems.

Architecture

Your research stack
pandas · Polars · NumPy · Arrow · any backtester
                         │
                         ▼
              Typed Python API
                         │
              Arrow-compatible boundary
                         │
             ┌───────────┼───────────┐
             ▼           ▼           ▼
        Rust kernels  Polars ops  NumPy/SciPy
             └───────────┼───────────┘
                         ▼
        Structured results and findings
                         │
                    JSON · Markdown · HTML

Python owns public contracts, temporal and statistical policy, provenance, and result construction. Most current computation runs through Polars, NumPy, or optional SciPy. The compiled Rust extension is deliberately narrow: it currently accelerates grouped rank IC, bootstrap-mean reduction, and half-open interval purging, each with a tested Python reference. Arrow-compatible columnar data is the interoperability contract; additional Rust kernels require end-to-end profiling and differential evidence.

What works now

Area Implemented in v0.13.0
Labels and timing Explicit observation, availability, entry, and label-end times; half-open intervals; trading-observation horizons; censoring, adjustment, and delisting evidence
Factor diagnostics Group-aware Pearson/Spearman IC; balanced, tie-preserving, split-aware, threshold, equal-width, and fixed-edge buckets; bucket returns and attrition
Signal transformations Availability-checked weighted least-squares neutralization with coefficient, rank, condition, residual-DF, and fit evidence
Stability and decay Exact multi-lag rank, autocorrelation, and membership turnover; descriptive decay plus validated exponential half-life inference
Diagnostic portfolios Explicit long/short bucket projections with gross/net reconciliation, group neutrality, concentration, contribution, and target-turnover evidence—without compounding or execution simulation
Event studies Availability-anchored windows, overlap and censoring evidence, clustered stationary-bootstrap intervals, and simultaneous response bands
Temporal and statistical validation Walk-forward, purged K-fold and CPCV paths; IID/dependent/joint bootstrap; permutation; Sharpe/PSR/DSR; CSCV/PBO; Reality Check and SPA
Robustness and trial history Parameter surfaces, seeded perturbations, subperiods, point-in-time regimes, universe scenarios, append-only experiment lineage, and multiple-testing correction
Trading realism Composable commission, spread, slippage, impact, and borrow assumptions; stress surfaces, break-even analysis, liquidity evidence, and capacity curves
Point-in-time correctness Availability-safe as-of joins, revision and future-data checks, survivorship states, half-open membership, universe drift, and dataset contracts
Evidence and reporting Immutable AnalysisResult evidence; signal/strategy/options audit profiles; deterministic JSON and Markdown; core HTML and optional evidence-native Plotly HTML
Reproducibility Deterministic .lacuna archives, published schemas, privacy redaction, SHA-256 integrity, and bounded non-executing verification
Interoperability Eager/lazy Polars, NumPy, Arrow, optional pandas and named MultiIndexes, generic factor/vendor/backtest schemas, DuckDB Arrow streams, and scikit-learn CV
Native acceleration Rust grouped-rank IC, bootstrap-mean reduction, and interval purging with reference, differential, integration, and Criterion coverage
Options extension Independently versioned lacuna-options 0.2.0 with chain validation, carry forwards, log-forward moneyness, delta buckets, and empirical IV residuals

Lacuna intentionally does not generate alpha, source market data, compound a portfolio, resolve overlapping holdings, simulate orders or fills, route trades, or run live strategies. It also does not infer calendars, execution timing, data availability, or backtester semantics from convenient defaults.

Install and quick start

Install Lacuna from PyPI. The distribution is named lacuna-quant; the import package and command remain lacuna:

python -m pip install lacuna-quant
lacuna doctor --strict

The cp311-abi3 wheels support CPython 3.11 and later on the published Linux x86-64/Linux arm64, macOS arm64, and Windows x86-64 targets. Optional dependencies are grouped as statistics, report, pandas, duckdb, and ml; see pyproject.toml for their exact bounds. The decay-fit and interactive-report calls below require SciPy, Jinja2, and Plotly from the statistics and report groups.

python -m pip install "lacuna-quant[statistics,report,pandas]"
python -m pip install lacuna-options

If you previously installed a GitHub Release wheel whose distribution metadata was lacuna, remove it before installing from PyPI. Installing both distributions leaves ownership of import lacuna ambiguous, and lacuna doctor reports that state as a failure.

python -m pip uninstall lacuna lacuna-options
python -m pip install --upgrade lacuna-quant lacuna-options

The same seven checksummed, provenance-attested distributions remain attached to the v0.13.0 GitHub Release.

For development, Lacuna uses uv for Python environments and Cargo for the Rust workspace:

git clone https://github.com/eyenoticeall/Lacuna.git
cd Lacuna

uv sync --group dev --group docs --extra pandas --extra statistics --extra report
uv run lacuna doctor
uv run pytest
cargo test --workspace

Inspect the runtime in machine-readable form:

uv run lacuna doctor --json --strict

The versioned diagnostic report checks package/native identity, the supported Python and wheel matrix, dependency metadata, packaged schemas, and runtime configuration. Stable check codes and nonzero strict-mode exits make it suitable for CI without reading research data or activating plugins.

Turn a cross-sectional signal and price panel into structured evidence:

import lacuna as lc

study = lc.SignalStudy(
    signal=signal,
    prices=prices,
    horizons=("1D", "5D", "10D", "20D"),
    signal_observed_at="open",
    entry="current_close",
    price_adjustment="total_return_adjusted",
    quantiles=5,
)

labels = study.labels()
split = lc.cv.PurgedKFold(n_splits=5, embargo=1).split(labels.frame)
bucketed = study.bucketize(spec=lc.BucketSpec.quantiles(count=5))
bucket_returns = study.bucket_returns(bucketed)
multi_lag = study.turnover(lags=(1, 5, 20))
decay_fit = study.fit_decay(resamples=2_000, seed=42)
projection = study.portfolio_projection(
    bucketed,
    horizon="5D",
    long_buckets=(5,),
    short_buckets=(1,),
    gross_exposure=1.0,
    net_exposure=0.0,
)

report = study.audit(
    bootstrap_resamples=10_000,
    seed=42,
    split=split,
    additional_evidence={
        "bucket_returns_explicit": bucket_returns,
        "multi_lag_stability": multi_lag,
        "decay_fit": decay_fit,
        "portfolio_projection": projection.evidence,
    },
)
report.show()
report.to_json("lacuna-audit.json")
report.to_html("lacuna-audit.html", renderer="plotly", view="signal")
report.bundle(
    "study.lacuna",
    provenance={"code_fingerprint": "git:abc123"},
)

verification = lc.verify_bundle("study.lacuna")
print(verification.archive_sha256)

Missing price-adjustment, delisting, survivorship, trial-history, or validation evidence stays visible as UNKNOWN; it is never silently promoted to a pass.

Compose evidence from every released phase with a scope-specific standardized profile:

evidence = {
    "purged_split": split.evidence,
    "trials": registry.snapshot(),
    "costs": cost_stress,
    "future_data": future_data_check,
    "vendor": adapted_vendor.evidence,
    "backtest": adapted_returns.evidence,
}

standard = lc.standard_audit(results=evidence, scope="strategy")
print(standard.table("category_coverage"))
standard.bundle("strategy-audit.lacuna", evidence=evidence)

The profile distinguishes required, optional, and not-applicable evidence, carries domain findings forward without changing their meaning, and deliberately emits no universal strategy-quality score.

The same methods are composable through the functional API:

labels = lc.labels.forward_returns(
    prices,
    horizons=("1D", "5D", "20D"),
    price_adjustment="total_return_adjusted",
)
ic = lc.signal.ic(signal, labels, method="spearman")
quantiles = lc.signal.quantiles(signal, labels, quantiles=5)
uncertainty = lc.validation.bootstrap(
    [row["ic"] for row in ic.table("ic_by_period") if row["ic"] is not None],
    method="stationary",
    expected_block_length=5,
    resamples=10_000,
    seed=42,
)

Keep model-fitting CPCV separate from selection analysis, then test a declared strategy family against one common benchmark:

paths = lc.cv.CombinatorialPurgedKFold(
    n_groups=6,
    n_test_groups=2,
    embargo=2,
).split(labels.frame)

pbo = lc.validation.probability_of_backtest_overfitting(
    synchronous_strategy_returns,
    partitions=8,
    partition_sensitivity=(4, 6, 10),
)

spa = lc.validation.superior_predictive_ability(
    performance_differentials,
    expected_block_length=20,
    resamples=10_000,
    seed=42,
)

Record every tried variant before selecting a winner, then adjust the complete family:

registry = lc.ExperimentRegistry("momentum-search", path="experiments.sqlite3")
for lookback, p_value in [(20, 0.03), (40, 0.01), (60, 0.20)]:
    registry.record(
        parameters={"lookback": lookback},
        metric=p_value,
        metric_name="p_value",
        method="strategy.evaluate",
        data_fingerprint="dataset:2026-08-26",
        code_fingerprint="git:abc123",
    )

adjusted = lc.validation.multiple_testing(registry, method="holm")

Stress normalized trades across explicit friction assumptions without hiding missing evidence:

surface = lc.costs.stress(
    trades,
    spread_bps=(0, 2, 5, 10, 20),
    slippage_bps=(0, 2, 5, 10),
    capital=10_000_000,
    annualization=252,
)

curve = lc.costs.capacity_curve(
    trades,
    capital=(1_000_000, 5_000_000, 10_000_000),
    base_capital=1_000_000,
    scenarios=(lc.costs.CapacityScenario("base", impact_coefficient=0.10),),
    classification_mode="point_in_time",
    available_time="market_available_time",
)

Keep future information and current-only universes out of historical decisions:

joined = lc.bias.asof_join(
    decisions,
    fundamentals,
    left_time="decision_time",
    right_time="available_time",
    by="instrument",
    revision="revision_id",
    revision_mode="point_in_time",
)

members = lc.bias.membership_at(
    index_membership,
    as_of=rebalance_time,
    identity=("index", "instrument"),
    source_status="confirmed_safe",
)

Anchor event studies to when information became available—not merely when the underlying event occurred:

windows = lc.events.event_windows(
    events,
    prices,
    anchor="available_time",
    before=5,
    after=10,
    overlap_policy="raise",
    price_adjustment="total_return_adjusted",
)
response = lc.events.event_response(
    windows,
    resamples=2_000,
    seed=42,
)

Cross external boundaries without hiding their semantics:

factor_schema = lc.adapters.FactorPanelSchema(
    schema_id="research.factor.v1",
    columns={
        "observation_time": "date",
        "instrument": "asset_id",
        "signal": "factor",
        "forward_return": "forward_5d",
    },
    semantics=lc.adapters.FactorPanelSemantics(
        signal_observation="market close",
        decision_time_rule="next session open",
        forward_return_entry="next session open",
        forward_return_exit="fifth session close",
        horizon_clock="trading observations",
        timezone="UTC",
        calendar="XNYS",
        adjustment_policy="total return adjusted",
        group_availability="unknown",
        imported_bucket_definition="not imported",
    ),
)
factor_panel = lc.adapters.adapt_factor_panel(raw_factor_data, factor_schema)

duckdb_frame = lc.adapters.from_duckdb(executed_relation)

cv = lc.adapters.as_sklearn_cv(
    lc.cv.PurgedKFold(n_splits=5, embargo=1),
    label_intervals,
)

candidates = lc.plugins.discover_plugins(group="audit_rules")  # metadata only
plugin = lc.plugins.activate_plugin(candidates[0], required_capability="audit.signal")

The options package is independently versioned and imported separately:

import lacuna_options as lo

chain = lo.validate_chain(option_quotes, year_basis=365.25)
bucketed = lo.delta_buckets(chain)
residuals = lo.empirical_residual(chain, expected="fair_iv")

For local Parquet, CSV, Arrow IPC, or Feather files:

uv run lacuna signal \
  --signal factor.parquet \
  --prices prices.parquet \
  --horizon 1D --horizon 5D --horizon 20D \
  --signal-observed-at open \
  --entry current_close \
  --price-adjustment total_return_adjusted \
  --bootstrap-resamples 10000 \
  --seed 42 \
  --out lacuna-audit.html \
  --html-renderer plotly \
  --bundle study.lacuna

uv run lacuna bundle verify study.lacuna

uv run lacuna audit \
  --scope strategy \
  --evidence split=purged-split.json \
  --evidence costs=cost-stress.json \
  --evidence bias=future-data.json \
  --out strategy-audit.html \
  --bundle strategy-audit.lacuna

Repository map

.
├── python/lacuna/          # public Python package
├── rust/
│   ├── lacuna-core/        # language-independent kernels
│   └── lacuna-python/      # PyO3 extension
├── extensions/
│   └── lacuna-options/     # independently versioned optional distribution
├── tests/                  # unit, property, reference, schema, golden, and integration tests
├── benches/                # Python benchmark entry points
├── schemas/                # published result compatibility schemas
├── docs/                   # engineering handbook and subsystem contracts
├── examples/               # executable examples
├── logos/                  # Lacuna brand assets
├── AGENTS.md               # repository contract for coding agents
├── pyproject.toml          # Python package and tooling
└── Cargo.toml              # Rust workspace

Roadmap and maturity

The planned 0.10.12 milestones are implemented, compatibility-fixtured, and released. The current 0.12 line includes the full factor-research sequence while retaining Lacuna's explicit no-backtester boundary.

Every repository-controlled item in the v1 readiness ledger has implementation evidence. 1.0.0 remains blocked by one intentionally external requirement: real users must apply Lacuna to independent research stacks. Until then, work should emphasize adoption feedback, compatibility, correctness, and measured performance rather than accumulating speculative features.

See the implementation roadmap for the phase-to-version progression and the v1 readiness ledger for the remaining gate. The full technical specification defines the architecture and statistical scope.

Engineering handbook

The technical specification is backed by implementation-oriented documentation:

The documentation distinguishes released v0.1–v0.12 behavior from later contracts. Contributors and coding agents should begin with AGENTS.md, then read the relevant methodology and subsystem pages before changing a method.

Principles

  • Time is part of the type system. Event, availability, effective, revision, and label times are not interchangeable.
  • Unknown is not pass. Missing evidence stays visible.
  • Results before reports. Every visualization is backed by structured, inspectable data.
  • Robustness means neighborhoods, not points. Isolated optima are warnings, not trophies.
  • Performance claims require benchmarks. Native code is earned through measurement.
  • Interoperate instead of replacing. Lacuna complements existing research stacks.

Development

# Format and lint
uv run ruff format --check .
uv run ruff check .
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings

# Type-check and test
uv run mypy
uv run pytest
cargo test --workspace

# Build a local wheel
uv run maturin build --release

# Run reproducible benchmarks
uv run lacuna bench --tier smoke
cargo bench --bench kernels

Pull requests pass through formatting, lint, strict typing, Python 3.11–3.14 on Linux, Python 3.13 on macOS and Windows, Rust, optional dataframe/reference integrations, strict documentation, and a minimum-Rust check, plus a clean source-distribution-to-wheel smoke test. See the CI architecture for the job graph and branch-protection contract.

Version-matching tags additionally build target-smoke-tested stable-ABI wheels for Linux x86_64, Linux aarch64, macOS arm64, and Windows x86_64, then publish checksums and GitHub provenance. See the release engineering contract.

Contribution guidance lives in CONTRIBUTING.md. The complete local documentation site can be built with uv run mkdocs serve. Security concerns should follow SECURITY.md.

License

The current repository and future distributions are released under the MIT License. Artifacts published before the MIT-only change retain the grants under which they were released; see the changelog.


Lacuna mark

Bring a signal or backtest. Lacuna will try to find the gaps in the evidence.

Download files

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

Source Distribution

lacuna_quant-0.13.0.tar.gz (215.2 kB view details)

Uploaded Source

Built Distributions

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

lacuna_quant-0.13.0-cp311-abi3-win_amd64.whl (333.0 kB view details)

Uploaded CPython 3.11+Windows x86-64

lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_x86_64.whl (422.2 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.28+ x86-64

lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_aarch64.whl (415.3 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.28+ ARM64

lacuna_quant-0.13.0-cp311-abi3-macosx_11_0_arm64.whl (402.1 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

File details

Details for the file lacuna_quant-0.13.0.tar.gz.

File metadata

  • Download URL: lacuna_quant-0.13.0.tar.gz
  • Upload date:
  • Size: 215.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for lacuna_quant-0.13.0.tar.gz
Algorithm Hash digest
SHA256 9e29c254b6744161bb7d0b1cfe8fc79d4178a7b9fabd05429679485d24ec949d
MD5 f1ca0ce8f04f0f2785552bcc954cd00f
BLAKE2b-256 d248370c346ff9d0d167e96f48396114a811fa6a177ba0cf4fdb46c3f525c43d

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.13.0.tar.gz:

Publisher: release.yml on eyenoticeall/Lacuna

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lacuna_quant-0.13.0-cp311-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.13.0-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 4f8f327466ea025e6712ba5406d769ebab06e79a89712f341e51aa456421204b
MD5 014e768338c8aa3e4ac578720059c811
BLAKE2b-256 2c6f7995298166201b2e8fd7bbde1b1fbd733d9101c81cb91ce8d2ba34f928f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.13.0-cp311-abi3-win_amd64.whl:

Publisher: release.yml on eyenoticeall/Lacuna

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 e8ede9e98f1731b154721a5cad68e47b24ed672e6318a7289cc5b6e8db69bc2f
MD5 fee8db3c7c84945785c09c7afa3477e6
BLAKE2b-256 2a784ee84471f91cd43dc17d1aa12bb2c28d6cd7518cac63e6ab8426775ea299

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_x86_64.whl:

Publisher: release.yml on eyenoticeall/Lacuna

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 9d7dc7c263f3c068c33185569f846ec48d912712a4dde0f2a4f1698c6748df72
MD5 c0e3bddc9d3a19eac2859844172cc4d7
BLAKE2b-256 08c269edc5dcd317e8a24e1459e66b2ea80f37a91180199587add034d5708f67

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.13.0-cp311-abi3-manylinux_2_28_aarch64.whl:

Publisher: release.yml on eyenoticeall/Lacuna

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lacuna_quant-0.13.0-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.13.0-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 af408ad4575cadb00bcb419ec959a643045a73db291a234dd4d1284d97fd48bb
MD5 99ac4c718e9971a665c08fcb27e942cb
BLAKE2b-256 25e99243dd4511e3d82a592dd7dc93715b49c5347edd21b74e718b9be0f6386a

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.13.0-cp311-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on eyenoticeall/Lacuna

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.14.0

5 files

This release

0.13.0 This release

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page