Координаталык түшүү менен Байес оптималдаштыруусу: кимиси жакшыраак параметрлерди табат
Бул "Иллюзиясыз бэктесттер" сериясынын бешинчи макаласы. Мурунку макалаларда биз чыгым-пайда асимметриясын, Монте-Карло бутстрэбин, фандинг мөлчөрлөрүнүн таасирин жана тезирээк бэктесттер үчүн Parquet кэшин карадык. Эми стратегиянын оптималдуу параметрлерин табуу процесси жөнүндө сүйлөшөлү — бул интуиция эң көп жолу ишке ашпай калган милдет.
Сизде 12 параметри бар стратегия бар. Ар бир параметр ~9 маани кабыл алат. Сиз чектелген drawdown менен PnL-ды максималдаштырган айкалышты табууну каалайсыз. Муну кантип жасайсыз?
Эгер жообуңуз "мен бардык айкалыштарды кайталайм" болсо — сизде көйгөй бар. Эгер жообуңуз "мен бир учурда бир параметрди өзгөртөм" болсо — сизде башка көйгөй бар. Бул макала ар бир мамиленин артында кандай көйгөйлөр жашырынып жатканы жана аларды кантип чечүү керектиги жөнүндө.
Толук издөө эмне үчүн мүмкүн эмес

Өлчөмдүүлүктүн наалаты
Толук издөө (grid search) ар бир параметр үчүн маанилердин ар бир айкалышын текшерет. 9 маани бар эки параметр үчүн бул жүгүртүү — толугу менен ишке ашырылуучу. Үч үчүн: — чыдамдуу.
Бирок 12 параметри бар чыныгы стратегия үчүн:
Эки жүз сексен эки миллиард жүгүртүү. Бир бэктест 1 секунда алса да (бул мурунтан эле оптимисттик), толук издөө мынча алат:
Бул экспоненциалдык өсүш: ар бир жаңы параметр издөө мейкиндигин 9-га көбөйтөт. 13-параметрди кошуңуз — жана 9 000 жылдын ордуна сизге 80 000 керек болот.
import math
def grid_search_cost(n_params: int, values_per_param: int, seconds_per_trial: float) -> dict:
"""Estimate the cost of exhaustive search."""
total_trials = values_per_param ** n_params
total_seconds = total_trials * seconds_per_trial
return {
"total_trials": total_trials,
"total_hours": total_seconds / 3600,
"total_years": total_seconds / (3600 * 24 * 365),
}
cost = grid_search_cost(12, 9, 1.0)
print(f"Trials: {cost['total_trials']:,.0f}") # 282,429,536,481
print(f"Years: {cost['total_years']:,.0f}") # 8,950
Алдын ала эсептөө менен деле
Parquet кэши жөнүндөгү макалада биз таймфреймдерди жана индикаторлорду алдын ала эсептөө бир бэктестти ~1 секундага чейин тездетээрин көрсөттүк. Бирок жүгүртүү башына 0,1 секунда болсо да, 12 параметрдин толук издөөсү 895 жыл талап кылат. Алдын ала эсептөө жардам берет, бирок экспоненциалдык өсүштүн негизги көйгөйүн чечпейт.
Бизге параметр мейкиндигин толук издөөгө караганда акылдуураак изилдеген ыкмалар керек.
Координаталык түшүү жана OAT: тез бирок сокур

