Загрузка данных
from pathlib import Path
path = Path("/content/AIQuant/aiquant/models/ensemble_pipeline.py")
text = path.read_text()
old = """ best_r = None
for long_thresh in [0.05, 0.08, 0.10, 0.12, 0.15, 0.18, 0.20, 0.25]:
for short_thresh in [-0.05, -0.08, -0.10, -0.12, -0.15, -0.18, -0.20, -0.25]:
sig = np.zeros(n, np.int8)
sig[oos_mask & (ens_score > long_thresh)] = 1
sig[oos_mask & (ens_score < short_thresh)] = -1
n_trades = int(np.sum(np.abs(np.diff(sig.astype(int)))) // 2)
if n_trades < 20: continue # must have at least 20 trades
changes = np.where(np.diff(sig, prepend=sig[0]) != 0)[0]
cap = capital
equity = np.full(n, capital, dtype=np.float64)
wins = losses = 0
gw = gl = 0.0
for i in range(len(changes) - 1):
eb = changes[i]; xb = changes[i + 1]
d = int(sig[eb])
if d == 0:
equity[eb:xb] = cap
continue
ep = c[eb]; xp = c[xb]
sz = (cap * 0.25) / ep
pnl = d * (xp - ep) * sz * (1 - FEE) ** 2
cap = max(cap + pnl, 1.0)
equity[eb:xb] = cap
if pnl > 0: wins += 1; gw += pnl
else: losses += 1; gl += abs(pnl)
equity[changes[-1]:] = cap
total_ret = (cap - capital) / capital * 100
peak = np.maximum.accumulate(equity)
dd = (equity - peak) / peak * 100
max_dd = float(np.min(dd))
n_days = len(equity) // 1440
if n_days > 2:
dl = np.array([
np.diff(np.log(equity[i*1440:(i+1)*1440] + 1e-10)).sum()
for i in range(n_days)
])
sharpe = float(dl.mean() / (dl.std() + 1e-10) * np.sqrt(365))
else:
sharpe = 0.0
tt = wins + losses
wr = wins / tt * 100 if tt > 0 else 0.0
cal = total_ret / abs(max_dd) if abs(max_dd) > 0.01 else 0.0
pf = gw / (gl + 1e-10)
r = dict(
sharpe=round(sharpe, 3), ret=round(total_ret, 2),
max_dd=round(max_dd, 2), calmar=round(cal, 3),
pf=round(pf, 3), trades=tt, win_rate=round(wr, 1),
final=round(cap, 2), equity=equity, signals=sig,
long_thresh=long_thresh, short_thresh=short_thresh,
)
if best_r is None or r['sharpe'] > best_r['sharpe']:
best_r = r
col = GREEN if r['sharpe'] >= 2.0 else (YELLOW if r['sharpe'] >= 1.0 else RED)
sh_str = f"{r['sharpe']:+.3f}"
print(
f" {col('↑')} Sharpe={col(sh_str)} "
f"Ret={r['ret']:+.1f}% MaxDD={r['max_dd']:.1f}% "
f"Trades={r['trades']:,} WR={r['win_rate']:.0f}% "
f"Calmar={r['calmar']:.2f} "
f"[L>{long_thresh} S<{short_thresh}]"
)
"""
new = """ # ── Honest threshold selection ───────────────────────────────────────────
# Use the first 70% of OOS predictions to choose thresholds.
# Keep the final 30% completely untouched for the final evaluation.
oos_idx = np.where(oos_mask)[0]
split = int(len(oos_idx) * 0.70)
val_idx = oos_idx[:split]
test_idx = oos_idx[split:]
def _evaluate_threshold(long_thresh, short_thresh, idx):
sig = np.zeros(n, np.int8)
sig[idx[ens_score[idx] > long_thresh]] = 1
sig[idx[ens_score[idx] < short_thresh]] = -1
n_trades = int(np.sum(np.abs(np.diff(sig.astype(int)))) // 2)
if n_trades < 20:
return None
changes = np.where(np.diff(sig, prepend=sig[0]) != 0)[0]
cap = capital
equity = np.full(n, capital, dtype=np.float64)
wins = losses = 0
gw = gl = 0.0
for i in range(len(changes) - 1):
eb = changes[i]
xb = changes[i + 1]
d = int(sig[eb])
if d == 0:
equity[eb:xb] = cap
continue
ep = c[eb]
xp = c[xb]
sz = (cap * 0.25) / ep
pnl = d * (xp - ep) * sz * (1 - FEE) ** 2
cap = max(cap + pnl, 1.0)
equity[eb:xb] = cap
if pnl > 0:
wins += 1
gw += pnl
else:
losses += 1
gl += abs(pnl)
equity[changes[-1]:] = cap
total_ret = (cap - capital) / capital * 100
peak = np.maximum.accumulate(equity)
dd = (equity - peak) / peak * 100
max_dd = float(np.min(dd))
n_days = len(equity) // 1440
if n_days > 2:
dl = np.array([
np.diff(np.log(equity[i*1440:(i+1)*1440] + 1e-10)).sum()
for i in range(n_days)
])
sharpe = float(dl.mean() / (dl.std() + 1e-10) * np.sqrt(365))
else:
sharpe = 0.0
tt = wins + losses
wr = wins / tt * 100 if tt > 0 else 0.0
cal = total_ret / abs(max_dd) if abs(max_dd) > 0.01 else 0.0
pf = gw / (gl + 1e-10)
return dict(
sharpe=round(sharpe, 3),
ret=round(total_ret, 2),
max_dd=round(max_dd, 2),
calmar=round(cal, 3),
pf=round(pf, 3),
trades=tt,
win_rate=round(wr, 1),
final=round(cap, 2),
equity=equity,
signals=sig,
long_thresh=long_thresh,
short_thresh=short_thresh,
)
# Thresholds are selected ONLY on validation OOS data.
best_val = None
for long_thresh in [0.05, 0.08, 0.10, 0.12, 0.15, 0.18, 0.20, 0.25]:
for short_thresh in [-0.05, -0.08, -0.10, -0.12, -0.15, -0.18, -0.20, -0.25]:
r = _evaluate_threshold(long_thresh, short_thresh, val_idx)
if r is None:
continue
if best_val is None or r['sharpe'] > best_val['sharpe']:
best_val = r
if best_val is None:
print(f" {RED('✗')} No valid threshold found on validation set.")
return {}
best_long = best_val['long_thresh']
best_short = best_val['short_thresh']
print(
f" {GREEN('✓')} Selected thresholds on validation: "
f"L>{best_long} S<{best_short} "
f"(validation Sharpe={best_val['sharpe']:+.3f})"
)
# Final result is measured ONLY on the untouched final 30%.
best_r = _evaluate_threshold(best_long, best_short, test_idx)
if best_r is None:
print(f" {RED('✗')} No valid trades on final test set.")
return {}
print(
f" {GREEN('✓')} FINAL TEST (unseen): "
f"Sharpe={best_r['sharpe']:+.3f} "
f"Ret={best_r['ret']:+.2f}% "
f"MaxDD={best_r['max_dd']:.2f}% "
f"Trades={best_r['trades']:,} "
f"WR={best_r['win_rate']:.1f}% "
f"PF={best_r['pf']:.3f}"
)
# Save raw OOS predictions for later analysis.
np.savez(
ROOT / "results" / "oos_preds.npz",
xgb=oos_xgb[oos_mask],
lgb=oos_lgb[oos_mask],
lstm=oos_lstm[oos_mask],
ens=ens_score[oos_mask],
labels=labels[oos_mask],
close=c[oos_mask],
)
print(f" {GREEN('✓')} Saved OOS predictions to results/oos_preds.npz")
"""
if old not in text:
raise RuntimeError("Старый блок не найден — файл не изменён.")
path.write_text(text.replace(old, new, 1))
print("✓ Патч успешно применён.")