Bootstrapping ng Confidence Bands sa Backtest
Isang equity curve ay isang sample lamang. Alamin kung paano bumuo at magbasa ng bootstrap confidence interval para sa backtest Sharpe na maaaring sumaklaw sa zero.
Sinasagot ng confidence interval ng backtest ang isang tanong: gaano kalaki sa equity curve ang dulot ng strategy, at gaano kalaki ang dulot ng partikular na bahagi ng kasaysayang pinatakbuhan nito. Binubuo ang interval na ito sa pamamagitan ng bootstrapping ng backtest: paulit-ulit na nire-resample ang return series, muling kinakalkula ang statistic sa bawat resample, at tinitingnan ang mga percentile ng mga resultang lumalabas. Ang Sharpe na 1.4 na sinukat batay sa daily observations sa loob ng isang taon ay may 95 percent interval na sapat ang lawak upang isama ang zero. Sinusukat ng mga panel sa ibaba kung bakit.
Bakit isang sample lamang ang isang equity curve
Nagbibigay ang backtest ng tig-isang numero para sa bawat statistic: isang Sharpe ratio, isang annualized return, isang pinakamalaking drawdown, at isang hit rate. Wala sa mga ito ang tunay na halaga ng strategy. Tantiya lamang ang bawat isa batay sa limitadong serye ng trading days. Kung ibang stretch na kapareho ang haba ang ginamit, iba rin ang lalabas na resulta.
Inaalis ng panel sa ibaba ang strategy sa pagsusuri. Sinusukat nito ang pinakasimpleng posisyon: ang paghawak sa SPY, isang calendar year sa bawat pagkakataon, batay sa close-to-close price returns.
Ang eksaktong SQL sa likod ng bawat numero
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 yearSaklaw ng bawat row ang humigit-kumulang 250 sessions, na malapit sa karaniwang trading year. Walang nagbago sa posisyon sa alinman sa mga ito. Sa 2012, ang annualized Sharpe ay 1.06; sa 2025 naman, ito ay 0.88, gamit ang 14 taon na ipinakita sa chart. Sa isang taon ng paghawak sa isang broad index, maaaring magbago ang headline number nang higit sa karaniwang itinuturing ng karamihan sa readers na noise. Namamana ng backtest ng strategy na kapareho ang haba ang hindi bababa sa ganoong kalaking variation. Nakasaad sa aming gabay sa Sharpe ratio ang annualization convention na ginamit sa column, habang ipinaliliwanag sa kung paano sinusukat ang monthly returns ang mechanics ng period return.
Gaano kalawak ang saklaw sa paligid ng Sharpe ng isang backtest?
Karaniwang bumababa ang standard error ng isang Sharpe estimate kasabay ng square root ng bilang ng observations. Sa halip na umasa sa formula na iyon, direktang sukatin ang spread. Hatiin ang dalawampung taon ng trading sessions sa magkakahiwalay na window na may takdang haba, kalkulahin ang annualized Sharpe sa loob ng bawat window, at tingnan kung gaano kalayo sa isa’t isa ang mga resultang iyon.
Ang eksaktong SQL sa likod ng bawat numero
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 wSa 21 sessions bawat window, mula -3.37 hanggang 6.97 ang 5th hanggang 95th percentile ng measured Sharpe, o spread na 10.34 Sharpe points sa 238 window. Kapag pinahaba ang bawat window sa 504 sessions, bumababa ang spread sa 1.67 points, na sinusukat naman sa 8 window lamang. Dalawang bagay ang sabay na nagbabago. Mas nagiging precise ang estimate kapag mas malaki ang sample size, ngunit bumabagsak ang bilang ng independent samples na maibibigay ng historical data. Iyan ang buong paksa.
Paano bumuo ng bootstrap confidence interval para sa backtest
Maikli lang ang pamamaraang ito at maaaring isulat nang buo.
- Magsimula sa aktuwal na return series ng strategy, na may isang figure bawat period, at N ang kabuuan.
- Random na kumuha ng N return mula sa listahang iyon, na may replacement. May mga lalabas nang dalawang beses at may mga hindi lalabas.
- Kalkulahin muli ang statistic gamit ang resample.
- Ulitin ito nang ilang libong beses at itala ang bawat value.
- Ayusin ang mga naitalang value at kunin ang 2.5th at 97.5th percentile para sa 95 percent interval.
I-set ang seed ng random number generator bago ang step 2, at itala ang seed sa tabi ng inilathalang interval. Ang bootstrap ay isang Monte Carlo estimate: magkakaiba ang huling digits ng dalawang run na walang seed, at walang paraan ang reviewer na suriin ang interval kung hindi niya ito kayang patakbuhin muli. Ito ang kaparehong disiplina na inilalarawan sa reproducible na setup ng backtest.
Malinis na dumaraan sa step 2 ang mean return at Sharpe dahil pareho nilang ginagamit ang return list bilang isang set. Hindi ganoon ang maximum drawdown. Binabasa ng drawdown ang path ayon sa pagkakasunod-sunod. Ang isang resample na nagbabago sa pagkakasunod-sunod ng returns ay maaaring magbunga ng worst drawdown na hindi lumitaw sa alinmang pagkakasunod-sunod ng aktuwal na trade sequence. Gayunman, sulit pa ring i-bootstrap ito, basta malinaw na nakalabel ang output kung ano talaga ito: ang distribution ng drawdown sa mga reshuffled history, hindi forecast ng susunod na drawdown. Nasa maximum drawdown ang depinisyon.
Bakit mali ang IID bootstrap para sa market returns
Ipinapalagay ng Step 2 na independent at identically distributed ang bawat return—ang IID assumption. Nilalabag ito ng daily returns sa paraang nagbabago sa lapad ng interval. Kaunti lamang ang one-day memory ng signed returns. Ngunit nagkakakumpol ang magnitude ng mga ito: magkakatabi ang malalaking galaw, at dumarating nang magkakasunod ang mga tahimik na araw.
Ang eksaktong SQL sa likod ng bawat numero
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 DESCPara sa SPY, ang lag-one autocorrelation ng absolute daily returns ay sinukat sa 0.366 sa loob ng 2514 sessions, kumpara sa -0.133 para sa signed returns sa parehong mga araw. Ikumpara ang dalawang bar sa anumang pangalan sa chart. Sinisira ng pag-shuffle sa return series ang clustering na ito, kaya ang IID bootstrap na isinagawa sa data na ito ay nag-uulat ng mas makitid na interval kaysa sa sinusuportahan ng sample. Ang error ay napupunta sa hindi kanais-nais na direksiyon: pinapaganda nito ang ipinapakitang performance ng strategy.
Moving block bootstrap at pagpili ng haba ng block
Ang pag-aayos ay ang pag-resample ng magkakadugtong na block sa halip na magkakahiwalay na return. Pumili ng block length na L, pagkatapos ay random na bumunot ng mga block na may L magkakasunod na return, na may replacement, at pagdugtung-dugtungin ang mga ito hanggang maging N ang haba ng synthetic series. Nananatiling buo ang dependence sa loob ng isang block: nasa orihinal na pagkakasunod-sunod pa rin ang mga return. Ang mga dugtungan lamang sa pagitan ng mga block ang artipisyal.
Ipinagpapalit ng block length ang dalawang uri ng error, at walang setting na makakaiwas sa pareho. Ang maiikling block ay kumikilos na parang IID bootstrap at minamaliit ang interval; bias ito. Mas maraming dependence ang napapanatili ng mahahabang block, ngunit mas kakaunti ang distinct block na mapagpipilian. Dahil dito, inuulit ng bawat resample ang malalaking bahagi ng parehong history, kaya nagiging maingay ang interval mismo; variance ito. Ang mga karaniwang rule of thumb na inilathala ay nagse-scale ng haba ayon sa one-third power ng N. Gamitin ang mga ito bilang panimulang punto.
Mas murang diagnostic ang pagsusuri kung paano nagse-scale ang variance ayon sa horizon. Sa ilalim ng independence, ang variance ng k-day return sum ay katumbas ng k times ng one-day variance, at ang ratio ng dalawa ay nasa 1.
Ang eksaktong SQL sa likod ng bawat numero
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 kSa 2 sessions, ang nasukat na ratio ay 0.844; sa 63 sessions, ito ay 0.584, na kinuwenta gamit ang 78 non-overlapping blocks. Ang mga value na malapit sa 1 ay nangangahulugang nagse-scale ang sum sa paraang inaasahan sa independent draws sa horizon na iyon. Ang mga value na malayo sa 1 ay tumutukoy sa mga horizon kung saan may epekto pa rin ang dependence. Iyon ang range na kailangang saklawin ng block length. Piliin ang pinakamaikling block na sumasaklaw dito, pagkatapos ay tingnan kung ilan na lamang ang natitirang block.
Pagre-resample sa loob ng mga bucket at ang bilang sa tabi ng bawat interval
Ang pagre-resample sa loob ng mga bucket, ayon sa volatility regime o calendar month, ay nagpapanatili sa conditioning na naging dahilan kung bakit mahalaga ang tanong. Kung ang claim ay kumikita ang isang strategy ng Sharpe nito sa high-volatility regimes, ang interval na binuo gamit lamang ang mga araw na may mataas na volatility ang sumusubok sa claim na iyon. Ang kapalit ay nasa arithmetic. Kapag hinati ang 250 observations sa apat na bucket, humigit-kumulang 60 ang natitira sa bawat bucket. Ang bootstrap na may 60 observations ay nagbibigay ng interval na halos dalawang beses na mas malawak kaysa sa bersiyon na gumagamit ng buong sample.
Kaya iulat ang bilang ng observations sa bucket sa tabi ng bawat interval, sa bawat pagkakataon. Ang column na block_count sa mga panel sa itaas ay nagpapakita ng gawaing ito: iba ang 5th percentile na kinuha mula sa siyam na window kumpara sa kinuha mula sa dalawang daang window, kahit pareho silang lumabas na may dalawang decimal place.
Mga hindi kayang ayusin ng bootstrap
Sinusukat ng bootstrap ang isang bagay: ang sampling noise sa isang statistic na kinuwenta mula sa datos na mayroon ka. Hindi nito sinasabi kung maaari bang makuha ang datos na iyon sa aktuwal na trading.
Ang backtest na may look-ahead bias ay lumilikha ng return series na hindi kailanman maaaring i-trade. Kapag nag-bootstrap ka ng ganitong serye, makakakuha ka ng makitid at mukhang kumpiyansang band sa paligid ng isang kathang-isip. Ang universe na binuo mula sa mga kasalukuyang miyembro ng index ay may survivorship bias, at mamanahin ito ng bawat resample ng universe na iyon. May ikatlo ring puwang: ang selection effect. Magpatakbo ng 200 variant, piliin ang pinakamaganda, at ilalarawan ng bootstrap interval nito ang sampling noise ng napiling variant habang binabalewala ang 199 draw na ginamit sa pagpili rito. Kapaki-pakinabang ang mga tapat na band. Hindi nito mapapalitan ang out-of-sample data.
Mga tala at method sa datos
- Ang returns ay close-to-close price returns mula sa daily bars. Hindi kasama ang mga dibidendo, kaya minamaliit nito ang level ng bawat return at Sharpe figure nang humigit-kumulang katumbas ng dividend yield. Halos hindi naaapektuhan ang dispersion na tinatalakay sa post na ito.
- Hindi nagsasapawan ang mga window at block, kaya bawat measured statistic ay gumagamit ng hiwalay na slice ng history. Ang overlapping windows ay magpapalaki sa mukhang bilang ng samples.
- Gumagamit ang quantiles ng deterministic estimator, kaya kapag muling pinatakbo ang isang panel, pareho ang percentile na ibinabalik nito sa halip na panibagong approximation.
- Gumagamit ang autocorrelation panel ng anim na malalaking pangalan na walang share split sa loob ng window. Inaalis din nito ang anumang araw na may galaw na lampas 35 percent upang hindi mapasama sa correlation ang mga artifact mula sa price adjustment.
FAQ
Ano ang sinasabi ng bootstrap confidence interval tungkol sa isang backtest?
Ipinapakita nito ang saklaw ng mga value na maaaring makatwirang makuha ng statistic kung muling isi-sample ang parehong proseso sa parehong bilang ng mga period. Kapag naglalaman ng zero ang 95 percent interval, masyadong maikli ang sample para matukoy kung may edge ang strategy kumpara sa kawalan ng anumang edge.
Ilang bootstrap resample ang sapat?
Para sa 95 percent interval, karaniwang stable na ang ilang libong resample, at karaniwang default ang 10,000 dahil maliit lang ang gastos nito. Mas marami ang kailangan para sa mas malalalim na tail quantile: ang 1st percentile ng 1,000 draw ay tinutukoy batay sa humigit-kumulang sampung value.
Anong block length ang dapat gamitin ng moving block bootstrap?
Walang iisang haba na palaging tama. Karaniwang itinatakda ang block length batay sa sample size na itinaas sa one-third power. Isang praktikal na pagsusuri ang tukuyin ang horizon kung kailan hindi na linear ang pag-scale ng variance; ito ang sinusukat ng variance ratio panel sa itaas. I-publish ang ginamit na block length kasabay ng interval.
Maaari bang gumamit ng bootstrap para sa maximum drawdown?
Oo, ngunit may caveat tungkol sa pagkakasunod-sunod. Nakadepende ang drawdown sa sequence ng returns, kaya ang resample na ginawa sa pamamagitan ng pag-shuffle ay nag-uulat ng drawdown ng muling inayos na history. Pinananatili ng block bootstrap ang maiikling run, kaya mas angkop ito para sa path statistics.
Nagtatama ba ang bootstrap ng overfitting?
Hindi. Sinusukat nito ang sampling noise sa loob ng isang return series. Hindi nito nakikita ang look-ahead bias at ang selection effect mula sa pagsubok ng maraming variant.
May kalakip na SQL ang bawat panel dito na gumawa sa mga resultang ipinapakita. Palitan ang ticker o ang haba ng window, at patakbuhin ito mismo sa Strasmore terminal.