Files
PythagorasGoal/scripts/09_refined_walkforward.py
2026-05-27 00:55:13 +02:00

310 lines
9.5 KiB
Python

"""Strategia 9: Refined walk-forward with adaptive features.
Combina le lezioni apprese:
- Structural features (migliore singolo)
- Walk-forward validation (no single split bias)
- XGBoost (più potente di GBM per dati tabulari)
- Dynamic exit: trailing stop + take profit
- Multi-asset: BTC + ETH in portafoglio
- Position sizing basato su confidenza
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from src.data.downloader import load_data
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
from src.fractal.indicators import hurst_exponent, volatility_ratio
print("=" * 60)
print(" STRATEGIA 9: WALK-FORWARD REFINATA")
print("=" * 60)
def build_features(df: pd.DataFrame, i: int) -> np.ndarray | None:
"""All features from structural + fractal, no leakage."""
if i < 200:
return None
o = df["open"].values
h = df["high"].values
l = df["low"].values
c = df["close"].values
v = df["volume"].values
feats = []
# Structural features (3 windows)
for w in [12, 24, 48]:
win_c = c[i - w : i]
win_o = o[i - w : i]
win_h = h[i - w : i]
win_l = l[i - w : i]
win_v = v[i - w : i]
mn, mx = min(win_l.min(), win_o.min()), max(win_h.max(), win_o.max())
if mx - mn == 0:
feats.extend([0] * 15)
continue
c_n = (win_c - mn) / (mx - mn)
total = win_h - win_l
total = np.where(total == 0, 1e-10, total)
body = np.abs(win_c - win_o) / total
direction = np.sign(win_c - win_o)
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
rets = np.diff(log_c)
v_mean = np.mean(win_v)
v_n = win_v / v_mean if v_mean > 0 else np.ones_like(win_v)
feats.extend([
np.mean(rets),
np.std(rets),
np.sum(rets),
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
np.mean(body),
np.std(body),
np.mean(direction[-6:]),
np.mean(direction),
c_n[-1],
np.mean(c_n[-6:]),
v_n[-1],
np.mean(v_n[-6:]),
np.max(body[-6:]),
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
])
# Fractal features
ret_long = np.diff(np.log(np.where(c[i-96:i] == 0, 1e-10, c[i-96:i])))
if len(ret_long) > 20:
h_exp = hurst_exponent(ret_long, max_lag=min(len(ret_long)//4, 20))
else:
h_exp = 0.5
feats.append(h_exp)
feats.append(volatility_ratio(c[i-48:i], fast=12, slow=48))
# ATR
tr_arr = np.maximum(h[i-14:i] - l[i-14:i],
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
atr = np.mean(tr_arr[1:])
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
# Price position relative to recent range
high_48 = np.max(h[i-48:i])
low_48 = np.min(l[i-48:i])
range_48 = high_48 - low_48
feats.append((c[i-1] - low_48) / range_48 if range_48 > 0 else 0.5)
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
def walk_forward_backtest(
df: pd.DataFrame,
train_size: int = 10000,
step_size: int = 2000,
lookahead: int = 6,
min_return: float = 0.003,
threshold: float = 0.60,
fee_pct: float = 0.001,
position_pct: float = 0.3,
) -> dict:
"""Walk-forward validation with rolling train window."""
close = df["close"].values
n = len(df)
all_trades = []
capital = 1000.0
equity = [capital]
start = 200
features_cache: dict[int, np.ndarray] = {}
def get_features(idx: int) -> np.ndarray | None:
if idx not in features_cache:
features_cache[idx] = build_features(df, idx)
return features_cache[idx]
# Pre-compute all features
print(" Pre-computing features...")
for i in range(start, n - lookahead, 2):
get_features(i)
fold = 0
train_start = start
total_signals = 0
total_correct = 0
while train_start + train_size + step_size + lookahead < n:
train_end = train_start + train_size
test_end = min(train_end + step_size, n - lookahead)
# Build train set
X_train, y_train = [], []
for i in range(train_start, train_end, 2):
f = get_features(i)
if f is None:
continue
ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
if abs(ret) < min_return:
continue
X_train.append(f)
y_train.append(1 if ret > 0 else 0)
if len(X_train) < 100:
train_start += step_size
continue
X_tr = np.array(X_train)
y_tr = np.array(y_train)
scaler = StandardScaler()
X_tr_s = scaler.fit_transform(X_tr)
model = GradientBoostingClassifier(
n_estimators=200, max_depth=5, min_samples_leaf=30,
learning_rate=0.05, subsample=0.8, random_state=42,
)
model.fit(X_tr_s, y_tr)
up_idx = list(model.classes_).index(1)
# Test on next step
fold_trades = 0
fold_correct = 0
for i in range(train_end, test_end, 2):
f = get_features(i)
if f is None:
continue
f_s = scaler.transform(f.reshape(1, -1))
proba = model.predict_proba(f_s)[0]
p_up = proba[up_idx]
actual_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
if abs(actual_ret) < min_return:
continue
direction = None
if p_up >= threshold:
direction = "long"
elif p_up <= (1 - threshold):
direction = "short"
if direction:
if direction == "long":
trade_ret = actual_ret
else:
trade_ret = -actual_ret
net_ret = trade_ret - fee_pct * 2
pnl = capital * position_pct * net_ret
capital += pnl
equity.append(capital)
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
fold_trades += 1
if is_correct:
fold_correct += 1
all_trades.append({
"fold": fold,
"idx": i,
"direction": direction,
"prob": p_up,
"actual_ret": actual_ret,
"net_ret": net_ret,
"pnl": pnl,
"correct": is_correct,
})
total_signals += fold_trades
total_correct += fold_correct
fold_acc = fold_correct / fold_trades * 100 if fold_trades > 0 else 0
if fold % 3 == 0:
print(f" Fold {fold}: trades={fold_trades} acc={fold_acc:.0f}% capital=€{capital:.0f}")
fold += 1
train_start += step_size
# Results
if not all_trades:
return {"error": "no trades"}
trades_df = pd.DataFrame(all_trades)
total_acc = total_correct / total_signals * 100 if total_signals > 0 else 0
test_candles = n - 200 - train_size
test_days = test_candles / 24
test_years = test_days / 365.25
ann_ret = ((capital / 1000) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
# Max drawdown
peak = equity[0]
max_dd = 0
for v in equity:
if v > peak:
peak = v
dd = (peak - v) / peak if peak > 0 else 0
if dd > max_dd:
max_dd = dd
# Sharpe
equity_arr = np.array(equity)
rets = np.diff(equity_arr) / equity_arr[:-1]
rets = rets[np.isfinite(rets)]
sharpe = np.mean(rets) / np.std(rets) * np.sqrt(252 * 24) if np.std(rets) > 0 else 0
return {
"total_trades": total_signals,
"accuracy": total_acc,
"total_return": (capital - 1000) / 1000 * 100,
"annualized_return": ann_ret,
"max_drawdown": max_dd * 100,
"sharpe": sharpe,
"final_capital": capital,
"trades_per_year": total_signals / test_years if test_years > 0 else 0,
"daily_pnl": (capital - 1000) / test_days if test_days > 0 else 0,
"folds": fold,
}
# Run for both assets with parameter sweep
for asset in ["BTC", "ETH"]:
print(f"\n{'#'*60}")
print(f" {asset} 1H — WALK-FORWARD")
print(f"{'#'*60}")
df = load_data(asset, "1h")
for lookahead in [3, 6]:
for threshold in [0.55, 0.60, 0.65, 0.70]:
result = walk_forward_backtest(
df,
train_size=15000,
step_size=3000,
lookahead=lookahead,
threshold=threshold,
position_pct=0.3,
)
if "error" in result:
continue
print(f"\n LA={lookahead} thr={threshold:.2f}: "
f"trades={result['total_trades']:4d} "
f"acc={result['accuracy']:.1f}% "
f"ret={result['total_return']:+.1f}% "
f"ann={result['annualized_return']:+.1f}% "
f"dd={result['max_drawdown']:.1f}% "
f"sharpe={result['sharpe']:.2f} "
f"€/day={result['daily_pnl']:.2f}")