A research framework for discovering, combining, and backtesting algorithmic trading strategies on BTC price data. The project explores whether classical technical signals — individually weak — can be made profitable through regime filtering, multi-signal confluence scoring, and mathematically-grounded position sizing.
Rather than implementing a single strategy, this project is a systematic search for edge:
- Generate signals from a library of independent technical strategies
- Detect the market regime (trending vs. ranging vs. noise) to know which signals apply
- Combine signals via weighted confluence scoring — only trade when multiple strategies agree
- Size positions using Kelly Criterion based on empirical win rates
- Backtest everything on historical 1-minute BTC data with realistic fees (0.05% per side), then validate out-of-sample
The best result found: +0.61% net return during BTC's 2022 bear market (when BTC itself fell ~55%), with a 62% win rate and -0.82% max drawdown over 82 trades across 15.5 months.
signals.py — Core signal library (6 classic strategies + OHLCV resampler)
advanced_signals.py — Extended signals (VWAP, Kalman, ATR z-score, LinReg Channel, etc.)
backtest.py — Backtest engine with realistic execution (entry at next open, worst-case stop fills)
strategy_engine.py — Full integrated pipeline (regime → confluence → Kelly → backtest)
regime.py — Market regime classifier (ADX + Efficiency Ratio + Hurst Exponent)
regime_calibrator.py — BTC-specific regime threshold calibration
ou_analysis.py — Ornstein-Uhlenbeck fitting to derive optimal hold periods and stop placement
confluence.py — Weighted multi-strategy confluence combinations
ml_filter.py — Random Forest classifier to filter trade entries by predicted win probability
signal_analysis.py — Bar-by-bar signal matrix and forward return analysis
tuner.py — Per-strategy grid search over parameters
momentum_tuner.py — Joint ROC + intraday momentum parameter grid search
leverage_sweep.py — Sweep over leverage levels to find risk-adjusted optimum
zscore_sweep.py — Sweep over z-score entry thresholds
yearly_analysis.py — Year-by-year performance breakdown
dataset/ — BTC 1-minute OHLCV data (2012–present, ~600k rows)
results/ — Output CSVs, parquets, and history files
logs/ — findings.md (research notes), ideas.md (open questions)
| Signal | Logic |
|---|---|
| MA Crossover | Fast EMA crosses above/below slow EMA |
| Bollinger Bands | Price crosses outside 2σ band |
| Mean Reversion | Rolling z-score > threshold → fade the move |
| Momentum (ROC) | Rate-of-change exceeds threshold → follow momentum |
| Breakout | Price breaks N-bar high/low on elevated volume |
| Intraday Momentum | NYSE open momentum (14:30 UTC trigger) |
| Signal | Logic |
|---|---|
| LinReg Channel | OLS trend z-score of residuals — avoids drift in rolling mean |
| VWAP Reversion | Fade moves far from daily VWAP |
| Kalman Z-score | Kalman-filtered price vs. raw price spread |
| ATR Z-score | Normalize price deviation by ATR (volatility-adjusted) |
| Volume Oscillator | Short/long volume MA ratio to confirm signal quality |
| Macro Trend Filter | 200-day EMA gate to block longs in bear markets |
1-min data
│
▼
5-min OHLCV resample ← reduces noise; fees now ~15% of expected move vs. 33% at 1-min
│
▼
Regime detection ← ADX + Efficiency Ratio + Hurst Exponent (majority vote)
│
├─ TREND → activate momentum/breakout signals (2× weight)
├─ RANGE → activate mean-reversion/VWAP/Kalman signals (2× weight)
└─ NEUTRAL → no trades (empirically 24–33% win rate — pure noise)
│
▼
Confluence score ≥ 2 ← blocks ~95% of signals; only highest-conviction setups
│
▼
ML filter (optional) ← Random Forest P(win) ≥ 0.55 gate
│
▼
ATR-based dynamic stop ← 2.0× ATR for RANGE, 1.5× ATR for TREND
│
▼
OU-derived hold period ← 1.5× mean-reversion half-life (~75 min on 5-min BTC)
│
▼
Kelly position sizing ← half-Kelly (f*/2) to reduce drawdown
│
▼
Backtest (3 modes)
A. Naive — no regime filter, equal weights, fixed params
B. Regime-gated — regime + ATR stops, no Kelly
C. Full engine — all of the above + Kelly sizing
Three independent measures vote on each bar:
- ADX — trend strength (higher = stronger trend regardless of direction)
- Efficiency Ratio —
|net move| / Σ|individual moves|→ 1.0 = clean trend, 0.0 = chop - Hurst Exponent — H > 0.5 = trending, H < 0.5 = mean-reverting, H ≈ 0.5 = random walk
BTC distribution on 5-min bars: ~50% RANGE, ~45% NEUTRAL, ~5% TREND. The TREND regime has historically shown only 9–10% win rate at 1-min resolution and is disabled for trading.
Fits the OU process dX = θ(μ - X)dt + σdW to price data via OLS regression to derive:
- Half-life =
ln(2)/θ→ how long before a deviation decays 50% back to mean - Optimal hold period ≈ 1–2× half-life
- Stop placement ≈ 2–3× σ from entry
On 5-min BTC: half-life ≈ 10 bars (50 min) → hold ≈ 15 bars (75 min).
A Random Forest classifier trained on potential entry bars (from the training set) that predicts whether a trade will be profitable. Features include all signal values, ATR, volume ratio, price momentum at multiple lookbacks, cyclical time-of-day and day-of-week encodings, regime encoding, and z-score magnitude.
Uses shallow trees (max_depth=4, min_samples_leaf=8) and out-of-bag scoring to control overfitting — sample sizes are small (~100–500 entries per training period).
All global parameters live at the top of signals.py:
FEES = 0.0005 # 0.05% per side
DATA_PATH = "dataset/btc_1-min_data.csv"
START_DATE = "2016-01-01"
END_DATE = "2026-03-29"
TRAIN_SPLIT = 0.80 # 80% in-sample, 20% out-of-sampleStrategy parameters in strategy_engine.py under TIMEFRAME_CONFIGS.
# Set up environment
python3.12 -m venv venv
venv/bin/pip install pandas numpy pyarrow scikit-learn
# Run the full strategy engine (all 3 modes on train + test)
venv/bin/python3.12 strategy_engine.py
# Sanity-check advanced signals
venv/bin/python3.12 advanced_signals.py
# Grid search a single strategy's parameters
venv/bin/python3.12 tuner.py
# Sweep z-score thresholds
venv/bin/python3.12 zscore_sweep.py
# Year-by-year breakdown
venv/bin/python3.12 yearly_analysis.pyResults are written to results/ as CSV/parquet. Research notes accumulate in logs/findings.md.
dataset/btc_1-min_data.csv — BTC/USD 1-minute OHLCV from 2012 to present (~600k rows).
Columns: Timestamp (Unix), Open, High, Low, Close, Volume
Data is loaded in chunks (50k rows at a time) and filtered to the configured date range to avoid loading the full file into memory.
- 1-min BTC is fundamentally mean-reverting: ~53% of bars are in RANGE regime; TREND only 3–5%.
- Fees kill 1-min signals: at 1-min, fees consume ~33% of expected move; at 5-min this drops to ~15%.
- Mean Reversion and Bollinger Bands produce identical signals at current parameters — never use both.
- Dual confirmation blocks 95% of trades but keeps only the highest-conviction setups with 57–58% win rate.
- The 200-day EMA macro filter is critical: during the 2022 bear market, it blocked longs entirely, reducing losing long trades from 84 to 13.
- LinReg Channel is cleaner than rolling mean z-score: rolling mean drifts with price in trends, creating false normalizations; OLS regression accounts for the trend direction.
- Kelly (half) reduces drawdown significantly on training data (-5% → -0.36%) at the cost of lower upside.
- Dynamic stop/target: scale stop size with volatility (ATR-based), or place stop just below previous local minimum in a zig-zag pattern.
- Confidence-based position sizing: allocate 10% of capital when uncertain, up to 70% when highly confident — beyond fixed Kelly.
- Neural network entry filter: train a model on all strategy outputs simultaneously to find patterns that rule-based confluence misses.