Model-free financial label and backtest validation engine.
Project description
finShell
Python validation engine for financial labels and cross-validated selectors.
finShell is a validation framework, not a signal generator or trading system. Passing finShell diagnostics does not imply live profitability. It is designed to reduce false confidence from overfit labels, selectors, and backtests.
Why finShell exists
Financial ML experiments are easy to overfit. A label can look predictive because the researcher repeatedly changed horizons, barriers, features, filters, or thresholds after seeing historical results.
finShell gives researchers a structured way to audit:
- whether a label beats random same-count paths,
- whether a selector survives purged CPCV and block bootstrap,
- whether selected quarantine trades beat random selection,
- whether selected outcomes survive economic path simulation.
Installation
python -m pip install finshell
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.
For local development:
git clone https://github.com/4SIGHTalgo/finShell
cd finShell
python -m pip install -e ".[dev]"
python -m pytest
Quick start with real data
import finshell as fs
study = fs.ValidationStudy(
"my_ohlc_events.parquet",
roles=fs.ColumnRoleMap(
timestamp="timestamp",
high="high",
low="low",
close="close",
side="side",
),
)
label = study.audit_label(
fs.TripleBarrierConfig(profit_take=0.015, stop_loss=0.010, vertical_bars=16),
)
cv = study.fit_selector(
fs.LogisticSelector(features=["volatility", "trend", "vix_change"], threshold=0.60),
)
oos = study.audit_oos()
economics = study.validate_economics()
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
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
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
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
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.
ColumnRoleMapmaps 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
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 finshell-0.3.3.tar.gz.
File metadata
- Download URL: finshell-0.3.3.tar.gz
- Upload date:
- Size: 42.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ba74f9108ec2cb4f4cb1d6affba1e028d77f37c56552bdaa862df0d7b472717
|
|
| MD5 |
55ea75aa970f3bcf209e3b868e3d075b
|
|
| BLAKE2b-256 |
7d8611f48dc1931e5e15c6821a54af0ca48556564e4c12544429fca5caf7b2ff
|
File details
Details for the file finshell-0.3.3-py3-none-any.whl.
File metadata
- Download URL: finshell-0.3.3-py3-none-any.whl
- Upload date:
- Size: 37.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
631a9916dab68c4cdd8242cea95c48353468d8bccb5557493acba62ac756cf2a
|
|
| MD5 |
34a5325d357d13c51b6727c30fc74119
|
|
| BLAKE2b-256 |
4bb0e6f67644cdc2add0ba745008127a0b26b55aa1ff67a924ea7a0e7c7f8601
|