Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Cara Membuat Interval Kepercayaan Backtest

Satu equity curve hanyalah satu sampel. Pelajari cara membangun dan membaca interval kepercayaan bootstrap untuk Sharpe backtest yang dapat mencakup nol.

Interval kepercayaan backtest menjawab satu pertanyaan: seberapa besar bagian dari equity curve yang berasal dari strategi, dan seberapa besar yang berasal dari potongan periode historis tertentu saat strategi tersebut dijalankan. Bootstrapping backtest membentuk interval itu dengan melakukan resampling terhadap rangkaian return berkali-kali, menghitung ulang statistik pada setiap sampel ulang, lalu membaca persentil dari hasilnya. Sharpe sebesar 1.4 yang diukur selama satu tahun berdasarkan observasi harian memiliki interval 95 persen yang cukup lebar hingga mencakup nol. Panel-panel di bawah mengukur penyebabnya.

Mengapa satu equity curve hanyalah satu sampel

Backtest memberikan satu angka untuk setiap statistik: satu Sharpe ratio, satu annualized return, satu worst drawdown, dan satu hit rate. Tidak satu pun mencerminkan nilai sebenarnya dari strategi. Masing-masing merupakan estimasi yang dibangun dari periode trading yang terbatas. Periode lain dengan panjang yang sama dapat menghasilkan angka yang berbeda.

Panel di bawah menghapus strategi dari analisis sepenuhnya. Panel ini mengukur posisi paling sederhana: memegang SPY selama satu tahun kalender pada satu waktu, berdasarkan price return dari penutupan ke penutupan.

QuerySatu posisi, satu tahun pada satu waktu: Sharpe tahunan SPY berdasarkan tahun kalender
SQL tepat di balik setiap angka
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2011-12-01'
      AND date <  '2026-01-01'
)
SELECT
    toString(toYear(date))                            AS year,
    count()                                           AS obs_count,
    round(avg(ret) * 252 * 100, 2)                    AS ann_return_pct,
    round(stddevSamp(ret) * sqrt(252) * 100, 2)       AS ann_vol_pct,
    round(avg(ret) / stddevSamp(ret) * sqrt(252), 2)  AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
  AND ret IS NOT NULL
GROUP BY year
ORDER BY year
Run this yourself

Setiap baris mencakup sekitar 250 sesi, mendekati standard trading year. Tidak ada perubahan pada posisi selama periode-periode tersebut. Pada 2012, annualized Sharpe tercatat 1.06. Pada 2025, angkanya 0.88, dengan 14 tahun ditampilkan dalam grafik. Memegang broad index selama satu tahun dapat mengubah angka utama lebih besar daripada yang biasanya dianggap pembaca sebagai noise. Backtest strategi dengan panjang yang sama setidaknya mewarisi variasi sebesar itu. Konvensi annualization yang digunakan pada kolom tersebut dijelaskan dalam panduan Sharpe ratio kami. Mekanisme period return dijelaskan dalam cara mengukur monthly return.

Seberapa lebar kisaran di sekitar Sharpe backtest?

Standard error dari estimasi Sharpe secara kasar turun sebanding dengan akar kuadrat jumlah observasi. Namun, alih-alih mengandalkan rumus tersebut, ukur spread-nya secara langsung. Bagi data sesi selama dua puluh tahun menjadi window yang tidak tumpang tindih dengan panjang tetap. Hitung Sharpe tahunan di dalam setiap window, lalu lihat seberapa jauh hasil-hasil tersebut tersebar.

QueryDispersi Sharpe terukur pada jendela SPY yang tidak tumpang tindih, 2006 hingga 2025
SQL tepat di balik setiap angka
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2005-12-01'
      AND date <  '2026-01-01'
),
indexed AS
(
    SELECT
        ret,
        row_number() OVER (ORDER BY date) AS i
    FROM daily
    WHERE date >= '2006-01-01'
      AND ret IS NOT NULL
),
blocks AS
(
    SELECT
        w,
        intDiv(i, w)                                            AS blk,
        count()                                                 AS n,
        avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252)        AS sharpe
    FROM indexed
    CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
    GROUP BY w, blk
    HAVING n = w
)
SELECT
    concat(toString(w), ' sessions')                          AS horizon,
    count()                                                   AS block_count,
    round(quantileDeterministic(0.05)(sharpe, blk), 2)        AS sharpe_p05,
    round(quantileDeterministic(0.50)(sharpe, blk), 2)        AS sharpe_p50,
    round(quantileDeterministic(0.95)(sharpe, blk), 2)        AS sharpe_p95,
    round(quantileDeterministic(0.95)(sharpe, blk)
        - quantileDeterministic(0.05)(sharpe, blk), 2)        AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY w