Бир идеянын эки варианты
Эки байланышкан мамиле бар — экөө тең бир учурда бир параметрди оптималдаштырат, бирок өтүүлөрдүн санында айырмаланат:
OAT (One-at-a-Time) сыдырып өтүү — бардык параметрлер боюнча бир өтүү. Биринчи параметрдин маанилерин кайталап чыгуу, эң жакшысын бекитүү, экинчисине өтүү — жана ушул сыяктуу. Бир жолу. Тез жана арзан.
Coordinate Descent — көп өтүүлүү. Акыркы параметрди оптималдаштыргандан кийин биринчисине кайра кайтып, оптимум өзгөргөнбү же жокпу текшерүү керек (себеби контекст өзгөрдү — башка параметр маанилери эми башкача). Раундар жыйналганга чейин кайталанат. Кымбатыраак, бирок тагыраак — ар бир раунд чечимди жакшыртышы мүмкүн.
Иш жүзүндө бэктесттер үчүн OAT көбүрөөк колдонулат: 12 параметр боюнча бир өтүү — 96 жүгүртүү. 3-5 раунддуу Coordinate Descent — 300-500 жүгүртүү, бул мурунтан эле Optuna менен салыштырууга болот, бирок анын артыкчылыктарысыз.
Ар биринде ~8 маани бар 12 параметр үчүн:
Grid search үчүн менен салыштырыңыз. OAT сызыктуу: ордуна . Бул анын негизги артыкчылыгы да, негизги көйгөйү да.
def oat_sweep(
param_grid: dict[str, list],
run_backtest_fn,
initial_params: dict,
metric: str = "effective_score",
) -> dict:
"""
OAT sweep: single pass, optimizing one parameter at a time.
param_grid: {"htf_entry_sell": [0.0, 0.005, ..., 0.05], ...}
initial_params: starting values for all parameters
metric: metric to optimize (effective_score recommended —
PnL per active time extrapolated to a year)
"""
best_params = initial_params.copy()
best_score = run_backtest_fn(**best_params)[metric]
for param_name, values in param_grid.items():
param_best_val = best_params[param_name]
param_best_score = best_score
for val in values:
candidate = best_params.copy()
candidate[param_name] = val
result = run_backtest_fn(**candidate)
score = result[metric]
if score > param_best_score:
param_best_score = score
param_best_val = val
best_params[param_name] = param_best_val
best_score = param_best_score
print(f"{param_name}: best={param_best_val}, score={param_best_score:.4f}")
return best_params
Оптималдаштыруу үчүн кайсы метриканы тандоо керек? Таза PnL же PnL@MaxLev ордуна, жылга экстраполяцияланган effective score — активдүү убакыт боюнча PnL колдонуу сунушталат. Бул метрика позициядагы убакытты эсепке алат жана ар кандай соода жыштыгы бар стратегияларды туура салыштырууга мүмкүндүк берет.
Сокур аймак: параметрлердин өз ара аракеттенүүсү
OAT ар бир параметрдин таасири аддитивдүү деп болжолдойт — башкача айтканда, бир параметрдин оптималдуу мааниси башкалардын маанилерине көз каранды эмес. Бул божомол кээ бир параметрлер үчүн туура келет, бирок байланышкандар үчүн бузулат.
Аддитивдүү жана байланышкан параметрлер
Оптималдаштыруудан мурун — параметрлерди классификациялоо пайдалуу:
Аддитивдүү (көз карандысыз) — биринин оптималдуу мааниси экинчисине көз каранды эмес. Аларды бирден арзан оптималдаштырууга болот:
htf_entry_sellжанаhtf_entry_buy— бир эле таймфреймде ар кандай багыттар (сатуу/сатып алуу) үчүн кирүү чектери. Сатуу чеги шорт сигналдарын чыпкалайт, сатып алуу чеги — лонгдарды. Алар кайчылашпаган мамилелер жыйындысында иштейт.tp_targetжанаbe_trigger— take-profit жана breakeven, эгер алар карама-каршы чыгуу шарттарын түзбөсө.
Байланышкан (интерактивдүү) — биринин оптималдуу мааниси экинчисине көз каранды. Биргелешкен оптималдаштыруу керек:
htf_entry_sellжанаmtf_entry_sell— ар кандай таймфреймдердеги бирдей багыт (сатуу) үчүн чектер. HTF кайсы сигналдар MTF-ке жетерин аныктайт, ал эми MTF чеги чыпкалоо натыйжалуулугун аныктайт. MTF өзгөргөндө HTF оптимуму жылышат.ltf_entry_sell,mtf_entry_sell,htf_entry_sell— бир багыт үчүн бүткүл чектер чынжыры.partial_fracжанаtp_target— жарым-жартылай жабуунун өлчөмү TP деңгээлине көз каранды.
Практикалык мамиле: алгач аддитивдүү параметрлерди OAT аркылуу арзан оптималдаштырыңыз. Андан кийин байланышкан топторду Optuna аркылуу оптималдаштырыңыз. Бул бюджетти кыскартат: Optuna-да 12 параметрдин ордуна, биз болгону 6-8 байланышканды жиберебиз, калгандары мурунтан эле бекитилген.
Мисал: OAT өз ара аракеттенүүнү кантип өткөрүп жиберет
Эки байланышкан чекти карап көрөлү:
htf_entry_sell— жогорку таймфреймдеги чек (сатуу багыты)mtf_entry_sell— орто таймфреймдеги чек (сатуу багыты)
OAT mtf_entry_sell = 0.01 (баштапкы маани) бекитет жана htf_entry_sell аркылуу кайталайт. Эң жакшы маанини табат: htf_entry_sell = 0.02. Аны бекитет жана кийинки параметрге өтөт — эч качан кайтпайт.
OAT эмнени өткөрүп жиберди:
htf_entry_sell |
mtf_entry_sell |
PnL |
|---|---|---|
| 0.02 | 0.01 | +42% |
| 0.02 | 0.02 | +38% |
| 0.03 | 0.02 | +51% |
| 0.03 | 0.01 | +35% |
(0.03, 0.02) айкалышы +51% PnL берет, бирок OAT аны эч качан карабайт, себеби mtf_entry_sell = 0.01 бекитилгенде, htf_entry_sell = 0.03 мааниси болгону +35% берет. OAT (0.02, 0.01) жергиликтүү оптимумда "тыгылып калды" жана (0.03, 0.02) глобалдык оптимумду көрө албайт.
Бул классикалык көйгөй: эгер максаттуу функциянын ландшафты диагоналдык кырка тоолорду камтыса (бир параметрдин оптимуму башкасы өзгөргөн сайын жылышса), OAT аларды өткөрүп жиберет.
Көйгөйдү формалдаштыруу
максаттуу функция (PnL) болсун. OAT мындай чекитти табат:
Бирок бул глобалдык оптимум үчүн зарыл, жетиштүү эмес шарт. Эгер Гесс матрицасында маанилүү диагоналдан тышкаркы элементтер болсо — OAT болгондо кайчылаш туундуларды эсепке албайт.
Байланышкан параметрлер үчүн (бир нече таймфреймдеги бир багыттын чектери) — өз ара аракеттенүүлөр эреже, өзгөчөлүк эмес. Жогорку таймфреймдеги кирүү чеги кайсы сигналдар ортоңкусуна жетерин аныктайт, ал эми ортоңкусундагы чек төмөнкүсүндөгү чыпкалоо натыйжалуулугун аныктайт. Аддитивдүү параметрлер үчүн (ар кандай багыттар, көз карандысыз чыпкалар) кайчылаш туундулар нөлгө жакын — жана OAT жакшы иштейт.
Байес оптималдаштыруусу: акылдуу издөө

