Python validation engine for financial labels and cross-validated selectors.
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.
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.
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
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
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
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
- 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


