Монте-Карло бутстрэп: бэктест үшін сенімділік интервалдарын небәрі 10 жол кодпен қалай алуға болады
📄 Бұл мақала зерттеу мақаласына айналды. Мұнда сипатталған бутстрэп сенімділік интервалдары серияттық тәуелділік жағдайында бақыланатын coverage тестінен өтті (iid vs trade-level vs block bootstrap, шындығы дәл белгілі 6000 эксперимент). Мақаланы онлайн (интерактивті нұсқа + PDF) bootstrap.marketmaker.cc сайтынан оқыңыз, код пен деректер github.com/suenot/bootstrap-coverage мекенжайында.
Сіз стратегияны бэктест арқылы жүргіздіңіз. PnL +42%, Sharpe 1.8, MaxDD -12% алдыңыз. Нәтижелер тамаша көрінеді. Сіз ботты өндіріске іске қосасыз, бір айдан кейін дродаун -28%-ға жеткенін және PnL нөлге қарай ұмтылып бара жатқанын байқайсыз.
Не дұрыс болмады? Бұл баг та емес, "нарық өзгерді" де емес. Мәселе — сіз шешімді бір ғана сан негізінде қабылдадыңыз, яғни бір нүктелік бағалау негізінде. Сіз стратегия +42% көрсеткенін білдіңіз, бірақ осы санға қаншалықты сенуге болатынын білмедіңіз.
Бір нүктелік бағалаулардың мәселесі
Жалғыз деректер нүктесі (сол жақта) жаңылыстыратын сурет береді, ал толық үлестірім (оң жақта) мүмкін нәтижелердің шынайы ауқымын ашады.
Тарихи деректер бойынша бэктест — нарық оқиғаларының белгілі бір тізбегі арқылы жасалған бір ғана жүгіріс. Нәтиже мәмілелердің ретіне байланысты: бірдей мәмілелермен жасалған бірдей стратегия, бірақ басқа ретпен, мүлдем басқа максималды дродаун көрсете алады.
491 мәміле бар делік. Әрбір мәміле — белгілі бір кірістілік үлестірімі бар кездейсоқ оқиға. Тарихи бэктест бұл процестің тек бір ғана жүзеге асуын көрсетеді. Бұл сүйекті бір рет лақтырып, оның әрдайым төртке түсетінін тұжырымдаумен бірдей.
Бізге шын мәнінде керегі:
- Нүктелік бағалау емес, керісінше интервал: "95% ықтималдықпен, түпкілікті PnL X пен Y аралығында болады"
- Жалғыз максималды дродаун емес, керісінше үлестірім: "ең нашар сценарийлердің 5%-ында дродаун Z%-дан асады"
- Орташа мән емес, керісінше құйрықтар: сәттілік сізге қарамаса не болады?
Дәл осы үшін Монте-Карло бутстрэп бар.
Монте-Карло бутстрэп деген не
Бутстрэп бастапқы деректер жиынтығынан мәмілелерді қайта қоя отырып ресэмплдеу арқылы мыңдаған балама эквити траекторияларын жасайды.
Бутстрэп — 1979 жылы Брэдли Эфрон ұсынған ресэмплдеу әдісі. Идея қарапайым әрі әдемі: деректер сынамасы болса, біз бастапқы жиынтықтан қайта қоя отырып элементтерді кездейсоқ таңдау арқылы мыңдаған "жаңа" сынама жасай аламыз.
Бэктест контекстінде бұл былай жұмыс істейді:
- Сізде әрбір мәміле үшін кірістіліктер массиві бар — мысалы, 491 мән
- Сіз осы массивтен қайта қоя отырып кездейсоқ түрде 491 мән таңдайсыз — кейбір мәмілелер екі рет кездеседі, кейбіреулері мүлдем кездеспейді
- Осы жаңа сынамадан эквити қисығын саласыз
- Мұны 10 000 рет қайталайсыз
- Бір сан емес, түпкілікті метрикалардың үлестірімін аласыз
Әрбір итерация — "балама сценарий": мәмілелердің реті мен жиынтығы сәл өзгеше болса, не болуы мүмкін еді.
10 жолда іске асыру
Толық жұмыс істейтін код мынау:
import numpy as np
def max_drawdown(equity_curve):
"""Calculate the maximum drawdown of an equity curve."""
peak = np.maximum.accumulate(equity_curve)
drawdown = (equity_curve - peak) / peak
return drawdown.min()
trade_returns = [...] # 491 values, e.g. [0.012, -0.005, 0.008, ...]
n_simulations = 10000
results = []
for _ in range(n_simulations):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
results.append({
"final_pnl": equity[-1] - 1,
"max_dd": max_drawdown(equity),
"sharpe": np.mean(sampled) / np.std(sampled) * np.sqrt(252)
})
Орындалу уақыты: қарапайым ноутбукте ~2 секунд. Стратегияңыздың 10 000 балама тарихы.
Сенімділік интервалдарын алу
Стратегияның негізгі метрикалары үшін сенімділік интервалдары: PnL, MaxDD және Sharpe Ratio, 5-ші, 50-ші және 95-ші процентиль жолақтарымен көрсетілген.
Енді бізде бір сан емес, үлестірім бар. Одан пайдалы ақпаратты былай алуға болады:
import pandas as pd
df = pd.DataFrame(results)
pnl_5 = np.percentile(df['final_pnl'], 5)
pnl_50 = np.percentile(df['final_pnl'], 50)
pnl_95 = np.percentile(df['final_pnl'], 95)
dd_5 = np.percentile(df['max_dd'], 5) # 5th — worst case
dd_50 = np.percentile(df['max_dd'], 50)
dd_95 = np.percentile(df['max_dd'], 95) # 95th — best case
print(f"PnL: {pnl_5:.1%} | {pnl_50:.1%} | {pnl_95:.1%}")
print(f"MaxDD: {dd_5:.1%} | {dd_50:.1%} | {dd_95:.1%}")
print(f"Sharpe: {np.percentile(df['sharpe'], 5):.2f} — {np.percentile(df['sharpe'], 95):.2f}")
Нақты стратегия үшін мысал шығысы:
| Metric | 5th percentile (worst) | Median | 95th percentile (best) |
|---|---|---|---|
| PnL | +18.3% | +41.7% | +72.1% |
| MaxDD | -23.4% | -12.8% | -5.1% |
| Sharpe | 1.12 | 1.76 | 2.41 |
Енді айырмашылық анық көрінеді:
- Бэктест PnL +42% көрсетті — бірақ ең нашар 5% сценарийде PnL небәрі +18.3%
- Бэктест MaxDD -12% көрсетті — бірақ ең нашар 5% сценарийде дродаун -23.4%
- Sharpe 1.8 — бірақ төменгі шек 1.12
5-ші процентиль — сіздің "реалистік ең нашар жағдайыңыз". Егер стратегия 5-ші процентильде пайда әкелмей қалса, оны өндіріске шығару қауіпті.
Визуализация: Fan Chart
Монте-Карло бутстрэп табиғи түрде fan chart ретінде визуализацияланады — эквити қисықтарының желпуіші:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
ax = axes[0]
for i in range(min(500, n_simulations)):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
ax.plot(equity, alpha=0.02, color='#4FC3F7')
all_equities = []
for _ in range(n_simulations):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
all_equities.append(equity)
all_equities = np.array(all_equities)
p5 = np.percentile(all_equities, 5, axis=0)
p50 = np.percentile(all_equities, 50, axis=0)
p95 = np.percentile(all_equities, 95, axis=0)
ax.fill_between(range(len(p5)), p5, p95, alpha=0.3, color='#7C4DFF', label='90% CI')
ax.plot(p50, color='#E040FB', linewidth=2, label='Median')
ax.set_title('Monte Carlo Bootstrap: Equity Curves')
ax.legend()
ax = axes[1]
ax.hist(df['final_pnl'] * 100, bins=80, color='#4FC3F7', alpha=0.7, edgecolor='#1A237E')
ax.axvline(pnl_5 * 100, color='#FF5252', linestyle='--', label=f'5th: {pnl_5:.1%}')
ax.axvline(pnl_50 * 100, color='#E040FB', linestyle='--', label=f'Median: {pnl_50:.1%}')
ax.axvline(pnl_95 * 100, color='#69F0AE', linestyle='--', label=f'95th: {pnl_95:.1%}')
ax.set_title('Distribution of Final PnL')
ax.set_xlabel('PnL, %')
ax.legend()
plt.tight_layout()
plt.savefig('monte_carlo_fan_chart.png', dpi=150)
plt.show()
Fan chart мүмкін нәтижелердің таралуы туралы интуитивті түсінік береді. Тар желпуіш стратегияның тұрақты екенін білдіреді. Кең желпуіш нәтиженің мәмілелер ретіндегі "сәттілікке" қатты тәуелді екенін білдіреді.
Fan chart (сол жақта) мүмкін эквити траекторияларының таралуын көрсетеді, ал гистограмма (оң жақта) белгіленген сенімділік интервалдарымен (5%, 50%, 95%) түпкілікті кірістіліктердің тығыздық үлестірімін көрсетеді.
Тереңдетілген талдау: банкроттық ықтималдығы
Банкроттық ықтималдығының визуализациясы: аман қалған эквити жолдары (циан) жоғары қарай иіледі, ал банкрот болған жолдар (қызыл) нөлдік-эквити шегінен төмен түседі.
Бутстрэп маңызды сұраққа жауап беруге мүмкіндік береді: стратегияның капиталдың X%-ын жоғалту ықтималдығы қандай?
ruin_threshold = -0.20
prob_ruin = (df['max_dd'] < ruin_threshold).mean()
print(f"P(MaxDD < -20%) = {prob_ruin:.1%}")
prob_loss = (df['final_pnl'] < 0).mean()
print(f"P(PnL < 0) = {prob_loss:.1%}")
worst_5pct = df['final_pnl'].quantile(0.05)
cvar = df[df['final_pnl'] <= worst_5pct]['final_pnl'].mean()
print(f"CVaR(5%) = {cvar:.1%}")
Бұл метрикаларды бір ғана бэктест жүгірісінен алу мүмкін емес. Дегенмен стратегияны іске қосу туралы шешім қабылдау үшін олар өте маңызды.
Терең дродаундардың неге математикалық тұрғыда қауіпті екені және кірістілік асимметриясы қалай жұмыс істейтіні туралы толығырақ Loss-Profit Asymmetry мақаласынан оқыңыз.
Классикалық бутстрэп қашан жұмыс істемейді
Әдістің білуге тұрарлық шектеулері бар.
Кірістіліктердің автокорреляциясы
Классикалық бутстрэп мәмілелердің тәуелсіз болатынын болжайды. Шындығында бұл жиі олай емес — стратегияда жеңіс пен жеңілістің серияларымен жүруі мүмкін. Автокорреляция айтарлықтай болса, блокты бутстрэпті қолданыңыз:
def block_bootstrap(returns, block_size=10, n_simulations=10000):
"""Bootstrap preserving local dependency structure."""
n = len(returns)
results = []
for _ in range(n_simulations):
starts = np.random.randint(0, n - block_size + 1, size=n // block_size + 1)
sampled = np.concatenate([returns[s:s+block_size] for s in starts])[:n]
equity = np.cumprod(1 + sampled)
results.append({
"final_pnl": equity[-1] - 1,
"max_dd": max_drawdown(equity),
})
return pd.DataFrame(results)
Блокты бутстрэп қатарлас мәмілелер арасындағы жергілікті тәуелділікті сақтап, MaxDD үшін неғұрлым шынайы сенімділік интервалдарын береді.
Блокты бутстрэп мәмілелер тізбегін блоктарға бөліп, оларды қайта қоя отырып ресэмплдеу арқылы блоктар ішіндегі автокорреляцияны сақтайды.
Нарықтың стационарлы еместігі
Бутстрэп бастапқы мәміле үлестірімімен жұмыс істейді. Нарық құрылымдық түрде өзгерсе (мысалы, құбылмалылық төмендесе немесе өтімділік өзгерсе), тарихи мәмілелер репрезентативті болмауы мүмкін. Мұны ескеру үшін:
- Жылжымалы терезені қолданыңыз: тек соңғы N мәміле бойынша бутстрэп жасаңыз
- Соңғы мәмілелерге көбірек салмақ беріңіз: өлшемделген бутстрэп
- Деректерді нарықтық режимдер бойынша бөліп, бөлек-бөлек бутстрэп жасаңыз
Мәмілелер саны аз болғанда
Бутстрэп n > 30 мәміле кезінде сенімді. Егер сізде 10 мәміле болса — қанша ресэмплдеу жасасаңыз да көмектеспейді. 491 мәміле — керемет сынама; нәтижелерге сенуге болады.
Бэктест тұрақтылығын бағалау тәсілдерін салыстыру
| Method | What it provides | Complexity | Time | When to use |
|---|---|---|---|---|
| Single backtest | One point estimate | Minimal | Seconds | Never as a final result |
| Walk-forward | Out-of-sample metrics | Medium | Minutes | To check for overfitting |
| Monte Carlo bootstrap | Confidence intervals | Minimal | ~2 sec | Always before production |
| Monte Carlo path | New price paths | High | Minutes-hours | For stress testing |
| Cross-validation | Average metrics across folds | Medium | Minutes | For parameter tuning |
Монте-Карло бутстрэп — минималды уақытта тәуекелдердің толық бейнесін беретін жалғыз әдіс.
Чек-лист: нәтижелерді интерпретациялау
Монте-Карло бутстрэп нәтижелерін былай интерпретациялауды ұсынамыз:
Өндіріске шығарыңыз, егер:
- 5-ші процентильдегі PnL оң болса
- 5-ші процентильдегі MaxDD тәуекелге деген талғамыңызға қолайлы болса
- банкроттық ықтималдығы < 1% болса
- 5-ші процентильдегі Sharpe > 0.5 болса
Қосымша жұмыс керек, егер:
- 5-ші процентильдегі PnL нөлге жақын болса
- 5-ші процентильдегі MaxDD 50-ші процентильдегіден айтарлықтай нашар болса
- fan chart-тың таралуы кең болса — стратегия тұрақсыз
Шығармаңыз, егер:
- 5-ші процентильдегі PnL теріс болса
- банкроттық ықтималдығы > 5% болса
- Sharpe сенімділік интервалы 0-ді қамтыса
marketmaker.cc-тегі біздің тәжірибе
marketmaker.cc сайтында біз өзіміздің бэктест қозғалтқышын жасаймыз, және Монте-Карло бутстрэп біздің құбырымыздың ажырамас бөлігі. Әрбір стратегия live трейдингке бекітілмес бұрын бутстрэптен автоматты түрде өтеді.
Біз бутстрэпті тікелей бэктест қозғалтқышына кірістірдік: жүгірістен кейін сіз тек түпкілікті PnL ғана емес, сенімділік интервалдары, fan chart, банкроттық ықтималдығы және блокты бутстрэп пен стандартты бутстрэптің салыстырмасы бар толық есеп аласыз. Бұған қосымша 2-3 секунд кетеді — нақты тәуекелдерді түсіну үшін бұл шамалы баға.
Біздің тәжірибемізде: бір нүктелік бағалау бойынша тартымды көрінетін стратегиялардың шамамен 30%-ы Монте-Карло бутстрэптен кейін сүзгіден өтпей қалады. Олардың 5-ші процентильдегі PnL теріс болып шығады немесе MaxDD қолайсыз болып табылады. Бутстрэпсіз бұл стратегиялар өндіріске шығып, өте жоғары ықтималдықпен шығынға әкелер еді.
Қорытынды
Монте-Карло бутстрэп — небәрі ~10 жол код және ~2 секунд есептеу. Ол бэктесттен алынған бір санды сенімділік интервалдары бар толық үлестірімге айналдырады. Бұл кез келген сандық талдау құралдарының ішіндегі ROI-і ең жоғарысы шығар:
- Минималды шығын: 30 минутта іске асыру
- Максималды пайда: стратегияның нақты тәуекелдерін түсіну
- Тәуелділік жоқ: тек NumPy
Егер сіз бутстрэпті әлі қолданбасаңыз — оны бүгін-ақ құбырыңызға қосыңыз. Бэктест нәтижелеріңізге қаншалықты сенуге болатынын білудің жалғыз жолы осы.
References
- Efron, B. — Bootstrap Methods: Another Look at the Jackknife (1979)
- Davison, A.C., Hinkley, D.V. — Bootstrap Methods and their Application (Cambridge)
- Aronson, D.R. — Evidence-Based Technical Analysis: Monte Carlo permutation
- QuantStart — Monte Carlo Simulation for Backtest Analysis
- Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
- Kevin Davey — Building Winning Algorithmic Trading Systems: Monte Carlo Analysis
- NumPy — numpy.random.choice
Citation
@software{soloviov2026montecarlobootstrap,
author = {Soloviov, Eugen},
title = {Monte Carlo Bootstrap: How to Get Confidence Intervals for a Backtest in 10 Lines of Code},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/monte-carlo-bootstrap-backtest},
version = {0.1.0},
description = {Why a single-point estimate from a backtest is a dangerous illusion. How Monte Carlo bootstrap in 2 seconds of computation gives you a 95\% confidence interval for PnL and MaxDD, and why this is a mandatory step before launching a strategy in production.}
}
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.