Run this yourself

Pada 21 sessions per window, persentil ke-5 hingga ke-95 dari Sharpe yang terukur berada pada kisaran -3.37 hingga 6.97. Ini berarti spread sebesar 10.34 poin Sharpe di antara 238 window. Jika setiap window diperpanjang menjadi 504 sessions, spread turun menjadi 1.67 poin, tetapi kini diukur hanya berdasarkan 8 window. Dua hal berubah sekaligus. Estimasi menjadi lebih presisi seiring bertambahnya ukuran sampel, sementara jumlah sampel independen yang dapat disediakan oleh data historis menyusut. Ketegangan antara kedua hal tersebut adalah inti persoalannya.

Cara melakukan bootstrap confidence interval untuk backtest

Prosedurnya cukup singkat untuk dituliskan secara lengkap.

  1. Mulai dengan deret return terealisasi strategi, satu angka untuk setiap periode, sebanyak N angka.
  2. Ambil N return dari daftar tersebut secara acak, dengan pengembalian. Sebagian dapat muncul dua kali, sementara sebagian lainnya tidak muncul sama sekali.
  3. Hitung kembali statistik pada sampel ulang tersebut.
  4. Ulangi beberapa ribu kali dan simpan setiap hasilnya.
  5. Urutkan nilai yang tersimpan, lalu ambil persentil ke-2,5 dan ke-97,5 untuk interval 95 persen.

Tetapkan seed pada random number generator sebelum langkah 2, lalu catat seed tersebut di samping interval yang dipublikasikan. Bootstrap merupakan estimasi Monte Carlo. Dua proses tanpa seed akan berbeda pada digit terakhir. Reviewer yang tidak dapat menjalankan ulang proses untuk memperoleh interval Anda tidak memiliki cara untuk memeriksanya. Ini merupakan disiplin yang sama seperti yang dijelaskan dalam penyiapan backtest yang dapat direproduksi.

Mean return dan Sharpe dapat melalui langkah 2 dengan baik karena keduanya membaca daftar return sebagai suatu kumpulan. Maximum drawdown berbeda. Drawdown membaca urutan path. Sampel ulang yang mengubah urutan return dapat menghasilkan drawdown terburuk yang tidak pernah dihasilkan oleh urutan perdagangan aktual mana pun. Bootstrap untuk statistik ini tetap layak dilakukan, asalkan hasilnya diberi label secara tepat: distribusi drawdown di seluruh histori yang diacak ulang, bukan forecast untuk drawdown berikutnya. Definisinya tersedia di maximum drawdown.

Mengapa bootstrap IID keliru untuk return pasar

Langkah 2 mengasumsikan bahwa setiap return saling independen dan memiliki distribusi yang identik—disebut asumsi IID. Return harian melanggar asumsi ini dengan cara yang mengubah lebar interval. Return bertanda hanya memiliki sedikit persistensi dari satu hari ke hari berikutnya. Namun, besarannya mengelompok: pergerakan besar cenderung berdekatan dengan pergerakan besar lainnya, sementara hari-hari tenang muncul secara beruntun.

QueryAutokorelasi lag satu: return bertanda dibandingkan dengan return absolut, 2016 hingga 2025
SQL tepat di balik setiap angka
WITH daily AS
(
    SELECT
        ticker,
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
      AND date >= '2015-11-01'
      AND date <  '2026-01-01'
),
lagged AS
(
    SELECT
        ticker,
        date,
        ret,
        lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
    FROM daily
    WHERE ret IS NOT NULL
      AND abs(ret) < 0.35
)
SELECT
    ticker,
    count()                                AS obs_count,
    round(corr(ret, ret_prev), 3)          AS return_autocorr,
    round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
  AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESC
Run this yourself

