finShell

Model-free financial label and backtest validation engine.


Keywords
finance, backtesting, cross-validation, cpcv, triple-barrier, financial-machine-learning, data-snooping, bootstrap
License
MIT
Install
pip install finShell==0.3.3

Documentation

finShell

Python validation engine for financial labels and cross-validated selectors.

Installation

python -m pip install .

The standard installation includes plotting, CPCV, block bootstrap, null tests, triple-barrier labels, logistic selectors, and sealed out-of-sample validation. Input columns are mapped with ColumnRoleMap, so source schemas do not need to use finShell's internal names.

Notebook Walkthrough

This deterministic example creates an original triple-barrier classification label from seeded mock OHLC data. The mock market has persistent latent signal, irregular noise, and a small adverse drift so its unfiltered equity is not a stylized sine wave. It is an API example, not evidence of a tradable edge.

1. Generate and audit the label

The first operation after creating the label is a same-count random-path audit. The figure compares the favorable-label equity path with the null paths and their dashed pointwise p95 boundary, then reports class balance.

import numpy as np
import pandas as pd
import finshell as fs

rows = 480
rng = np.random.default_rng(2024)
signal = np.zeros(rows)
innovations = rng.normal(0.0, 0.75, rows)
for index in range(1, rows):
    signal[index] = 0.82 * signal[index - 1] + innovations[index]
signal = (signal - signal.mean()) / signal.std()
market_noise = rng.normal(0.0, 0.004, rows - 1)
next_returns = 0.0040 * np.tanh(signal[:-1]) + market_noise - 0.0010
close = np.empty(rows)
close[0] = 100.0
for index in range(rows - 1):
    close[index + 1] = close[index] * (1.0 + next_returns[index])
intrabar = np.abs(rng.normal(0.0012, 0.0005, rows))

frame = pd.DataFrame({
    "event_time": pd.date_range("2024-01-01", periods=rows, freq="1h", tz="UTC"),
    "signal_feature": signal,
    "high": close * (1.0 + intrabar),
    "low": close * (1.0 - intrabar),
    "close": close,
    "side": np.ones(rows, dtype=int),
})
study = fs.ValidationStudy(
    frame,
    roles=fs.ColumnRoleMap(
        timestamp="event_time", high="high", low="low", close="close", side="side"
    ),
    artifact_dir="assets/readme",
)
label = study.audit_label(
    fs.TripleBarrierConfig(profit_take=0.015, stop_loss=0.012, vertical_bars=8),
    null_tests=fs.NullTestConfig(
        random_simulations=300, stored_random_paths=40, random_seed=7
    ),
)
print(
    f"passed={label.passed} favorable={label.summary['favorable_count']} "
    f"real_total={label.summary['real_total']:.4f} "
    f"random_p95={label.summary['random_final_p95']:.4f} "
    f"p_value={label.summary['p_value']:.4f}"
)
passed=True favorable=73 real_total=1.0950 random_p95=-0.1088 p_value=0.0000

Label audit

2. Fit inside CPCV folds

LogisticSelector is fitted separately on every block-bootstrap training path. Validation and test rows are never included in those fits, and the final 20% quarantine remains sealed. The left panel shows the distribution across CPCV fold means. The right panel preserves the hierarchy: each CPCV permutation is a row containing its validation and test block-bootstrap distributions.

cv = study.fit_selector(
    fs.LogisticSelector(
        features=["signal_feature"], threshold=0.30, random_state=13
    ),
    cpcv=fs.CPCVConfig(
        n_groups=6, holdout_groups=2, validate_groups=1, max_splits=4
    ),
    bootstrap=fs.FoldBlockBootstrapConfig(
        replicates=4, block_bars=8, random_seed=13
    ),
)
print(
    f"passed={cv.passed} valid_bootstrap_fits={cv.summary['valid_bootstrap_fits']} "
    f"validate_return={cv.summary['mean_validate_total_return']:.4f} "
    f"test_return={cv.summary['mean_test_total_return']:.4f}"
)
passed=True valid_bootstrap_fits=16 validate_return=0.0206 test_return=0.0589

CPCV selector distributions

3. Audit selected quarantine trades

The already-fitted selector scores the sealed quarantine without refitting. Its equity path must beat both same-count random selection p95 and the unfiltered no-selector equity path.

oos = study.audit_oos(
    null_tests=fs.NullTestConfig(
        random_simulations=300, stored_random_paths=40, random_seed=17
    )
)
print(
    f"passed={oos.passed} selected={oos.summary['selected_count']} "
    f"selected_total={oos.summary['selected_total']:.4f} "
    f"random_p95={oos.summary['random_final_p95']:.4f} "
    f"no_selector={oos.summary['no_selector_total']:.4f} "
    f"p_value={oos.summary['p_value']:.4f}"
)
passed=True selected=36 selected_total=0.3088 random_p95=0.1589 no_selector=0.1981 p_value=0.0000

Out-of-sample selected-trade audit

4. Validate economic paths

The last stage block-bootstraps the selected OOS backtest outcomes into account- balance paths. Each path ends at the configured upper balance, lower balance, or trade-count horizon. The single chart reports the probability of every resolution state and the median number of trades to resolution.

economics = study.validate_economics(
    paths=300,
    block_bars=4,
    random_seed=23,
    initial_balance=100_000,
    upper_balance=105_000,
    lower_balance=95_000,
    max_trades=12,
)
print(
    f"passed={economics.passed} paths={economics.summary['bootstrap_paths']} "
    f"upper_hit={economics.summary['upper_hit_probability']:.1%} "
    f"lower_hit={economics.summary['lower_hit_probability']:.1%} "
    f"vertical={economics.summary['vertical_probability']:.1%} "
    f"median_resolution={economics.summary['median_resolution_trades']:.1f} trades"
)
passed=True paths=300 upper_hit=89.7% lower_hit=1.0% vertical=9.3% median_resolution=5.0 trades

Triple-barrier economic validation

Data Assumptions

  • Input may be a pandas DataFrame, CSV file, or Parquet file.
  • Timestamps must be parseable; ingestion normalizes them to UTC and sorts them.
  • ColumnRoleMap maps user-defined column names into the validation contract.
  • OHLC and side columns are required when finShell generates triple-barrier labels.
  • Outcomes are additive per-event returns used for cumulative equity and path tests.
  • Randomized and bootstrap results are deterministic for a fixed seed.
  • Quarantine data is the final chronological 20% by default and is not used in CPCV.
  • Economic barrier paths resample contiguous selected-trade outcomes from the OOS backtest and compound them from the configured initial account balance.

Run the test suite with:

.\.venv\Scripts\python.exe -m pytest