Research·Strategy Hypothesis Testing·Advanced

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.

quant-researchtrading-strategies

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:

  1. Observation: what market inefficiency or pattern do you see?
  2. Hypothesis: what do you predict will happen, and why?
  3. Test metric: what statistic would confirm or reject the hypothesis?
  4. Null hypothesis: what would the data look like if the effect is noise?
  5. Significance threshold: what p-value or effect size convinces you?

This notebook provides a complete reusable template for hypothesis-driven strategy research:

  1. Structures the hypothesis with the five-component framework
  2. Tests the hypothesis statistically on historical data
  3. Separates in-sample exploration from out-of-sample validation
  4. Reports the test result with effect size and confidence intervals
  5. Documents decision to proceed or reject the strategy idea
  6. Exports the test results
[ ]
!pip install numpy pandas matplotlib seaborn scipy yfinance --quiet
[ ]
import 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()
cell output

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.

Strategy Hypothesis Template · BitPredict