Идея
Сокур санап чыгуу же ач көз издөөнүн ордуна, Байес оптималдаштыруусу максаттуу функциянын сурогат моделин курат жана ар бир кадамда күтүлгөн жакшыруу максималдуу болгон чекитти тандайт.
Алгоритм:
- Бир нече кокус чекитти тандап, максаттуу функцияны баалоо
- Сурогат модель куруу (байкалган чекиттерден -ны жакындатат)
- Күтүлгөн жакшыруу максималдуу чекитти табуу (acquisition функциясы)
- Максаттуу функцияны ошол чекитте баалоо
- Сурогат моделди жаңылоо
- 3-5 кадамдарды кайталоо
OAT-тан негизги айырмасы: Байес оптималдаштыруусу бардык параметрлерди бир учурда карайт жана параметр мейкиндигиндеги диагоналдык кырка тоолорду изилдей алат.
TPE (Tree-structured Parzen Estimator)

TPE — Optuna-дагы демейки сэмплер. -ны түз моделдөөнүн ордуна, TPE эки бөлүштүрүүнү моделдейт:
- — максаттуу функция чегинен жакшы болгон параметрлердин бөлүштүрүлүшү
- — максаттуу функция чегинен начар болгон параметрлердин бөлүштүрүлүшү
TPE-нин acquisition функциясы — катыш:
TPE чоң ("жакшы" окшош параметрлер) жана кичине ("жаман" окшош эмес параметрлер) чекиттерди тандайт.
TPE эмне үчүн бэктесттер үчүн ылайыктуу:
- Параметрлер арасындагы шарттуу көз карандылыктарды иштетет
- Максаттуу функциянын үзгүлтүксүздүгүн талап кылбайт
- Орточо бюджеттерде натыйжалуу (100-1000 итерация)
- Категориялык жана дискреттик параметрлерди колдойт
Гаусс процесси (GP)
TPE-ге альтернатива — Гаусс процесси. GP -ны көп өлчөмдүү нормалдуу процесс катары моделдейт жана мааниге болжолду гана эмес, ошондой эле ар бир чекитте белгисиздикти да берет.
мында — орточо маани, — ковариация функциясы (ядро).
GP мындай учурларда жакшы иштейт:
- параметрлер аз (10-15-ке чейин)
- максаттуу функция тегиз
- ар бир жүгүртүү кымбат (мүнөттөр, саат)
Алдын ала эсептелген Parquet кэши менен бэктесттер үчүн, мында бир жүгүртүү ~1 секунда алат, адатта TPE артык көрүлөт: ал моделди тезирээк курат жана 500+ итерацияга жакшыраак масштабдалат.
Optuna менен практикалык интеграция

Толук иштеген мисал
import optuna
from optuna.samplers import TPESampler
import numpy as np
def run_backtest(htf_pre, mtf_pre, ltf_pre, **params) -> dict:
"""
Runs a backtest with given parameters.
Returns a dict with metrics: pnl, max_dd, n_trades, trading_time, sharpe.
Uses precomputed Parquet cache — ~1 second per run.
"""
pass
def objective(trial: optuna.Trial) -> float:
"""Objective function for Optuna."""
params = {
"htf_entry_sell": trial.suggest_float("htf_entry_sell", 0.0, 0.05, step=0.005),
"htf_entry_buy": trial.suggest_float("htf_entry_buy", 0.0, 0.05, step=0.005),
"mtf_entry_sell": trial.suggest_float("mtf_entry_sell", 0.0, 0.05, step=0.005),
"mtf_entry_buy": trial.suggest_float("mtf_entry_buy", 0.0, 0.05, step=0.005),
"ltf_entry_sell": trial.suggest_float("ltf_entry_sell", 0.0, 0.05, step=0.005),
"ltf_entry_buy": trial.suggest_float("ltf_entry_buy", 0.0, 0.05, step=0.005),
"htf_exit_sell": trial.suggest_float("htf_exit_sell", 0.0, 0.03, step=0.005),
"htf_exit_buy": trial.suggest_float("htf_exit_buy", 0.0, 0.03, step=0.005),
"mtf_exit_sell": trial.suggest_float("mtf_exit_sell", 0.0, 0.03, step=0.005),
"mtf_exit_buy": trial.suggest_float("mtf_exit_buy", 0.0, 0.03, step=0.005),
"min_hold_bars": trial.suggest_int("min_hold_bars", 1, 20),
"trail_pct": trial.suggest_float("trail_pct", 0.001, 0.02, step=0.001),
}
result = run_backtest(htf_pre, mtf_pre, ltf_pre, **params)
return -result["pnl_at_max_lev"]
study = optuna.create_study(
sampler=TPESampler(seed=42),
study_name="strategy_optimization",
direction="minimize",
)
study.optimize(objective, n_trials=500, show_progress_bar=True)
print(f"Best PnL: {-study.best_value:.2f}%")
print(f"Best params: {study.best_params}")
print(f"Total trials: {len(study.trials)}")
Бэктест башына ~1 секундада (алдын ала эсептелген кэш менен):
Толук издөөнүн 8 950 жылы менен салыштырганда сегиз мүнөт. Жана TPE 500 итерацияда OAT 96-да өткөрүп жиберген айкалыштарды табат, себеби ал параметр мейкиндигин бир учурда бир октун ордуна бир мезгилде изилдейт.
Изилдөөнү сактоо жана улантуу
import optuna
study = optuna.create_study(
storage="sqlite:///optuna_study.db",
study_name="strategy_v2",
sampler=TPESampler(seed=42),
direction="minimize",
load_if_exists=True, # continue if study already exists
)
study.optimize(objective, n_trials=300)
study.optimize(objective, n_trials=200)
Чектөөлөрдү кошуу
Бардык параметр айкалыштары жарактуу эмес. Мисалы, чыгуу чеги кирүү чегинен ашпашы керек:
def objective_with_constraints(trial: optuna.Trial) -> float:
htf_entry = trial.suggest_float("htf_entry_sell", 0.0, 0.05, step=0.005)
htf_exit = trial.suggest_float("htf_exit_sell", 0.0, 0.03, step=0.005)
if htf_exit > htf_entry:
raise optuna.TrialPruned()
result = run_backtest(htf_pre, mtf_pre, ltf_pre, **params)
return -result["pnl_at_max_lev"]
Сэмплерлерди салыштыруу