Untuk SPY, autokorelasi lag-satu atas return harian absolut tercatat sebesar 0.366 sepanjang 2514 sesi, dibandingkan dengan -0.133 untuk return bertanda pada hari yang sama. Bandingkan kedua batang pada nama mana pun di grafik. Pengacakan seri return menghancurkan pengelompokan tersebut. Bootstrap IID yang dijalankan pada data ini kemudian menghasilkan interval yang lebih sempit daripada yang didukung sampel. Kesalahan ini mengarah ke arah yang paling tidak membantu: membuat kinerja strategi terlihat lebih baik.

Bootstrap blok bergerak dan pemilihan panjang blok

Perbaikannya adalah mengambil sampel ulang dari blok yang berurutan, bukan dari return tunggal. Pilih panjang blok L, ambil blok yang terdiri atas L return berurutan secara acak dengan pengembalian, lalu rangkai hingga panjang seri sintetis mencapai N. Dependensi di dalam blok tetap utuh karena return tersebut mempertahankan urutan aslinya. Hanya penggabungan antarblok yang bersifat artifisial.

Panjang blok menyeimbangkan dua jenis kesalahan, dan tidak ada pengaturan yang dapat menghindari keduanya. Blok pendek berperilaku seperti bootstrap IID dan membuat interval menjadi terlalu sempit. Ini adalah bias. Blok panjang mempertahankan lebih banyak dependensi, tetapi menyisakan lebih sedikit blok berbeda untuk diambil. Akibatnya, setiap resample mengulang potongan besar dari histori yang sama dan interval itu sendiri menjadi lebih bising. Ini adalah variance. Aturan praktis yang dipublikasikan biasanya menetapkan panjang blok sebanding dengan N pangkat satu per tiga. Gunakan aturan tersebut sebagai titik awal.

Diagnostik yang lebih murah adalah memeriksa bagaimana variance berubah menurut horizon. Dalam kondisi independen, variance dari jumlah return selama k hari sama dengan k kali variance satu hari. Rasio keduanya adalah 1.

QueryRasio varians berdasarkan panjang blok: apakah varians SPY berskala seperti pengambilan sampel independen?
SQL tepat di balik setiap angka
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2005-12-01'
      AND date <  '2026-01-01'
),
indexed AS
(
    SELECT
        ret,
        row_number() OVER (ORDER BY date) AS i
    FROM daily
    WHERE date >= '2006-01-01'
      AND ret IS NOT NULL
),
base AS
(
    SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
    SELECT
        k,
        intDiv(i, k)  AS blk,
        count()       AS n,
        sum(ret)      AS block_ret
    FROM indexed
    CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
    GROUP BY k, blk
    HAVING n = k
)
SELECT
    concat(toString(k), ' sessions')                          AS block_length,
    count()                                                   AS block_count,
    round(varSamp(block_ret) / (k * any(var_1d)), 3)          AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY k
Run this yourself

Pada 2 sessions, rasio tersebut tercatat sebesar 0.844; pada 63 sessions, tercatat sebesar 0.584, berdasarkan 78 blok yang tidak saling tumpang tindih. Nilai yang mendekati 1 berarti jumlah tersebut meningkat sesuai dengan pola independent draws pada horizon itu. Nilai yang menyimpang dari 1 menunjukkan horizon ketika dependensi masih berpengaruh. Itulah rentang yang harus dicakup oleh panjang blok. Pilih blok terpendek yang mencakup rentang tersebut, lalu periksa jumlah blok yang tersisa.

Resampling berbasis bucket dan jumlah observasi di samping setiap interval

Resampling dalam bucket, berdasarkan rezim volatilitas atau bulan kalender, mempertahankan kondisi yang membuat pertanyaan tersebut menarik. Jika klaimnya adalah bahwa suatu strategi memperoleh Sharpe-nya dalam rezim volatilitas tinggi, interval yang dibentuk hanya dari hari-hari dengan volatilitas tinggi adalah interval yang menguji klaim tersebut. Konsekuensinya adalah beban aritmetika. Membagi 250 observasi ke dalam empat bucket menyisakan sekitar 60 observasi per bucket, dan bootstrap dengan 60 observasi menghasilkan interval yang lebarnya sekitar dua kali lipat dibandingkan versi seluruh sampel.

Karena itu, selalu laporkan jumlah observasi dalam bucket di samping setiap interval. Kolom block_count pada panel di atas memperlihatkan kebiasaan tersebut: persentil ke-5 yang dihitung dari sembilan jendela adalah objek yang berbeda dari persentil ke-5 yang dihitung dari dua ratus jendela, meskipun keduanya ditampilkan dengan dua angka desimal.

