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


from pathlib import Path

path = Path("/content/AIQuant/aiquant/models/ensemble_pipeline.py")
text = path.read_text()

start_marker = "best_r = None"
end_marker = "    # ── Save best params ─────────────────────────────────────────────────────"

start = text.index(start_marker)
end = text.index(end_marker, start)

new_block = '''# ── Honest threshold selection ─────────────────────────────────────────────
    # Split OOS chronologically: 70% validation / 30% final unseen test.
    oos_idx = np.where(oos_mask)[0]

    if len(oos_idx) < 1000:
        print(f"  {RED('✗')} Not enough OOS samples for validation/test split.")
        return {}

    split_pos = int(len(oos_idx) * 0.70)
    val_idx = oos_idx[:split_pos]
    test_idx = oos_idx[split_pos:]

    print(
        f"  OOS split: {len(val_idx):,} validation bars / "
        f"{len(test_idx):,} final-test bars"
    )

    def evaluate_threshold(lt, st, idx):
        """Evaluate thresholds only on the supplied contiguous index range."""
        scores = ens_score[idx]
        prices = c[idx]

        sig = np.zeros(len(idx), dtype=np.int8)
        sig[scores > lt] = 1
        sig[scores < st] = -1

        changes = np.where(np.diff(sig, prepend=0) != 0)[0]

        if len(changes) < 20:
            return None

        cap = float(capital)
        equity = np.full(len(idx), cap, dtype=np.float64)

        wins = losses = 0
        gw = gl = 0.0

        for j in range(len(changes) - 1):
            eb = changes[j]
            xb = changes[j + 1]
            d = int(sig[eb])

            if d == 0:
                equity[eb:xb] = cap
                continue

            ep = prices[eb]
            xp = prices[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(idx) / 1440.0

        if n_days > 2:
            daily_log_returns = np.array([
                np.diff(
                    np.log(
                        equity[i * 1440:min((i + 1) * 1440, len(equity))]
                        + 1e-10
                    )
                ).sum()
                for i in range(int(np.ceil(n_days)))
                if len(equity[i * 1440:min((i + 1) * 1440, len(equity))]) > 1
            ])

            if len(daily_log_returns) > 1 and daily_log_returns.std() > 0:
                sharpe = float(
                    daily_log_returns.mean()
                    / daily_log_returns.std()
                    * np.sqrt(365)
                )
            else:
                sharpe = 0.0
        else:
            sharpe = 0.0

        trades = wins + losses
        win_rate = wins / trades * 100 if trades > 0 else 0.0
        calmar = total_ret / abs(max_dd) if abs(max_dd) > 0.01 else 0.0
        profit_factor = gw / (gl + 1e-10)

        return dict(
            sharpe=round(sharpe, 3),
            ret=round(total_ret, 2),
            max_dd=round(max_dd, 2),
            calmar=round(calmar, 3),
            pf=round(profit_factor, 3),
            trades=trades,
            win_rate=round(win_rate, 1),
            final=round(cap, 2),
            equity=equity,
            signals=sig,
            long_thresh=lt,
            short_thresh=st,
        )

    # Select thresholds ONLY on validation data.
    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]:

            r = evaluate_threshold(long_thresh, short_thresh, val_idx)

            if r is None:
                continue

            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
                )

                print(
                    f"  {col('↑')} Validation Sharpe={col(f"{r['sharpe']:+.3f}")}  "
                    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}]"
                )

    if best_r is None:
        print(f"  {RED('✗')} No valid threshold found.")
        return {}

    best_long = best_r['long_thresh']
    best_short = best_r['short_thresh']

    print()
    print("  ── FINAL TEST (unseen) ─────────────────────────────────────────────")
    final_r = evaluate_threshold(best_long, best_short, test_idx)

    if final_r is None:
        print(f"  {RED('✗')} Final test produced fewer than 20 trades.")
        return {}

    print(
        f"  Final Test: Ret={final_r['ret']:+.2f}%  "
        f"Sharpe={final_r['sharpe']:+.3f}  "
        f"MaxDD={final_r['max_dd']:.2f}%  "
        f"PF={final_r['pf']:.3f}  "
        f"Trades={final_r['trades']:,}  "
        f"WR={final_r['win_rate']:.1f}%"
    )

    # Use final-test result for reporting, while thresholds were chosen
    # exclusively on validation data.
    best_r = final_r

    # Save raw OOS predictions for later analysis.
    np.savez(
        RESULTS_DIR / 'oos_preds.npz',
        oos_idx=oos_idx,
        ens_score=ens_score[oos_idx],
        val_idx=val_idx,
        test_idx=test_idx,
        best_long=np.array(best_long),
        best_short=np.array(best_short),
    )

'''

text = text[:start] + new_block + text[end:]

path.write_text(text)

print("✅ Threshold block replaced")