Optuna бир нече сэмплерлерди колдойт. Ар биринин өз күчтүү жактары бар.
TPESampler (демейки)
sampler = optuna.samplers.TPESampler(
n_startup_trials=20, # random trials before modeling begins
seed=42,
)
- Принцип: Tree-structured Parzen Estimator
- Күчтүү жактары: аралаш параметр түрлөрү үчүн жакшы, 1000+ итерацияга чейин масштабдалат
- Алсыз жактары: параметрлердин күчтүү өз ара аракеттенүүсү менен натыйжалуулугу төмөн болушу мүмкүн
- Качан колдонуу керек: башкасын тандоого негиз болбосо, демейки боюнча
CmaEsSampler
sampler = optuna.samplers.CmaEsSampler(seed=42)
- Принцип: Covariance Matrix Adaptation Evolution Strategy — ковариация матрицасын ыңгайлаштырган эволюциялык алгоритм
- Күчтүү жактары: үзгүлтүксүз параметрлер арасындагы өз ара аракеттенүүлөрдү табууда мыкты, корреляцияларды эсепке алат
- Алсыз жактары: категориялык параметрлерди колдобойт, инициализация үчүн көбүрөөк итерация талап кылат
- Качан колдонуу керек: бардык параметрлер үзгүлтүксүз болсо жана күчтүү өз ара аракеттенүүлөрдөн шектенсеңиз
GPSampler
sampler = optuna.samplers.GPSampler(seed=42)
- Принцип: acquisition функциясы бар Гаусс процесси
- Күчтүү жактары: эң жакшы үлгү алуу натыйжалуулугу (жакшы натыйжа үчүн аз итерация), белгисиздик бааларын берет
- Алсыз жактары: итерация санында — болгондо жай
- Качан колдонуу керек: бир бэктест кымбат болсо (мүнөттөр) жана бюджет 100-200 итерация менен чектелсе
RandomSampler (базалык сызык)
sampler = optuna.samplers.RandomSampler(seed=42)
- Принцип: бирдей кокус сэмплирлөө
- Күчтүү жактары: жергиликтүү оптимумдарда тыгылып калбайт, мейкиндиктин толук камтылышы
- Алсыз жактары: мурунку натыйжаларды колдонбойт
- Качан колдонуу керек: салыштыруу үчүн базалык сызык катары же изилдөө анализи үчүн
QMCSampler
sampler = optuna.samplers.QMCSampler(seed=42)
- Принцип: Quasi-Monte Carlo (Sobol/Halton ырааттуулуктары) — кокус сэмплерге караганда мейкиндикти бирдейирээк толтурат
- Күчтүү жактары: RandomSampler-ге караганда мейкиндиктин жакшыраак камтылышы, кайталангычтык
- Алсыз жактары: натыйжаларга ыңгайлашпайт
- Качан колдонуу керек: TPE-ге өтүүдөн мурун алгачкы 50-100 итерация үчүн
Жыйынтык таблица
| Сэмплер | Түрү | Өз ара аракеттенүү | Категориялык | Эң жакшы бюджет |
|---|---|---|---|---|
| TPE | Байес | Жарым-жартылай | Ооба | 100-1000 |
| CmaEs | Эволюциялык | Ооба | Жок | 200-2000 |
| GP | Байес | Ооба | Чектелген | 50-200 |
| Random | Кокус | Жок | Ооба | Каалагандай (базалык сызык) |
| QMC | Квази-кокус | Жок | Жок | 50-500 |
Практикалык бенчмарк
import optuna
import time
def benchmark_sampler(sampler, n_trials=300):
"""Compare samplers on the same task."""
study = optuna.create_study(sampler=sampler, direction="minimize")
start = time.time()
study.optimize(objective, n_trials=n_trials, show_progress_bar=False)
elapsed = time.time() - start
return {
"best_value": -study.best_value,
"elapsed_sec": elapsed,
"best_trial": study.best_trial.number,
}
samplers = {
"TPE": optuna.samplers.TPESampler(seed=42),
"CmaEs": optuna.samplers.CmaEsSampler(seed=42),
"GP": optuna.samplers.GPSampler(seed=42),
"Random": optuna.samplers.RandomSampler(seed=42),
"QMC": optuna.samplers.QMCSampler(seed=42),
}
for name, sampler in samplers.items():
result = benchmark_sampler(sampler, n_trials=300)
print(f"{name:8s}: best PnL={result['best_value']:.2f}%, "
f"found at trial #{result['best_trial']}, "
f"time={result['elapsed_sec']:.1f}s")
12 параметри бар стратегия үчүн типтүү натыйжалар:
| Сэмплер | Эң жакшы PnL | Итерацияда табылды | Сэмплер чыгымы |
|---|---|---|---|
| TPE | ~51% | ~180 | Төмөн |
| CmaEs | ~49% | ~250 | Орточо |
| GP | ~48% | ~90 | болгондо жогору |
| Random | ~42% | ~270 | Минималдуу |
| QMC | ~43% | ~200 | Минималдуу |
TPE жана CmaEs акыркы PnL боюнча кокус издөөдөн туруктуу түрдө 15-20% жакшы натыйжа көрсөтөт. GP жакшы натыйжаларды эртерээк табат, бирок итерациялардын саны көп болгондо эсептөө чегине жетет.
Көп максаттуу оптималдаштыруу: PnL менен MaxDD

