Machine learning strategy for ETHUSDT on 30-minute bars. Predicts 2-hour (4-bar) forward returns using walk-forward validation across 2019–2026 data.
Designed for live trading with realistic transaction costs (2–5 bps/side). All reported results are out-of-sample.
Starting from the theoretical no-fee ceiling, each step brings the strategy closer to live-trading reality:
| Step | Sharpe | What changed |
|---|---|---|
| No-fee ceiling (fixed hold) | 2.04 | Raw signal quality, ~2,000 trades/yr |
| → Dynamic hold · 5 bps | 1.11 | Exit when model conviction gone, ~100 trades/yr |
| → + ER > 0.6 filter · 5 bps | 1.29 | Only trade trending regimes, ~60 trades/yr |
| Buy & Hold | 0.73 | Benchmark |
Full dataset 2019→2026. Split 80/20: params selected on first 80%, evaluated on last 20% (never touched during tuning).
Walk-forward param selection: 6 folds, 20% train → 10% validate → step 10%.
| Sharpe | Ann Return | Max DD | RT/yr | |
|---|---|---|---|---|
| Dynamic hold · 5 bps | 1.11 | 50.7% | -45.7% | 182 |
| + ER > 0.7 filter · 5 bps | 1.32 | 37.1% | -26.1% | 60 |
| Buy & Hold | 0.74 | — | — | — |
Fold stability (all 8 folds, ER filter, 5 bps):
| Mean Sharpe | Std | Min (worst fold) | All positive? |
|---|---|---|---|
| 1.37 | 0.61 | 0.35 | Yes |
ETH fell ~60% from its Dec 2024 peak during this period.
| Sharpe | Ann Return | Max DD | |
|---|---|---|---|
| + ER > 0.7 filter · 5 bps | 1.64 | 56.5% | -31.2% |
| Dynamic hold (no filter) · 5 bps | 1.59 | 83.9% | -23.4% |
| Buy & Hold | -0.34 | — | — |
Params locked from dev: PCT=92nd percentile threshold, ER>0.7, 5 bps/side. Test Sharpe (1.64) consistent with dev mean (1.32) — no evidence of overfit.
Separate check on data fetched after development. Train on last 20% before Aug 2025, predict forward.
| Sharpe | Ann Return | Max DD | |
|---|---|---|---|
| + ER > 0.6 filter · 5 bps | 2.03 | +68.8% | -8.6% |
| Buy & Hold | -1.89 | -79.0% | -63.8% |
| Strategy | Sharpe | Annual Return | Max Drawdown |
|---|---|---|---|
| Ensemble fixed hold — 80th pctile | 2.07 | 158.6% | -30.2% |
| ElasticNet standalone | 1.94 | 154.2% | -42.9% |
| LightGBM standalone | 1.86 | 141.5% | -46.4% |
Fixed hold makes ~2,000 position changes/year. Sharpe collapses below 0 at 5 bps/side. Dynamic hold + ER filter reduces this to ~60 round-trips/year.
LightGBM + ElasticNet ensemble predicting 4-bar (2h) forward return. Predictions are z-score normalized per model using training statistics, then averaged.
- Threshold = 95th percentile of |z-score| on training set
- Enter long when
mean_z > threshold - Enter short when
mean_z < -threshold + drift_bias - Efficiency Ratio filter: only enter when
ER > 0.6(trending regime)
Instead of exiting after a fixed N bars:
- Hold for at least 4 bars (minimum)
- Stay in position while the 4-bar prediction remains in the same direction
- Exit when prediction crosses zero — model conviction gone
- Average hold: ~20 bars (10 hours)
The Kaufman Efficiency Ratio measures trending (≈1) vs choppy (≈0) markets:
ER = |net price move over 10 bars| / sum(|each bar's move| over 10 bars)
ER > 0.6 fires on only 12% of bars — the strongest trending periods. This filter sat out the entire 2021–2022 crypto bear market (choppy, mean-reverting) while the baseline strategy bled through 385 days of drawdown.
Baseline MDD: -42.5% peak 2021-05-23 → trough 2022-06-13 (385 days)
ER>0.6 MDD: -25.6% peak 2025-03-02 → trough 2025-03-11 (8 days)
Sliding window: 20% train → 10% validate → step 10% forward, 8 folds.
Fold 1: |████████████████████|░░░░░░░░░░|
Fold 2: |████████████████████|░░░░░░░░░░|
...
Fold 8: |████████████████████|░░░░░░░░░░|
▓ = Train (20%) ░ = Validation (10%)
| Category | Features |
|---|---|
| Returns | return_1, return_4, return_48, return_96 |
| Volatility | volatility_48, max_volatility_480 |
| Momentum | rsi, sma_cross, roc_20 |
| Price/SMA | price_to_sma20, price_to_sma50, price_to_ema20 |
| Price-to-max | close_to_max_240, close_to_max_2400 |
| Volume | volume_ratio, volume_to_max_240, volume_to_max_480, force |
Same as above minus return_96 and close_to_max_2400, plus lagged returns:
| Feature | Description |
|---|---|
return_4_lag48 |
2h return seen 24h ago |
return_4_lag96 |
2h return seen 48h ago |
Feature importance (LightGBM, avg across 8 folds):
volatility_48 16.6% ████████
close_to_max_2400 11.4% █████
close_to_max_240 10.3% █████
sma_cross 9.8% ████
return_96 9.4% ████
max_volatility_480 8.7% ████ ← top 6 = 66% of total gain
Why the ensemble works: LightGBM and ElasticNet have low signal correlation (0.19) — genuinely independent models. Averaging their z-scored predictions smooths out each model's bad folds.
Prediction quality is weak but consistent:
- LightGBM directional accuracy on 4-bar target: ~50.1% (barely above random)
- Alpha comes from consistent application over thousands of bars, not individual accuracy
- The 4-bar model has 2.5× better signal/noise than a 1-bar model — don't use short-horizon for exits
eth-alpha/
├── data/
│ └── ETHUSDT.csv # Raw 30-min OHLCV (2019–2026, ~110k bars)
├── src/
│ ├── features.py # generate_features(), get_data()
│ ├── signals.py # build_position_dynamic(), build_position_filtered()
│ ├── backtest.py # evaluate_holdN(cost_bps=)
│ ├── models.py # All model definitions
│ └── walk_forward.py # run_walk_forward(), eva_full_result()
├── configs/
│ ├── strategy_lgb.yaml # Tree model features (18)
│ └── strategy_linear.yaml # Linear model features (19, +lags)
├── assets/
│ ├── equity_curve.png # Improvement story plot
│ └── live_eval.png # Live evaluation plot
│
├── run_er_filter.py # ★ Final strategy: ER filter + dynamic hold
├── run_live_eval.py # ★ Live evaluation Aug 2025 → Mar 2026
├── run_clean_split.py # ★ Clean 80/20 split validation
├── run_holdout_test.py # Holdout test: confirm no overfit
├── fetch_new_data.py # Fetch latest OHLCV from Binance
├── run_plot_equity.py # Generate improvement story chart
├── run_dynamic_threshold_sweep.py # Threshold sweep for dynamic hold
├── run_costs.py # Cost sweep across all models
├── run_best.py # Best standalone models
├── run_feature_importance.py # LightGBM feature importance
├── run_signal_corr.py # Signal correlation between models
├── run_stability.py # Per-fold Sharpe stability
└── run_pred_quality.py # Prediction quality diagnostic
pip install pandas numpy scikit-learn lightgbm xgboost pyyaml scipy matplotlib requests
# Fetch latest data (extends dataset to current date)
python fetch_new_data.py
# ★ Final strategy with transaction costs
python run_er_filter.py
# ★ Live evaluation on Aug 2025 → Mar 2026
python run_live_eval.py
# Clean 80/20 split (params from first 80%, test on last 20%)
python run_clean_split.py
# Holdout test (confirm no overfit to dev period)
python run_holdout_test.py
# Cost sweep to see fee sensitivity
python run_costs.py- No slippage modeled — live performance will be lower for large sizes
- No position sizing — raw signal evaluation; vol-targeting would reduce MDD further
- ER filter based on 10-bar lookback — longer lookback (e.g. 20) could be more stable
- 2019–2020 data is training-only; all reported metrics are 2021+ OOS
- The live eval period (Aug 2025–Mar 2026) was not used in any parameter selection

