Загрузка данных


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("✓ Патч успешно применён.")