Эмне үчүн бир критерий жетишсиз
Drawdown чектөөлөрүсүз PnL-ды максималдаштыруу — кырсыкка алып баруучу жол. Чыгым-пайда асимметриясына байланыштуу, PnL +80% жана MaxDD -30% бар стратегия PnL +50% жана MaxDD -5% бар стратегияга караганда бир топ коркунучтуу.
Оптималдаштыруу маселеси чындыгында көп максаттуу:
Бул максаттар карама-каршы: агрессивдүү параметрлер PnL менен drawdown-ду экөөнү тең көбөйтөт. Чечим бир чекит эмес, Парето фронту: бир метриканы жакшыртпай, экинчисин начарлатпай болбогон чечимдер жыйындысы.
Optuna-дагы NSGA-II / NSGA-III
import optuna
def multi_objective(trial: optuna.Trial) -> tuple[float, float]:
"""Multi-objective function: (PnL, MaxDD)."""
params = {
"htf_entry_sell": trial.suggest_float("htf_entry_sell", 0.0, 0.05, step=0.005),
"htf_entry_buy": trial.suggest_float("htf_entry_buy", 0.0, 0.05, step=0.005),
"mtf_entry_sell": trial.suggest_float("mtf_entry_sell", 0.0, 0.05, step=0.005),
"mtf_entry_buy": trial.suggest_float("mtf_entry_buy", 0.0, 0.05, step=0.005),
"ltf_entry_sell": trial.suggest_float("ltf_entry_sell", 0.0, 0.05, step=0.005),
"ltf_entry_buy": trial.suggest_float("ltf_entry_buy", 0.0, 0.05, step=0.005),
"htf_exit_sell": trial.suggest_float("htf_exit_sell", 0.0, 0.03, step=0.005),
"htf_exit_buy": trial.suggest_float("htf_exit_buy", 0.0, 0.03, step=0.005),
"mtf_exit_sell": trial.suggest_float("mtf_exit_sell", 0.0, 0.03, step=0.005),
"mtf_exit_buy": trial.suggest_float("mtf_exit_buy", 0.0, 0.03, step=0.005),
"min_hold_bars": trial.suggest_int("min_hold_bars", 1, 20),
"trail_pct": trial.suggest_float("trail_pct", 0.001, 0.02, step=0.001),
}
result = run_backtest(htf_pre, mtf_pre, ltf_pre, **params)
pnl = result["pnl"] # maximize
max_dd = result["max_dd"] # minimize (already a negative number)
return pnl, max_dd # Optuna: both directions are set in create_study
study = optuna.create_study(
directions=["maximize", "minimize"],
sampler=optuna.samplers.NSGAIIISampler(seed=42),
study_name="multi_objective_strategy",
)
study.optimize(multi_objective, n_trials=500)
pareto_trials = study.best_trials
print(f"Pareto front: {len(pareto_trials)} solutions")
for t in pareto_trials[:5]:
print(f" PnL={t.values[0]:.2f}%, MaxDD={t.values[1]:.2f}%")
Парето фронтундагы чекитти тандоо
Парето фронту бир нече чечим берет. Бирин кантип тандоо керек?
def select_from_pareto(
pareto_trials: list,
max_dd_limit: float = -5.0,
min_pnl: float = 20.0,
) -> list:
"""
Filter the Pareto front by constraints.
max_dd_limit: maximum acceptable drawdown (e.g., -5%)
min_pnl: minimum acceptable PnL (%)
"""
filtered = []
for trial in pareto_trials:
pnl, max_dd = trial.values
if max_dd >= max_dd_limit and pnl >= min_pnl:
max_lev = min(50 / abs(max_dd), 100) if max_dd != 0 else 100
pnl_at_max_lev = pnl * max_lev
filtered.append({
"trial": trial,
"pnl": pnl,
"max_dd": max_dd,
"max_lev": max_lev,
"pnl_at_max_lev": pnl_at_max_lev,
})
filtered.sort(key=lambda x: x["pnl_at_max_lev"], reverse=True)
return filtered
Эскертүү: максималдуу левередждеги PnL-ды эсептегенде, фандинг мөлчөрлөрүн эсепке алуу керек, болбосо теориялык жогорку левередж чыныгы рынокто чыгымга айланат. Мындан тышкары, акыркы PnL — бир чекиттик баа, ал эми натыйжанын туруктуулугун баалоо үчүн Монте-Карло бутстрэп керек.
Мисал: Парето фронтундагы үч стратегия
| Стратегия | PnL | MaxDD | MaxLev | PnL@MaxLev | Соода убактысы |
|---|---|---|---|---|---|
| Стратегия A | ~55% | ~0.9% | ~55x | ~3025% | ~15% |
| Стратегия B | ~25% | ~0.75% | ~66x | ~1650% | ~5% |
| Стратегия C | ~300% | ~17% | ~3x | ~900% | ~45% |
+300% таасирдүү PnL бар стратегия C жогорку drawdown-га байланыштуу PnL@MaxLev боюнча эң аз тартымдуу болуп чыгат. Стратегия A таза левереджделген киреше боюнча алдыда турат, бирок активдүү убакыт боюнча PnL-ды эсепке алганда стратегия B артыкчылыктуу болушу мүмкүн — бош убакыттын 95%-ы башка стратегиялар менен толтурулушу мүмкүн.
Контур графиктери жана параметрдин маанилүүлүгү