Hal-hal yang tidak dapat diperbaiki bootstrap

Bootstrap mengukur satu hal, yaitu noise sampling dalam statistik yang dihitung dari data yang tersedia. Bootstrap tidak menjawab apakah data tersebut pernah dapat diperoleh secara nyata.

Backtest yang terkontaminasi bias look-ahead menghasilkan rangkaian return yang tidak pernah dapat diperdagangkan. Bootstrap atas rangkaian itu hanya mengembalikan rentang yang sempit dan tampak meyakinkan di sekitar sesuatu yang fiktif. Universe yang disusun dari anggota indeks saat ini mengandung bias survivorship, dan setiap resample dari universe tersebut mewarisi bias itu. Kesenjangan ketiga adalah efek seleksi: jalankan 200 varian, pilih yang terbaik, lalu interval bootstrap-nya hanya menggambarkan noise sampling dari satu varian tersebut dan mengabaikan 199 hasil lain yang digunakan dalam proses pemilihannya. Rentang yang jujur tetap berguna. Namun, rentang tersebut bukan pengganti data out-of-sample.

Catatan data dan metode
  • Return adalah return harga close-to-close dari daily bar. Dividen tidak disertakan, sehingga level setiap return dan angka Sharpe menjadi lebih rendah sekitar sebesar dividend yield. Dispersi yang dibahas dalam tulisan ini hampir tidak terpengaruh.
  • Window dan block tidak saling tumpang tindih, sehingga setiap statistik yang diukur menggunakan bagian riwayat yang terpisah. Window yang saling tumpang tindih akan memperbesar jumlah sampel yang tampak.
  • Kuantil menggunakan estimator deterministik. Karena itu, saat panel dijalankan ulang, persentil yang dihasilkan tetap sama dan bukan estimasi baru.
  • Panel autokorelasi menggunakan enam saham berkapitalisasi besar yang tidak mengalami stock split selama window, serta menghapus setiap hari dengan pergerakan di atas 35 persen. Dengan demikian, artefak penyesuaian harga tidak masuk ke dalam korelasi.

FAQ

Apa yang dijelaskan interval kepercayaan bootstrap tentang backtest?

Interval ini memberikan rentang nilai yang secara masuk akal dapat diambil statistik tersebut jika proses yang sama disampling kembali untuk jumlah periode yang sama. Interval 95 persen yang mencakup nol berarti sampel terlalu pendek untuk membedakan strategi dari kondisi tanpa edge sama sekali.

Berapa banyak bootstrap resample yang diperlukan?

Untuk interval 95 persen, beberapa ribu resample biasanya sudah stabil, dan 10.000 merupakan default yang umum dengan biaya komputasi rendah. Kuantil ekor yang lebih ekstrem memerlukan lebih banyak resample: persentil ke-1 dari 1.000 hasil pengundian dibaca dari sekitar sepuluh nilai.

Berapa panjang blok yang sebaiknya digunakan dalam moving block bootstrap?

Tidak ada panjang yang benar secara universal. Aturan praktis mengaitkannya dengan ukuran sampel yang dipangkatkan satu per tiga, dan pemeriksaan praktisnya adalah horizon ketika variance tidak lagi meningkat secara linear. Hal ini diukur oleh panel variance ratio di atas. Cantumkan panjang yang digunakan bersama interval tersebut.

Apakah maximum drawdown dapat dihitung dengan bootstrap?

Ya, dengan catatan terkait urutan. Drawdown bergantung pada urutan return, sehingga resample yang dibuat dengan mengacak urutan akan menghasilkan drawdown dari riwayat yang telah disusun ulang. Block bootstrap mempertahankan rangkaian pendek tetap utuh dan merupakan alat yang lebih baik untuk statistik berbasis lintasan.

Apakah bootstrap mengoreksi overfitting?

Tidak. Bootstrap mengukur sampling noise dalam satu seri return. Look-ahead bias dan selection effect akibat menguji banyak varian berada di luar hal yang dapat dideteksinya.


Setiap panel di sini memuat SQL yang menghasilkannya. Ubah ticker atau panjang window, lalu jalankan sendiri di terminal Strasmore.

#backtesting#bootstrap#statistics#confidence intervals#sharpe ratio