Backtest کے لیے confidence bands کیسے بنائیں
ایک equity curve صرف ایک sample ہے۔ Backtest Sharpe پر bootstrap confidence interval اکثر اتنا وسیع ہوتا ہے کہ صفر بھی شامل ہو جاتا ہے۔ اسے بنانا اور پڑھنا سیکھیں۔
Backtest کا confidence interval ایک بنیادی سوال کا جواب دیتا ہے: equity curve میں کتنا حصہ strategy کا ہے، اور کتنا اس مخصوص تاریخی دور کا جس پر strategy چلائی گئی۔ Backtest کو bootstrap کرنے کے لیے return series سے بار بار resample لیے جاتے ہیں، ہر resample پر statistic دوبارہ calculate کیا جاتا ہے، اور حاصل ہونے والی قدروں کے percentiles دیکھے جاتے ہیں۔ ایک سال کی daily observations پر ناپا گیا Sharpe 1.4، 95 percent interval رکھتا ہے جو صفر کو بھی شامل کرنے کے لیے کافی وسیع ہے۔ ذیل کے panels اس کی وجوہات کو quantify کرتے ہیں۔
ایک equity curve ایک ہی sample کیوں ہوتی ہے
Backtest ہر statistic کے لیے ایک ہی number دیتا ہے: ایک Sharpe ratio، ایک annualized return، ایک worst drawdown، اور ایک hit rate۔ ان میں سے کوئی بھی strategy کی حقیقی قدر نہیں ہوتا۔ ہر number محدود trading days پر مشتمل ایک مخصوص مدت سے اخذ کیا گیا estimate ہوتا ہے۔ اسی لمبائی کی کوئی دوسری مدت مختلف نتیجہ دے سکتی ہے۔
ذیل کا panel strategy کو مکمل طور پر بحث سے خارج کر دیتا ہے۔ یہ ممکنہ حد تک سادہ position کی پیمائش کرتا ہے: SPY کو ایک ایک calendar year تک hold کرنا، close-to-close price returns کی بنیاد پر۔
ہر عدد کے پیچھے موجود درست SQL
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ہر row میں تقریباً 250 sessions شامل ہیں، جو standard trading year کے قریب ہے۔ ان میں سے کسی مدت کے دوران position میں کوئی تبدیلی نہیں کی گئی۔ 2012 میں annualized Sharpe 1.06 ناپا گیا؛ 2025 میں یہ 0.88 رہا، جبکہ chart میں 14 سال شامل ہیں۔ broad index کو ایک سال تک hold کرنے سے headline number میں اتنی تبدیلی آ سکتی ہے جسے اکثر قارئین noise سے کہیں زیادہ سمجھیں گے۔ اسی مدت کا strategy backtest کم از کم اتنی ہی غیر یقینی inherited کرتا ہے۔ column کے پیچھے موجود annualization convention ہماری Sharpe ratio guide میں، جبکہ period-return mechanics ماہانہ returns کی پیمائش کیسے کی جاتی ہے میں بیان کیے گئے ہیں۔
Backtest Sharpe کے گرد band کتنی چوڑی ہے؟
Sharpe estimate کی standard error مشاہدات کی تعداد کے square root کے ساتھ تقریباً کم ہوتی ہے۔ اس فارمولے پر انحصار کرنے کے بجائے spread کو براہِ راست ناپیں۔ بیس سال کے trading sessions کو مقررہ لمبائی کی non-overlapping windows میں تقسیم کریں، ہر window کے اندر annualized Sharpe calculate کریں، اور دیکھیں کہ یہ اعداد ایک دوسرے سے کتنے فاصلے پر ہیں۔
ہر عدد کے پیچھے موجود درست SQL
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فی window 21 sessions پر، measured Sharpe کا 5th سے 95th percentile -3.37 سے 6.97 تک ہے۔ یہ 238 windows میں 10.34 Sharpe points کا spread بنتا ہے۔ ہر window کو 504 sessions تک بڑھانے پر spread کم ہو کر 1.67 points رہ جاتا ہے، لیکن اب یہ صرف 8 windows پر measured ہے۔ بیک وقت دو چیزیں تبدیل ہوتی ہیں۔ Sample size بڑھنے سے estimate زیادہ precise ہو جاتا ہے، جبکہ history سے دستیاب independent samples کی تعداد تیزی سے کم ہو جاتی ہے۔ یہی tension اس پورے موضوع کا مرکز ہے۔
بیک ٹیسٹ کے confidence interval کو bootstrap کرنے کا طریقہ
یہ طریقۂ کار اتنا مختصر ہے کہ اسے مکمل طور پر بیان کیا جا سکتا ہے۔
- Strategy کی realized return series سے آغاز کریں۔ ہر period کے لیے ایک figure ہو، اور کل N figures ہوں۔
- اس فہرست سے N returns بے ترتیب طور پر منتخب کریں اور replacement کے ساتھ واپس شامل کرتے جائیں۔ کچھ returns دو مرتبہ آ سکتے ہیں، جبکہ کچھ ایک مرتبہ بھی نہیں آئیں گے۔
- اس resample پر statistic دوبارہ calculate کریں۔
- یہ عمل کئی ہزار مرتبہ دہرائیں اور ہر value محفوظ کرتے جائیں۔
- محفوظ شدہ values کو sort کریں، پھر 95 percent interval کے لیے 2.5th اور 97.5th percentiles حاصل کریں۔
Step 2 سے پہلے random number generator کو seed کریں، اور published interval کے ساتھ seed بھی درج کریں۔ Bootstrap ایک Monte Carlo estimate ہوتا ہے۔ دو unseeded runs کے آخری digits میں اختلاف آ سکتا ہے۔ اگر reviewer آپ کے interval کو دوبارہ run نہ کر سکے تو اس کی جانچ کا کوئی طریقہ نہیں رہتا۔ یہی discipline قابلِ تکرار backtest setup میں بیان کی گئی ہے۔
Mean return اور Sharpe، step 2 کے بعد درست طور پر apply ہو جاتے ہیں، کیونکہ دونوں return list کو ایک set کے طور پر پڑھتے ہیں۔ Maximum drawdown ایسا نہیں کرتا۔ Drawdown returns کے ترتیب وار path کو دیکھتا ہے۔ ایسا resample جو returns کی ترتیب بدل دے، ایک ایسا worst drawdown پیدا کر سکتا ہے جو حقیقی trade sequence کی کسی ترتیب میں سامنے نہیں آیا تھا۔ اس کے باوجود اسے bootstrap کرنا مفید ہے، بشرطیکہ output کو واضح طور پر اسی حیثیت سے label کیا جائے: یہ reshuffled histories میں drawdown کی distribution ہے، اگلے drawdown کی forecast نہیں۔ اس کی تعریف maximum drawdown میں موجود ہے۔
مارکیٹ returns کے لیے IID bootstrap کیوں غلط ہے
Step 2 میں فرض کیا جاتا ہے کہ ہر return آزاد اور یکساں طور پر distributed ہے۔ اسے IID assumption کہا جاتا ہے۔ Daily returns اس assumption کی خلاف ورزی اس انداز میں کرتے ہیں کہ interval کی چوڑائی بدل جاتی ہے۔ Signed returns میں ایک دن کی memory بہت کمزور ہوتی ہے۔ لیکن ان کی magnitudes cluster کرتی ہیں: بڑی movements کے ساتھ بڑی movements آتی ہیں، جبکہ پرسکون دن مسلسل کئی دنوں تک جاری رہتے ہیں۔
ہر عدد کے پیچھے موجود درست SQL
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 DESCSPY کے لیے 2514 sessions کے دوران absolute daily returns کی lag-one autocorrelation 0.366 رہی، جبکہ انہی دنوں کے signed returns کے لیے یہ شرح -0.133 تھی۔ چارٹ میں کسی بھی نام پر دونوں bars کا موازنہ کریں۔ Return series کو shuffle کرنے سے یہ clustering ختم ہو جاتی ہے، اور اس data پر چلایا گیا IID bootstrap sample کی تائید سے کم چوڑائی والا interval ظاہر کرتا ہے۔ یہ غلطی کم مددگار سمت میں ہوتی ہے: اس سے strategy حقیقت سے زیادہ پرکشش دکھائی دیتی ہے۔
Moving block bootstrap اور block length کا انتخاب
اس کا حل یہ ہے کہ single returns کے بجائے مسلسل blocks کو دوبارہ sample کیا جائے۔ ایک block length L منتخب کریں، پھر replacement کے ساتھ L مسلسل returns پر مشتمل blocks بے ترتیب طور پر منتخب کریں، اور انہیں آخر تک جوڑتے جائیں، یہاں تک کہ synthetic series کی لمبائی N ہو جائے۔ ایک block کے اندر dependence اپنی اصل حالت میں برقرار رہتی ہے، کیونکہ returns اسی ترتیب سے رہتے ہیں۔ صرف blocks کے درمیان جوڑ مصنوعی ہوتے ہیں۔
Block length دو غلطیوں کے درمیان توازن قائم کرتی ہے، اور کوئی setting دونوں سے مکمل طور پر نہیں بچتی۔ چھوٹے blocks IID bootstrap کی طرح برتاؤ کرتے ہیں اور interval کو کم ظاہر کرتے ہیں؛ یہ bias ہے۔ بڑے blocks زیادہ dependence برقرار رکھتے ہیں، لیکن منتخب کرنے کے لیے distinct blocks کم رہ جاتے ہیں۔ اس لیے ہر resample ایک ہی history کے بڑے حصے بار بار دہراتا ہے، اور interval خود noisy ہو جاتا ہے؛ یہ variance ہے۔ شائع شدہ rules of thumb length کو N کی one-third power کے ساتھ بڑھاتے ہیں۔ انہیں ابتدائی points سمجھیں۔
ایک کم لاگت diagnostic یہ ہے کہ جانچا جائے کہ horizon کے ساتھ variance کس طرح scale کرتا ہے۔ Independence کی صورت میں k-day return sum کا variance، one-day variance کے k گنا کے برابر ہوتا ہے، اور دونوں کا ratio 1 رہتا ہے۔
ہر عدد کے پیچھے موجود درست SQL
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 k2 sessions پر ratio 0.844 ریکارڈ ہوا؛ 63 sessions پر یہ 0.584 تھا، جسے 78 non-overlapping blocks پر calculate کیا گیا۔ 1 کے قریب values کا مطلب ہے کہ اس horizon پر sum اسی طرح scale ہو رہا ہے جیسے independent draws کرتے۔ 1 سے دور values ان horizons کی نشاندہی کرتی ہیں جہاں dependence اب بھی اثرانداز ہے، اور یہی وہ range ہے جسے block length کو cover کرنا چاہیے۔ سب سے مختصر block منتخب کریں جو اس range کو clear کر دے، پھر دیکھیں کہ block count کم ہو کر کتنا رہ گیا ہے۔
Buckets کے اندر resampling اور ہر interval کے ساتھ observations کی تعداد
Buckets کے اندر، volatility regime کے لحاظ سے یا calendar month کے مطابق resampling کرنے سے وہ conditioning برقرار رہتی ہے جس نے اصل سوال کو اہم بنایا تھا۔ اگر دعویٰ یہ ہے کہ کوئی strategy اپنی Sharpe high-volatility regimes میں حاصل کرتی ہے، تو صرف high-volatility دنوں سے بنائی گئی interval ہی اس دعوے کو test کرتی ہے۔ اس کی قیمت arithmetic ہے۔ 250 observations کو چار buckets میں تقسیم کرنے سے ہر bucket میں تقریباً 60 observations رہ جاتی ہیں، اور 60 observations کا bootstrap، full-sample version کے مقابلے میں تقریباً دو گنا وسیع interval دیتا ہے۔
اس لیے ہر بار ہر interval کے ساتھ bucket count بھی report کریں۔ اوپر دیے گئے panels میں block_count column اسی practice کو نمایاں کرتا ہے: نو windows سے حاصل کیا گیا 5th percentile، دو سو windows سے حاصل کیے گئے 5th percentile سے مختلف object ہے، چاہے دونوں two decimals تک ایک جیسے نظر آئیں۔
Bootstrap کیا ٹھیک نہیں کر سکتا
Bootstrap صرف ایک چیز کی پیمائش کرتا ہے: دستیاب data سے calculate کیے گئے statistic میں sampling noise۔ یہ اس بات پر خاموش رہتا ہے کہ وہ data کبھی حقیقتاً قابلِ حصول بھی تھا یا نہیں۔
look-ahead bias سے آلودہ backtest ایسی return series پیدا کرتا ہے جس پر کبھی trade نہیں کیا جا سکتا تھا۔ اس series کا bootstrap ایک خیالی نتیجے کے گرد بظاہر تنگ اور پُراعتماد band واپس دیتا ہے۔ آج کے index members سے تیار کیا گیا universe survivorship bias رکھتا ہے، اور اس universe کا ہر resample بھی اسی bias کو آگے منتقل کرتا ہے۔ تیسرا خلا selection effect ہے: 200 variants چلائیں، بہترین variant رکھیں، تو اس کا bootstrap interval صرف اسی ایک variant کے sampling noise کو بیان کرتا ہے، جبکہ اسے منتخب کرنے کے عمل میں شامل دیگر 199 نتائج کو نظرانداز کر دیتا ہے۔ دیانت دار bands مفید ہوتے ہیں۔ لیکن یہ out-of-sample data کا متبادل نہیں۔
Data notes and method
- Returns روزانہ bars سے حاصل کردہ close-to-close price returns ہیں۔ Dividends شامل نہیں کیے گئے، اس لیے ہر return اور Sharpe figure کی سطح تقریباً dividend yield کے برابر کم ظاہر ہوتی ہے۔ تاہم اس post میں زیرِ بحث dispersion پر اس کا اثر تقریباً نہیں پڑتا۔
- Windows اور blocks non-overlapping ہیں، اس لیے ہر measured statistic تاریخ کے الگ حصے سے حاصل کیا جاتا ہے۔ Overlapping windows samples کی بظاہر تعداد بڑھا دیتے۔
- Quantiles کے لیے deterministic estimator استعمال کیا گیا ہے، اس لیے panel دوبارہ چلانے پر نیا تخمینہ نہیں بلکہ وہی percentile واپس آتا ہے۔
- Autocorrelation panel میں چھ بڑے names استعمال کیے گئے ہیں۔ window کے دوران جن میں share split نہیں ہوا، اور 35 فیصد سے زیادہ move والے ہر دن کو خارج کر دیا گیا ہے۔ اس سے correlation میں price-adjustment کے آثار شامل نہیں ہوتے۔
اکثر پوچھے جانے والے سوالات
Bootstrap confidence interval آپ کو backtest کے بارے میں کیا بتاتا ہے؟
یہ ان اقدار کی range بتاتا ہے جو اسی عمل کو اتنے ہی periods پر دوبارہ sample کرنے کی صورت میں statistic کے لیے قابلِ قبول طور پر سامنے آ سکتی ہیں۔ اگر 95 percent interval میں zero شامل ہو تو sample اتنا مختصر ہے کہ strategy کو کسی بھی edge کی عدم موجودگی سے الگ نہیں کیا جا سکتا۔
Bootstrap resamples کی تعداد کتنی ہونی چاہیے؟
95 percent interval کے لیے چند ہزار resamples عموماً کافی stable ہوتے ہیں، جبکہ 10,000 ایک عام default ہے اور اس کی لاگت بھی کم ہے۔ زیادہ گہری tail quantiles کے لیے زیادہ resamples درکار ہوتے ہیں: 1,000 draws کے 1st percentile کو تقریباً دس values کی بنیاد پر پڑھا جاتا ہے۔
Moving block bootstrap میں block length کیا ہونی چاہیے؟
اس کے لیے کوئی universally correct length نہیں ہے۔ عمومی اصول کے تحت اسے sample size کے one-third power کے ساتھ scale کیا جاتا ہے۔ ایک عملی جانچ یہ ہے کہ اس horizon کو دیکھا جائے جہاں variance کی scaling linear نہیں رہتی؛ اوپر موجود variance ratio panel یہی پیمائش کرتا ہے۔ استعمال کی گئی length کو interval کے ساتھ ضرور publish کریں۔
کیا maximum drawdown پر bootstrap کیا جا سکتا ہے؟
ہاں، لیکن ordering کو مدنظر رکھنا ضروری ہے۔ Drawdown returns کی sequence پر منحصر ہوتا ہے، اس لیے shuffling سے بنایا گیا resample reordered history کا drawdown رپورٹ کرتا ہے۔ Block bootstrap مختصر runs کو برقرار رکھتا ہے، اس لیے path statistics کے لیے یہ بہتر tool ہے۔
کیا bootstrap overfitting کو درست کرتا ہے؟
نہیں۔ یہ ایک return series کے اندر sampling noise کی پیمائش کرتا ہے۔ Look-ahead bias اور کئی variants کو test کرنے سے پیدا ہونے والا selection effect، دونوں اس کے دائرۂ نظر سے باہر رہتے ہیں۔
یہاں موجود ہر panel اسے تیار کرنے والے SQL کے ساتھ دیا گیا ہے۔ ticker یا window length تبدیل کریں اور Strasmore terminal پر خود run کریں۔