Ландшафтты визуалдаштыруу
Оптималдаштыруудан кийин — визуалдаштыруу. Optuna камтылган куралдарды сунуштайт:
import optuna.visualization as vis
fig_contour = vis.plot_contour(
study,
params=["htf_entry_sell", "mtf_entry_sell"],
)
fig_contour.show()
fig_importance = vis.plot_param_importances(study)
fig_importance.show()
fig_history = vis.plot_optimization_history(study)
fig_history.show()
fig_parallel = vis.plot_parallel_coordinate(
study,
params=["htf_entry_sell", "mtf_entry_sell", "ltf_entry_sell"],
)
fig_parallel.show()
fig_slice = vis.plot_slice(study)
fig_slice.show()
Контур графиги: өз ара аракеттенүүлөрдү окуу
Контур графиги параметр жубу үчүн максаттуу функциянын эки өлчөмдүү кесилишин курат. Эгер изосызыктар октордун бирине параллель болсо — параметрлер өз ара аракеттенбейт, жана OAT так ошол эле оптимумду табмак. Эгер изосызыктар диагоналдуу болсо — өз ара аракеттенүү бар, жана OAT аны өткөрүп жиберет.
key_params = ["htf_entry_sell", "mtf_entry_sell", "ltf_entry_sell",
"htf_entry_buy", "mtf_entry_buy", "ltf_entry_buy"]
for i, p1 in enumerate(key_params):
for p2 in key_params[i+1:]:
fig = vis.plot_contour(study, params=[p1, p2])
fig.write_image(f"contour_{p1}_vs_{p2}.png")
Эгер контур графиги плато көрсөтсө — максаттуу функция аз өзгөргөн аймак — бул жакшы белги. Плато натыйжанын кичине параметр четтөөлөрүнө туруктуу экенин билдирет. Плато анализи жана анын ашыкча ылайыкташуу менен байланышы жөнүндө көбүрөөк — алдыдагы Плато анализи макаласында.
Параметрдин маанилүүлүгү
importance = optuna.importance.get_param_importances(study)
for param, imp in importance.items():
print(f"{param:20s}: {imp:.4f}")
Типтүү чыгуу:
htf_entry_sell : 0.2841
mtf_entry_sell : 0.2103
ltf_entry_sell : 0.1567
trail_pct : 0.1204
htf_entry_buy : 0.0892
...
Маанилүүлүгү < 0,01 болгон параметрлерди алардын демейки маанисинде бекитүүгө болот — бул маселенин өлчөмдүүлүгүн азайтат жана оптималдаштырууну тездетет. Бирок этияттык менен: төмөн маанилүүлүк параметрдин башкалар менен өз ара аракеттенүүдө гана маанилүү экенин да билдириши мүмкүн. Контур графиктери аркылуу текшериңиз.
Алдын ала эсептелген кэш: эмне үчүн бэктест башына 1 секунда баарын өзгөртөт

Бир бэктестин ылдамдыгы кайсы оптималдаштыруу ыкмасын ала аларыңызды аныктайт.
| Бэктест убактысы | 96 OAT | 500 TPE | 2000 CmaEs |
|---|---|---|---|
| 60 секунда | 1,6 саат | 8,3 саат | 33 саат |
| 10 секунда | 16 мүнөт | 83 мүнөт | 5,5 саат |
| 1 секунда | 1,5 мүнөт | 8 мүнөт | 33 мүнөт |
| 0,1 секунда | 10 секунда | 50 секунда | 3,3 мүнөт |
Бэктест башына 60 секундада, 500 TPE итерациясы 8 сааттты алат. Дагы деле чыдамдуу, бирок итерациялоо (максаттуу функцияны өзгөртүү, кайра иштетүү) кымбат. 1 секундада — 8 мүнөт, жана сиз күнүнө ондогон эксперименттерди жүргүзө аласыз.
Так ушул себептен Parquet кэшине алдын ала эсептөө жөн гана ылдамдыкты оптималдаштыруу эмес, жеткиликтүү ыкмалар мейкиндигин кеңейтүү. Кэшсиз сиз OAT же 100 GP итерациясы менен чектелесиз. Кэш менен — 2000 CmaEs итерациясын же толук көп максаттуу NSGA-III-ды ала аласыз.
import pyarrow.parquet as pq
import time
t0 = time.time()
htf_pre = pq.read_table("cache/htf_indicators.parquet").to_pandas()
mtf_pre = pq.read_table("cache/mtf_indicators.parquet").to_pandas()
ltf_pre = pq.read_table("cache/ltf_indicators.parquet").to_pandas()
print(f"Cache loaded in {time.time() - t0:.2f}s") # ~0.3s
t1 = time.time()
result = run_backtest(htf_pre, mtf_pre, ltf_pre, htf_entry_sell=0.02, ...)
print(f"Backtest in {time.time() - t1:.2f}s") # ~1.0s
Практикалык сунуштар

