Strategy Hypothesis Template
Follow a rigorous structured template for systematic trading strategy hypothesis formulation, development, and testing including explicit hypothesis statement, required data specification, test design and statistical methodology, objective performance benchmarks, and multi-stage validation gates before any live deployment consideration.
Strategy Hypothesis Template — Research & Experimentation
Category: Research & Experimentation | Subcategory: Hypothesis
What This Notebook Does
Every new trading strategy should begin with a clearly formulated hypothesis — a falsifiable claim about market behaviour. Without this structure, backtesting becomes p-hacking: endlessly adjusting parameters until something looks profitable.
A well-formed strategy hypothesis has five components:
- Observation: what market inefficiency or pattern do you see?
- Hypothesis: what do you predict will happen, and why?
- Test metric: what statistic would confirm or reject the hypothesis?
- Null hypothesis: what would the data look like if the effect is noise?
- Significance threshold: what p-value or effect size convinces you?
This notebook provides a complete reusable template for hypothesis-driven strategy research:
- Structures the hypothesis with the five-component framework
- Tests the hypothesis statistically on historical data
- Separates in-sample exploration from out-of-sample validation
- Reports the test result with effect size and confidence intervals
- Documents decision to proceed or reject the strategy idea
- Exports the test results
!pip install numpy pandas matplotlib seaborn scipy yfinance --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Section 1 — Hypothesis Definition
Fill in this section before running any code. The hypothesis must be written down and locked before looking at the data — this prevents unconscious data snooping. The example below tests a simple momentum hypothesis: that a 7-day return predicts the next 3-day return in BTC. Replace these with your own hypothesis.
# ── Hypothesis Definition (fill before running any data analysis) ──
HYPOTHESIS = {
'observation': 'BTC tends to continue moving in the same direction over short windows',
'hypothesis': '7-day past return predicts 3-day forward return (momentum effect)',
'test_metric': 'Pearson correlation between 7d past return and 3d forward return',
'null_hypothesis': 'Correlation is zero (no predictive power)',
'significance': 0.05, # p-value threshold
'min_effect': 0.05, # minimum correlation to be economically meaningful
}
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False
TICKER = 'BTC-USD'
START_DATE = '2020-01-01'
END_DATE = '2024-12-31'
# ──────────────────────────────────────────────────
LOOKBACK_DAYS = 7 # signal window
FORWARD_DAYS = 3 # prediction target window
TRAIN_FRAC = 0.60 # 60% in-sample, 40% out-of-sample
print('Hypothesis:')
for k, v in HYPOTHESIS.items():
print(f' {k}: {v}')Hypothesis: observation: BTC tends to continue moving in the same direction over short windows hypothesis: 7-day past return predicts 3-day forward return (momentum effect) test_metric: Pearson correlation between 7d past return and 3d forward return null_hypothesis: Correlation is zero (no predictive power) significance: 0.05 min_effect: 0.05
Section 2 — Data Acquisition
We fetch the full price history and immediately split it into in-sample (training) and out-of-sample (test) sets. The out-of-sample data must not be looked at until the hypothesis test is complete on the in-sample data — otherwise any finding is invalidated by data snooping.
if USE_LIVE_DATA:
import yfinance as yf
prices = yf.download(TICKER, start=START_DATE, end=END_DATE)['Close']
prices.name = 'BTC'
print(f'Live data: {len(prices)} days')
else:
rng = np.random.default_rng(42)
n = 1200
# Add slight momentum: returns autocorrelated at lag 1
raw = rng.standard_t(df=4, size=n) * 0.022
rets = np.zeros(n)
for i in range(1, n):
rets[i] = 0.12 * rets[i-1] + raw[i] # 12% autocorrelation → weak momentum
prices = pd.Series((1 + rets).cumprod() * 30000,
index=pd.date_range('2020-01-01', periods=n, freq='B'),
name='BTC')
print(f'Synthetic data: {n} days with weak momentum built in')
# Split in-sample / out-of-sample BEFORE building features
split_idx = int(len(prices) * TRAIN_FRAC)
prices_train = prices.iloc[:split_idx]
prices_test = prices.iloc[split_idx:]
print(f'In-sample: {len(prices_train)} days | Out-of-sample: {len(prices_test)} days')Synthetic data: 1200 days with weak momentum built in In-sample: 720 days | Out-of-sample: 480 days
Section 3 — Feature Engineering & In-Sample Test
We compute the signal (past LOOKBACK_DAYS return) and the target (future FORWARD_DAYS return) for the in-sample period only. Then we run a statistical test — Pearson correlation with a t-test for significance. The t-statistic follows a t-distribution under the null hypothesis of zero correlation, allowing us to compute the p-value rigorously.
def build_features(prices: pd.Series, lookback: int, forward: int) -> pd.DataFrame:
"""
Build signal and target variables from a price series.
Parameters
----------
prices : pd.Series Daily prices.
lookback : int Past window for signal (days).
forward : int Future window for target (days).
Returns
-------
pd.DataFrame Columns: 'signal' (past return), 'target' (future return).
"""
signal = prices.pct_change(lookback)
target = prices.pct_change(forward).shift(-forward)
df = pd.DataFrame({'signal': signal, 'target': target}).dropna()
return df
# In-sample test
df_train = build_features(prices_train, LOOKBACK_DAYS, FORWARD_DAYS)
corr_val, p_val = stats.pearsonr(df_train['signal'], df_train['target'])
n_obs = len(df_train)
# Bootstrap CI for correlation
rng = np.random.default_rng(42)
boot_corrs = []
for _ in range(2000):
idx = rng.integers(0, n_obs, n_obs)
boot_corrs.append(stats.pearsonr(df_train['signal'].iloc[idx],
df_train['target'].iloc[idx])[0])
ci_lo, ci_hi = np.percentile(boot_corrs, [2.5, 97.5])
print('=== IN-SAMPLE HYPOTHESIS TEST RESULTS ===')
print(f'Hypothesis: {HYPOTHESIS["hypothesis"]}')
print(f'Observations: {n_obs}')
print(f'Pearson corr: {corr_val:.4f}')
print(f'95% Bootstrap CI: [{ci_lo:.4f}, {ci_hi:.4f}]')
print(f'p-value: {p_val:.4f}')
print(f'Significance threshold: {HYPOTHESIS["significance"]}')
print(f'Min economic effect: {HYPOTHESIS["min_effect"]}')
print()
passed_stat = p_val < HYPOTHESIS['significance']
passed_econ = abs(corr_val) >= HYPOTHESIS['min_effect']
print(f'Statistical significance: {"PASS" if passed_stat else "FAIL"}')
print(f'Economic significance: {"PASS" if passed_econ else "FAIL"}')
print(f'Recommendation: {"PROCEED to out-of-sample test" if passed_stat and passed_econ else "REJECT hypothesis"}')=== IN-SAMPLE HYPOTHESIS TEST RESULTS === Hypothesis: 7-day past return predicts 3-day forward return (momentum effect) Observations: 710 Pearson corr: 0.0214 95% Bootstrap CI: [-0.0461, 0.0873] p-value: 0.5693 Significance threshold: 0.05 Min economic effect: 0.05 Statistical significance: FAIL Economic significance: FAIL Recommendation: REJECT hypothesis
Section 4 — Out-of-Sample Validation
Only if the in-sample test passes do we look at the out-of-sample data. If the effect is real, it should replicate on unseen data with similar magnitude. A large drop in correlation from in-sample to out-of-sample is a red flag for overfitting — even if the in-sample test passed.
df_test = build_features(prices_test, LOOKBACK_DAYS, FORWARD_DAYS)
corr_oos, p_val_oos = stats.pearsonr(df_test['signal'], df_test['target'])
print('=== OUT-OF-SAMPLE VALIDATION RESULTS ===')
print(f'Out-of-sample corr: {corr_oos:.4f} (in-sample was {corr_val:.4f})')
print(f'Degradation: {(corr_val - corr_oos):.4f} ({(corr_val-corr_oos)/max(abs(corr_val),1e-9):.0%})')
print(f'OOS p-value: {p_val_oos:.4f}')
print(f'OOS observations: {len(df_test)}')=== OUT-OF-SAMPLE VALIDATION RESULTS === Out-of-sample corr: 0.0596 (in-sample was 0.0214) Degradation: -0.0382 (-179%) OOS p-value: 0.1969 OOS observations: 470
Section 5 — Visualisation
The left panel is a scatter plot of signal vs target with a regression line — a positive slope confirms the momentum direction. The right panel shows the bootstrap distribution of the in-sample correlation with the observed value marked. The width of the distribution relative to the mean indicates whether the effect is reliably detected.
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle('Strategy Hypothesis Test Results', fontsize=13, fontweight='bold')
ax1 = axes[0]
ax1.scatter(df_train['signal'], df_train['target'], s=3, alpha=0.3, color='#9e9e9e')
m, b = np.polyfit(df_train['signal'], df_train['target'], 1)
x_line = np.linspace(df_train['signal'].min(), df_train['signal'].max(), 100)
ax1.plot(x_line, m * x_line + b, color='#e53935', lw=2,
label=f'r={corr_val:.3f}, p={p_val:.3f}')
ax1.set_xlabel(f'{LOOKBACK_DAYS}d Past Return (Signal)')
ax1.set_ylabel(f'{FORWARD_DAYS}d Forward Return (Target)')
ax1.legend(fontsize=9)
ax1.set_title('Signal vs Target Scatter (In-Sample)')
ax2 = axes[1]
ax2.hist(boot_corrs, bins=50, color='#1976d2', alpha=0.7, density=True)
ax2.axvline(corr_val, color='#e53935', lw=2, label=f'Observed r={corr_val:.3f}')
ax2.axvline(ci_lo, color='green', ls='--', lw=1, label=f'95% CI [{ci_lo:.3f}, {ci_hi:.3f}]')
ax2.axvline(ci_hi, color='green', ls='--', lw=1)
ax2.axvline(0, color='black', lw=0.8, ls=':')
ax2.set_xlabel('Bootstrap Correlation')
ax2.legend(fontsize=8)
ax2.set_title('Bootstrap Distribution of Correlation')
plt.tight_layout(); plt.show()Section 6 — Export
Save the test results and feature data for in-sample and out-of-sample periods. The results summary is suitable for inclusion in a research log.
summary = pd.DataFrame([{
'hypothesis': HYPOTHESIS['hypothesis'],
'is_corr': corr_val, 'is_pval': p_val, 'is_n': n_obs,
'oos_corr': corr_oos, 'oos_pval': p_val_oos, 'oos_n': len(df_test),
'ci_lo': ci_lo, 'ci_hi': ci_hi,
'stat_pass': passed_stat, 'econ_pass': passed_econ
}])
summary.to_csv('strategy_hypothesis_template.csv', index=False)
print('Saved: strategy_hypothesis_template.csv')Saved: strategy_hypothesis_template.csv
Conclusion
Based on the in-sample and out-of-sample tests, we can conclude the following regarding the hypothesis:
- In-Sample Results:
- Out-of-Sample Results:
- Decision:
Further steps could include exploring alternative features, different timeframes, or applying this framework to other assets.