refactor: riorganizzazione script — Strategy ABC, folder strategies/waste/analysis
- src/strategies/base.py: Strategy ABC con Signal, BacktestResult, YearlyStats - src/strategies/indicators.py: keltner_ratio, detect_squeezes, ema, atr, rv, corr - scripts/strategies/: SQ01-SQ04 (squeeze puro/filtri), ML01 (squeeze+GBM) - scripts/waste/: W01-W22 script scartati + REF originali - scripts/analysis/: compare, best_yearly, final_report, paper_status - CLAUDE.md aggiornato con nuova struttura e tabella strategie Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""S2-01: Mean Reversion oraria con filtro orario.
|
||||
Idea: crypto ha bias di ritorno alla media nelle ore notturne (00-06 UTC)
|
||||
e di momentum nelle ore diurne USA (14-20 UTC).
|
||||
- Compra quando RSI < 30 in ore notturne
|
||||
- Vendi quando RSI > 70 in ore notturne
|
||||
- Hold max 4h, stop loss 1.5%
|
||||
Timeframe: 1h. Ingresso quasi giornaliero.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
|
||||
FEE = 0.001
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def rsi(close: np.ndarray, period: int = 14) -> np.ndarray:
|
||||
delta = np.diff(close)
|
||||
gain = np.where(delta > 0, delta, 0)
|
||||
loss = np.where(delta < 0, -delta, 0)
|
||||
result = np.full(len(close), 50.0)
|
||||
avg_gain = np.mean(gain[:period])
|
||||
avg_loss = np.mean(loss[:period])
|
||||
for i in range(period, len(delta)):
|
||||
avg_gain = (avg_gain * (period - 1) + gain[i]) / period
|
||||
avg_loss = (avg_loss * (period - 1) + loss[i]) / period
|
||||
if avg_loss == 0:
|
||||
result[i + 1] = 100
|
||||
else:
|
||||
rs = avg_gain / avg_loss
|
||||
result[i + 1] = 100 - 100 / (1 + rs)
|
||||
return result
|
||||
|
||||
|
||||
def bollinger_pct(close: np.ndarray, window: int = 20) -> np.ndarray:
|
||||
result = np.full(len(close), 0.5)
|
||||
for i in range(window, len(close)):
|
||||
w = close[i - window : i]
|
||||
ma = np.mean(w)
|
||||
std = np.std(w)
|
||||
if std > 0:
|
||||
result[i] = (close[i] - (ma - 2 * std)) / (4 * std)
|
||||
return result
|
||||
|
||||
|
||||
def run_mean_reversion(asset, tf="1h"):
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
n = len(df)
|
||||
|
||||
timestamps = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
hours = timestamps.dt.hour.values
|
||||
|
||||
rsi_vals = rsi(close, 14)
|
||||
bb_pct = bollinger_pct(close, 20)
|
||||
|
||||
split = int(n * 0.7)
|
||||
|
||||
configs = [
|
||||
# (rsi_low, rsi_high, allowed_hours, hold_max, stop_pct, name)
|
||||
(25, 75, list(range(0, 7)), 4, 0.015, "night_0-6_rsi25"),
|
||||
(30, 70, list(range(0, 7)), 4, 0.015, "night_0-6_rsi30"),
|
||||
(25, 75, list(range(0, 10)), 6, 0.02, "extended_0-9"),
|
||||
(30, 70, list(range(20, 24)) + list(range(0, 6)), 4, 0.015, "late_night"),
|
||||
(20, 80, list(range(0, 24)), 4, 0.015, "all_hours_rsi20"),
|
||||
# Bollinger band mean reversion
|
||||
]
|
||||
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} {tf} — MEAN REVERSION")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
for rsi_low, rsi_high, allowed, hold_max, stop, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
|
||||
for i in range(max(split, 20), n - hold_max):
|
||||
hour = hours[i]
|
||||
if hour not in allowed:
|
||||
continue
|
||||
|
||||
day = timestamps[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 2:
|
||||
continue
|
||||
|
||||
direction = None
|
||||
if rsi_vals[i] < rsi_low and bb_pct[i] < 0.2:
|
||||
direction = "long"
|
||||
elif rsi_vals[i] > rsi_high and bb_pct[i] > 0.8:
|
||||
direction = "short"
|
||||
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry = close[i]
|
||||
best_exit = i + 1
|
||||
for j in range(i + 1, min(i + hold_max + 1, n)):
|
||||
price = close[j]
|
||||
if direction == "long":
|
||||
pnl_pct = (price - entry) / entry
|
||||
if pnl_pct <= -stop:
|
||||
best_exit = j
|
||||
break
|
||||
if pnl_pct >= stop * 1.5:
|
||||
best_exit = j
|
||||
break
|
||||
else:
|
||||
pnl_pct = (entry - price) / entry
|
||||
if pnl_pct <= -stop:
|
||||
best_exit = j
|
||||
break
|
||||
if pnl_pct >= stop * 1.5:
|
||||
best_exit = j
|
||||
break
|
||||
best_exit = j
|
||||
|
||||
exit_price = close[best_exit]
|
||||
if direction == "long":
|
||||
trade_ret = (exit_price - entry) / entry
|
||||
else:
|
||||
trade_ret = (entry - exit_price) / entry
|
||||
|
||||
net = trade_ret * LEVERAGE - FEE * 2 * LEVERAGE
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 0)
|
||||
|
||||
is_correct = trade_ret > 0
|
||||
total += 1
|
||||
if is_correct:
|
||||
correct += 1
|
||||
daily_trades[day] = daily_trades.get(day, 0) + 1
|
||||
|
||||
if total < 20:
|
||||
continue
|
||||
|
||||
acc = correct / total * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
test_days = (n - split) / 24
|
||||
test_years = test_days / 365.25
|
||||
ann = ((capital / INITIAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
dpnl = (capital - INITIAL) / test_days if test_days > 0 else 0
|
||||
days_with_trades = len(daily_trades)
|
||||
trades_per_day = total / days_with_trades if days_with_trades > 0 else 0
|
||||
|
||||
tag = "✅" if acc >= 60 and ann >= 30 else ""
|
||||
print(f" {name:25s}: trades={total:5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% dd_est ~{abs(min(0, ret/3)):.0f}% €/day={dpnl:.2f} days_active={days_with_trades} {tag}")
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
run_mean_reversion(asset, "1h")
|
||||
run_mean_reversion(asset, "15m")
|
||||
Reference in New Issue
Block a user