OAT-ты качан колдонуу керек
OAT төмөнкү учурларда орундуу:
-
Изилдөө анализи. Сиз жаңы гана стратегияны изилдей баштап, кайсы параметрлер жалпы натыйжага таасир этерин түшүнгүңүз келет. 1,5 мүнөттө 96 жүгүртүү — сонун баштапкы чекит.
-
Аддитивдүү параметрлер. Кайчылашпаган мамилелер жыйындысында иштеген параметрлер үчүн (сатуу жана сатып алуу багыттары, ар кандай инструменттер), OAT туура натыйжаны тезирээк берет.
-
Өтө кымбат бэктест. Эгер бир жүгүртүү 10+ мүнөт алса жана тездетилбесе, 96 жүгүртүү (16 саат) менен OAT 500 TPE итерациясынан (3,5 күн) артык.
Optuna-ны качан колдонуу керек
Көпчүлүк учурда Optuna артык:
-
3-төн көп параметр. Өз ара аракеттенүүлөр иш жүзүндө кепилденген — OAT оптимумду өткөрүп жиберет.
-
Мульти-таймфрейм стратегиялары. Ар кандай таймфреймдердеги чектер дээрлик ар дайым өз ара байланышкан.
-
Акыркы оптималдаштыруу. Стратегия Монте-Карло бутстрэбинен өткөндө жана анын туруктуулугуна ишенимдүү болгондо — Optuna эң жакшы параметрлерди табат.
-
Көп максаттуу маселелер. PnL менен MaxDD менен соода убактысы — OAT бул маселени принципиалдуу түрдө чече албайт.
Гибриддик мамиле: аддитивдүүгө OAT + байланышканга Optuna
OAT менен Optuna арасында тандоо кажет эмес — аларды бириктирүү жакшыраак:
-
Параметрлерди классификациялоо. Аддитивдүү (көз карандысыз) жана байланышкан (интерактивдүү) деп бөлүңүз. 12 бөлүү параметри үчүн мисал:
- Аддитивдүү:
htf_entry_sell<->htf_entry_buy,mtf_entry_sell<->mtf_entry_buy,ltf_entry_sell<->ltf_entry_buy(сатуу/сатып алуу — ар кандай багыттар, кайчылашпаган мамилелерде иштейт) - Байланышкан топ, сатуу:
htf_entry_sell,mtf_entry_sell,ltf_entry_sell(чыпкалоо чынжыры: сатуу сигналдары үчүн HTF -> MTF -> LTF) - Байланышкан топ, сатып алуу:
htf_entry_buy,mtf_entry_buy,ltf_entry_buy
- Аддитивдүү:
-
Аддитивдүүгө OAT. Сатуу жана сатып алуу топторун көз карандысыз оптималдаштырыңыз. Эгер сатуу параметрлери сатып алуу мамилелерине таасир этпесе — OAT мүнөттөрдө туура натыйжа берет.
-
Байланышканга Optuna. Ар бир топтун ичинде (сатуу: кирүү+чыгуу 6 параметри) TPE колдонуңуз. 12-нин ордуна 6 параметр — бюджет эки эсе азаят.
sell_params = oat_sweep(sell_param_grid, run_backtest, initial_params)
def objective_sell(trial):
params = sell_params.copy()
params["htf_entry_sell"] = trial.suggest_float("htf_entry_sell", 0.0, 0.05, step=0.005)
params["mtf_entry_sell"] = trial.suggest_float("mtf_entry_sell", 0.0, 0.05, step=0.005)
params["ltf_entry_sell"] = trial.suggest_float("ltf_entry_sell", 0.0, 0.05, step=0.005)
params["htf_exit_sell"] = trial.suggest_float("htf_exit_sell", 0.0, 0.02, step=0.001)
params["mtf_exit_sell"] = trial.suggest_float("mtf_exit_sell", 0.0, 0.02, step=0.001)
params["ltf_exit_sell"] = trial.suggest_float("ltf_exit_sell", 0.0, 0.02, step=0.001)
return -run_backtest(**params)["effective_score"]
study = optuna.create_study(sampler=optuna.samplers.TPESampler())
study.optimize(objective_sell, n_trials=300) # 6 parameters → 300 is enough
Толук оптималдаштыруу түтүгү
1. Precompute Parquet cache (once)
2. Classify parameters: additive vs coupled
3. OAT for additive (~50 runs, ~1 min) → fix
4. Optuna TPE for coupled groups (300 iterations x 2 groups, ~10 min)
5. Optuna NSGA-III for meta-parameters (500 iterations, ~8 min) → Pareto front
6. Contour plots → visualize interactions
7. Monte Carlo bootstrap of best points → confidence intervals
8. Walk-Forward → out-of-sample validation
8-кадам — walk-forward оптималдаштыруу — ашыкча ылайыкташуудан коргонуу үчүн өтө маанилүү. Бул жөнүндө көбүрөөк алдыдагы Walk-Forward макаласында.
Оптималдаштыруунун тузактары
Ашыкча ылайыкташуу (overfitting). Параметрлер канчалык көп болсо жана оптималдаштыруу канчалык так болсо — стратегияны тарыхый маалыматтарга ыңгайлаштыруу коркунучу ошончолук жогору. 12 параметри бар 500 Optuna итерациясы окутуу жыйынында мыкты иштеген, бирок жаңы маалыматтарда пайдасыз айкалышты табат.
Коргонуу:
- Маалыматтарды train/test (70/30) деп бөлүңүз
- Туруктуулукту баалоо үчүн Монте-Карло бутстрэбин колдонуңуз
- Walk-forward аркылуу текшериңиз
- Платодогу чечимдерге артыкчылык бериңиз (бул жөнүндө көбүрөөк Плато анализинде)
Көптөгөн салыштыруулар маселеси. Эгер сиз 500 айкалышты текшерсеңиз, кокус түрдө "жакшы" натыйжа табуу ыктымалдуулугу артат. Bonferroni түзөтүүсү же FDR (False Discovery Rate) көзөмөлү жардам берет, бирок жөнөкөй мамиле — үлгүдөн тышкаркы текшерүү.
Жетишсиз бюджет. 12 параметр үчүн 50 итерациясы бар TPE өтө аз. Алгачкы 20 итерация кокус (startup), моделдөө үчүн болгону 30 калат. Минималдуу бюджет: 12 параметр үчүн итерация, сунушталган: .
Freqtrade: ал өндүрүштүк фреймворктто кантип иштейт

