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.

Get started

Install the current release from PyPI, then verify the package, native extension, dependency metadata, bundled schemas, and platform support in one command:

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

The distribution is named lacuna-quant; Python code and the command line continue to use lacuna. Stable-ABI wheels support CPython 3.11 and later on Linux x86-64 and arm64, macOS arm64, and Windows x86-64.

Install only the optional method families you need:

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

Start with the guided signal audit, review the v0.13.0 release, or use the Alphalens migration guide when bringing an existing factor workflow.

If you installed a historical GitHub wheel whose distribution metadata was lacuna, remove it before migrating; the unrelated PyPI project with that name is not Lacuna:

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

Reinstall lacuna-options afterward only if your research uses the extension.

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, plus built-in PBO/CSCV partition reduction above its measured crossover, 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 on main for the v0.14 release target
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 Single-threaded Rust grouped-rank IC, bootstrap-mean, interval-purge, and admitted PBO reducers with checked bulk-array boundaries and callable references
Options extension Independently versioned lacuna-options 0.2.0, with a 0.2.1 release target widening core compatibility through v0.14 without API changes

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.

Research workflows

Lacuna accepts explicit research artifacts and returns structured evidence before rendering a report. The complete study below uses the statistics and report extras installed in the Get started section.

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:

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

lacuna bundle verify study.lacuna

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.13 milestones are implemented, compatibility-fixtured, and released. The current 0.13 line adds verified PyPI distribution while retaining the complete factor-research sequence and Lacuna's explicit no-backtester boundary. The v0.14 implementation is complete but release-gated: grouped IC and PBO remain provisional until pinned Linux, same-wheel ABI, target wheel, and exact-SHA non-publishing preflight evidence promote every migration decision to a terminal state.

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.13 behavior, release-gated v0.14 work, and 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

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
# 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.14.0.tar.gz (250.4 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.14.0-cp311-abi3-win_amd64.whl (417.0 kB view details)

Uploaded CPython 3.11+Windows x86-64

lacuna_quant-0.14.0-cp311-abi3-manylinux_2_28_x86_64.whl (506.5 kB view details)

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

lacuna_quant-0.14.0-cp311-abi3-manylinux_2_28_aarch64.whl (494.4 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.28+ ARM64

lacuna_quant-0.14.0-cp311-abi3-macosx_11_0_arm64.whl (478.6 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: lacuna_quant-0.14.0.tar.gz
  • Upload date:
  • Size: 250.4 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.14.0.tar.gz
Algorithm Hash digest
SHA256 0fea7cbb51ad20f725e1926bf19ff9886f961331126e4b5f1432af43b9b7303b
MD5 ef4bd94f0ee3cd887d3e5fbeddafec51
BLAKE2b-256 1fb97eb58932d4fd452d245d54e7fa8847ff9cc029331b4cfd1a74a61d4da327

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.14.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.14.0-cp311-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.14.0-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 321185cf2c40a31ff501e63eb544f9b9c19f0e211df1b2cbbe6bd92569c74eb6
MD5 60e2e4ade0660e2d6843beb07a95f0f6
BLAKE2b-256 da78ea5a3373b2cec0f78b8d5216b5f66bac6c25ae9604a3ed7ce34e3832a505

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.14.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.14.0-cp311-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.14.0-cp311-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 262d2c6ed19d91fbee1fbb06ea44aa5f9bf3aaa020d3643eb5448c9724b58135
MD5 f9bab3f97c05c64cf420285c713a10d6
BLAKE2b-256 1e766a0557f9f963a6d8abd2ab8c5ba74a64e4701d6b0c5312d6abd710a156ec

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.14.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.14.0-cp311-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.14.0-cp311-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 07a814d817bb29fa076706a18c0f3be543630aab613efeaf1ee4869fba6a524a
MD5 266d381a1c280234b8de4979eb3cec80
BLAKE2b-256 485fa1782e1370ab15fa87bb902eea36681c8bb77b7b3230af81f1cc951e3217

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.14.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.14.0-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for lacuna_quant-0.14.0-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a5541ba08d4e520770cdef359f7abc113e780f6592453600de21ec28c6cfa9ab
MD5 f16b3edc1d89f9a49a0f503c78e38000
BLAKE2b-256 9d2cddbfaa0b62349ed2d8e07658b8ff4028bb867f5f7a1cf3c8fa2a03f576ec

See more details on using hashes here.

Provenance

The following attestation bundles were made for lacuna_quant-0.14.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

This release

0.14.0 This release

5 files

0.13.0

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