Freqtrade — популярдуу алготрейдинг фреймворктордун бири — Hyperopt модулу аркылуу ички жагынан Optuna колдонот. Анын тажрыйбасы биздин сунуштарды тастыктайт:
- Сэмплерлер: TPE (демейки), GP, CmaEs, NSGA-II, QMC — баары конфигурация аркылуу жеткиликтүү
- Чыгым функциялары: 12 камтылган чыгым функциясы, анын ичинде ShortTradeDurHyperOptLoss, SharpeHyperOptLoss, MaxDrawDownHyperOptLoss
- Көп максаттуу: бир нече метриканы бир учурда оптималдаштыруу үчүн NSGA-II жана NSGA-III колдоосу
- Ыңгайлаштырылган сэмплерлер: каалаган Optuna менен шайкеш сэмплерди туташтыруу мүмкүнчүлүгү
Freqtrade экосистемасынан негизги сабак: камтылган чыгым функциялары типтүү сценарийлерди камтыйт, бирок олуттуу оптималдаштыруу үчүн сизге стратегияңыздын өзгөчөлүктөрүн эсепке алган ыңгайлаштырылган максаттуу функция керек — активдүү убакыт, фандинг чыгымдары, так аткаруу симуляциясы үчүн ыңгайлашуучу drill-down.
Жыйынтык

Coordinate Descent (OAT) — тез жана интуитивдүү ыкма. 12 параметр үчүн болгону 96 жүгүртүү керек жана бир жарым мүнөттө аяктайт. Бирок ал параметрлердин өз ара аракеттенүүсүнө сокур — ал эми мульти-таймфрейм стратегияларда өз ара аракеттенүүлөр дээрлик ар дайым бар.
Optuna (TPE, GP, CmaEs) аркылуу Байес оптималдаштыруусу параметр мейкиндигин бүтүндөй изилдейт. Алдын ала эсептелген Parquet кэши менен 8 мүнөттө 500 итерация OAT-ка көрүнбөгөн айкалыштарды табат.
Көп максаттуу оптималдаштыруу (NSGA-III) "PnL-ды максималдаштыруу" маселесин "PnL менен MaxDD-дын Парето фронтун куруу" маселесине айландырат — жана ар кандай тобокел-киреше баланстары бар чечимдер жыйындысын сунуштайт.
Бирок оптималдаштыруу — түтүктүн бир бөлүгү гана. Табылган параметрлерди Монте-Карло бутстрэби аркылуу текшерүү, фандинг мөлчөрлөрүнө оңдоо, активдүү убакытты эсепке алуу менен кайра эсептөө жана walk-forward текшерүүдөн өткөрүү керек. Бул жөнүндө көбүрөөк сериянын алдыдагы макалаларында.
Пайдалуу шилтемелер
- Optuna: A Next-generation Hyperparameter Optimization Framework (Akiba et al., 2019)
- Algorithms for Hyper-Parameter Optimization (Bergstra et al., 2011) — the original TPE paper
- Optuna Documentation — Samplers
- Optuna Visualization Module
- Hansen, N. — The CMA Evolution Strategy: A Tutorial
- Deb, K. et al. — NSGA-II: A Fast and Elitist Multiobjective Genetic Algorithm (2002)
- Snoek, J. et al. — Practical Bayesian Optimization of Machine Learning Algorithms (2012)
- Freqtrade Documentation — Hyperopt
- Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12
- Bergstra, J. & Bengio, Y. — Random Search for Hyper-Parameter Optimization (2012)
Цитата
@article{soloviov2026optuna,
author = {Soloviov, Eugen},
title = {Coordinate Descent vs Bayesian Optimization: Which Finds Better Parameters},
year = {2026},
url = {https://marketmaker.cc/en/blog/post/optuna-vs-coordinate-descent},
description = {Why exhaustive search is impossible for 12+ parameters, how coordinate descent misses interactions, and how Optuna with a TPE sampler finds in 500 iterations what OAT cannot find in 96.}
}
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.