← Maqolalarga qaytish
March 11, 2026
5 daqiqa o'qish

Koordinatali tushish va Bayes optimallashtirishi: qaysi biri yaxshiroq parametrlarni topadi

Koordinatali tushish va Bayes optimallashtirishi: qaysi biri yaxshiroq parametrlarni topadi
#algotrading
#backtest
#optimization
#Optuna
#TPE
#Bayesian optimization
#coordinate descent
#hyperparameters

Bu "Illyuziyasiz bektestlar" turkumining beshinchi maqolasi. Oldingi maqolalarda biz zarar-foyda asimmetriyasini, Monte-Karlo bootstrap, funding stavkalarining ta'sirini va tezroq bektestlar uchun Parquet keshini ko'rib chiqdik. Endi strategiyaning optimal parametrlarini topish jarayoni haqida gaplashamiz — bu vazifada intuitsiya eng ko'p muvaffaqiyatsizlikka uchraydi.

Sizda 12 ta parametrga ega strategiya bor. Har bir parametr ~9 ta qiymat qabul qiladi. Siz cheklangan drawdown bilan PnL ni maksimallashtiruvchi kombinatsiyani topmoqchisiz. Buni qanday qilasiz?

Agar javobingiz "men barcha kombinatsiyalarni ko'rib chiqaman" bo'lsa — sizda muammo bor. Agar javobingiz "men bir vaqtda bitta parametrni o'zgartiraman" bo'lsa — sizda boshqa muammo bor. Bu maqola har bir yondashuv ortida qanday muammolar yashiringani va ularni qanday hal qilish haqida.

Nega to'liq qidiruv mumkin emas

The curse of dimensionality: exponential growth of the search space

O'lchamlilik la'nati

To'liq qidiruv (grid search) har bir parametr uchun qiymatlarning har bir kombinatsiyasini sinab ko'radi. 9 ta qiymatga ega ikkita parametr uchun bu 92=819^2 = 81 ishga tushirish — to'liq bajariladigan. Uchtasi uchun: 93=7299^3 = 729 — chidamli.

Lekin 12 ta parametrga ega haqiqiy strategiya uchun:

Ngrid=912=282,429,536,481N_{grid} = 9^{12} = 282{,}429{,}536{,}481

Ikki yuz sakson ikki milliard ishga tushirish. Hatto bitta bektest 1 soniya olsa ham (bu allaqachon optimistik), to'liq qidiruv quyidagicha vaqt oladi:

T=282×1093600×24×3658,950 yilT = \frac{282 \times 10^{9}}{3600 \times 24 \times 365} \approx 8{,}950 \text{ yil}

Bu eksponensial o'sish: har bir yangi parametr qidiruv maydonini 9 ga ko'paytiradi. 13-parametrni qo'shing — va 9 000 yil o'rniga sizga 80 000 kerak bo'ladi.

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

Oldindan hisoblash bilan ham

Parquet keshi haqidagi maqolada biz timeframe va indikatorlarni oldindan hisoblash bitta bektestni ~1 soniyagacha tezlashtirishini ko'rsatdik. Lekin hatto ishga tushirish boshiga 0,1 soniyada ham, 12 parametrning to'liq qidiruvi 895 yil talab qiladi. Oldindan hisoblash yordam beradi, lekin eksponensial o'sishning tub muammosini hal qilmaydi.

Bizga parametrlar maydonini to'liq qidiruvdan aqlliroq o'rganadigan usullar kerak.

Koordinatali tushish va OAT: tez, lekin ko'r

Parameter space exploration: OAT vs Bayesian optimization

Bir g'oyaning ikkita varianti

Ikkita bog'liq yondashuv bor — ikkalasi ham bir vaqtda bitta parametrni optimallashtiradi, lekin o'tishlar soni bilan farqlanadi:

OAT (One-at-a-Time) skanerlash — barcha parametrlar bo'yicha bitta o'tish. Birinchi parametrning qiymatlarini ko'rib chiqish, eng yaxshisini belgilash, ikkinchisiga o'tish — va hokazo. Bir marta. Tez va arzon.

Coordinate Descent — ko'p o'tishli. Oxirgi parametrni optimallashtirgandan so'ng birinchisiga qaytib, optimum o'zgarganmi yoki yo'qligini tekshirish kerak (chunki kontekst o'zgardi — boshqa parametr qiymatlari endi boshqacha). Raundlar yaqinlashguncha takrorlanadi. Qimmatroq, lekin aniqroq — har bir raund yechimni takomillashtirishi mumkin.

Amalda bektestlar uchun OAT ko'proq ishlatiladi: 12 parametr bo'yicha bitta o'tish — 96 ta ishga tushirish. 3-5 raundli Coordinate Descent — 300-500 ta ishga tushirish, bu allaqachon Optuna bilan solishtiriladi, lekin uning afzalliklarisiz.

Har birida ~8 ta qiymatga ega 12 parametr uchun:

NOAT=K×N=12×8=96 ishga tushirishN_{OAT} = K \times N = 12 \times 8 = 96 \text{ ishga tushirish}

Grid search uchun 282×109282 \times 10^9 bilan solishtiring. OAT chiziqli: O(NK)O(N^K) o'rniga O(KN)O(K \cdot N). Bu uning ham asosiy afzalligi, ham asosiy muammosi.

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

Optimallashtirish uchun qaysi metrikani tanlash kerak? Xom PnL yoki PnL@MaxLev o'rniga, yilga ekstrapolyatsiya qilingan effective score — faol vaqt bo'yicha PnL ishlatish tavsiya etiladi. Bu metrika pozitsiyadagi vaqtni hisobga oladi va turli savdo chastotasiga ega strategiyalarni to'g'ri solishtirishga imkon beradi.

Ko'r nuqta: parametrlarning o'zaro ta'siri

OAT har bir parametrning ta'siri additiv deb hisoblaydi — ya'ni, bitta parametrning optimal qiymati boshqalarning qiymatlariga bog'liq emas. Bu taxmin ba'zi parametrlar uchun to'g'ri, lekin bog'langanlar uchun buziladi.

Additiv va bog'langan parametrlar

Optimallashtirishdan oldin — parametrlarni tasniflash foydali:

Additiv (mustaqil) — birining optimal qiymati ikkinchisiga bog'liq emas. Ularni birma-bir arzon optimallashtirish mumkin:

  • htf_entry_sell va htf_entry_buy — bir xil timeframe da turli yo'nalishlar (sotish/sotib olish) uchun kirish chegaralari. Sotish chegarasi short signallarni filtrlaydi, sotib olish chegarasi — longlarni. Ular bir-biriga ustma-ust tushmaydigan bitimlar to'plamida ishlaydi.
  • tp_target va be_trigger — take-profit va breakeven, agar ular qarama-qarshi chiqish shartlarini yaratmasa.

Bog'langan (interaktiv) — birining optimal qiymati ikkinchisiga bog'liq. Birgalikda optimallashtirish kerak:

  • htf_entry_sell va mtf_entry_sell — turli timeframe lardagi bir xil yo'nalish (sotish) uchun chegaralar. HTF qaysi signallar MTF ga yetib borishini belgilaydi, MTF chegarasi esa filtrlash samaradorligini belgilaydi. MTF o'zgarganda HTF optimumi siljiydi.
  • ltf_entry_sell, mtf_entry_sell, htf_entry_sell — bitta yo'nalish uchun barcha chegaralar zanjiri.
  • partial_frac va tp_target — qisman yopishning hajmi TP darajasiga bog'liq.

Amaliy yondashuv: avval additiv parametrlarni OAT orqali arzon optimallashtiring. Keyin bog'langan guruhlarni Optuna orqali optimallashtiring. Bu byudjetni kamaytiradi: Optuna da 12 parametr o'rniga, biz faqat 6-8 bog'langanini yuboramiz, qolganlari esa allaqachon belgilangan.

Misol: OAT o'zaro ta'sirni qanday o'tkazib yuboradi

Ikkita bog'langan chegarani ko'rib chiqamiz:

  • htf_entry_sell — yuqori timeframe dagi chegara (sotish yo'nalishi)
  • mtf_entry_sell — o'rta timeframe dagi chegara (sotish yo'nalishi)

OAT mtf_entry_sell = 0.01 (boshlang'ich qiymat) ni belgilaydi va htf_entry_sell bo'yicha ko'rib chiqadi. Eng yaxshi qiymatni topadi: htf_entry_sell = 0.02. Uni belgilaydi va keyingi parametrga o'tadi — hech qachon qaytmaydi.

Mana OAT nimani o'tkazib yubordi:

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) kombinatsiyasi +51% PnL beradi, lekin OAT uni hech qachon ko'rib chiqmaydi, chunki belgilangan mtf_entry_sell = 0.01 da htf_entry_sell = 0.03 qiymati faqat +35% beradi. OAT (0.02, 0.01) mahalliy optimumida "qotib qoldi" va (0.03, 0.02) global optimumini ko'ra olmaydi.

Bu klassik muammo: agar maqsad funksiyasining landshafti diagonal tizmalarni o'z ichiga olsa (bitta parametrning optimumi boshqasi o'zgarganda siljisa), OAT ularni o'tkazib yuboradi.

Muammoni formallashtirish

f(θ1,θ2,,θK)f(\theta_1, \theta_2, \ldots, \theta_K) maqsad funksiyasi (PnL) bo'lsin. OAT quyidagi nuqtani topadi:

fθi=0i\frac{\partial f}{\partial \theta_i} = 0 \quad \forall i

Lekin bu global optimum uchun zaruriy, yetarli emas shart. Agar Gess matritsasida Hij=2fθiθjH_{ij} = \frac{\partial^2 f}{\partial \theta_i \partial \theta_j} muhim diagonaldan tashqari elementlar bo'lsa — OAT iji \neq j bo'lganda 2fθiθj\frac{\partial^2 f}{\partial \theta_i \partial \theta_j} o'zaro hosilalarni hisobga olmaydi.

Bog'langan parametrlar uchun (bir necha timeframe dagi bir yo'nalishning chegaralari) — o'zaro ta'sirlar qoida, istisno emas. Yuqori timeframe dagi kirish chegarasi qaysi signallar o'rtanchisiga yetib borishini belgilaydi, o'rtanchisidagi chegara esa quyi timeframe dagi filtrlash samaradorligini belgilaydi. Additiv parametrlar uchun (turli yo'nalishlar, mustaqil filtrlar) o'zaro hosilalar nolga yaqin — va OAT yaxshi ishlaydi.

Bayes optimallashtirishi: aqlli qidiruv

Bayesian optimization: surrogate model of the objective function

G'oya

Ko'r sanab chiqish yoki ochko'z qidirish o'rniga, Bayes optimallashtirishi maqsad funksiyasining surrogat modelini quradi va har bir qadamda kutilayotgan yaxshilanish maksimal bo'lgan nuqtani tanlaydi.

Algoritm:

  1. Bir nechta tasodifiy nuqtalarni tanlash, maqsad funksiyasini baholash
  2. Surrogat model qurish (kuzatilgan nuqtalardan f(θ)f(\theta) ni yaqinlashtiradi)
  3. Kutilayotgan yaxshilanish maksimal bo'lgan nuqtani topish (acquisition funksiyasi)
  4. Maqsad funksiyasini o'sha nuqtada baholash
  5. Surrogat modelni yangilash
  6. 3-5 qadamlarni takrorlash

OAT dan asosiy farqi: Bayes optimallashtirishi barcha parametrlarni bir vaqtda ko'rib chiqadi va parametrlar maydonidagi diagonal tizmalarni o'rganishi mumkin.

TPE (Tree-structured Parzen Estimator)

TPE sampler: modeling good and bad parameter distributions

TPE — Optuna dagi standart sampler. f(θ)f(\theta) ni to'g'ridan-to'g'ri modellashtirish o'rniga, TPE ikkita taqsimotni modellashtiradi:

  • l(θ)l(\theta) — maqsad funksiyasi yy^* chegarasidan yaxshiroq bo'lgan parametrlarning taqsimoti
  • g(θ)g(\theta) — maqsad funksiyasi yy^* chegarasidan yomonroq bo'lgan parametrlarning taqsimoti

TPE ning acquisition funksiyasi — nisbat:

EI(θ)l(θ)g(θ)\text{EI}(\theta) \propto \frac{l(\theta)}{g(\theta)}

TPE l(θ)l(\theta) katta ("yaxshi" ga o'xshash parametrlar) va g(θ)g(\theta) kichik ("yomon" ga o'xshamaydigan parametrlar) bo'lgan nuqtalarni tanlaydi.

TPE nima uchun bektestlar uchun mos:

  • Parametrlar orasidagi shartli bog'liqliklarni boshqaradi
  • Maqsad funksiyasining uzluksizligini talab qilmaydi
  • O'rtacha byudjetlarda samarali (100-1000 iteratsiya)
  • Kategorik va diskret parametrlarni qo'llab-quvvatlaydi

Gauss jarayoni (GP)

TPE ga muqobil — Gauss jarayoni. GP f(θ)f(\theta) ni ko'p o'lchovli normal jarayon sifatida modellashtiradi va nafaqat qiymat bashoratini, balki har bir nuqtadagi noaniqlikni ham beradi.

f(θ)GP(m(θ),  k(θ,θ))f(\theta) \sim \mathcal{GP}\bigl(m(\theta),\; k(\theta, \theta')\bigr)

bu yerda m(θ)m(\theta) — o'rtacha, k(θ,θ)k(\theta, \theta') — kovariatsiya funksiyasi (yadro).

GP quyidagi holatlarda yaxshi ishlaydi:

  • parametrlar kam (10-15 tagacha)
  • maqsad funksiyasi silliq
  • har bir ishga tushirish qimmat (daqiqalar, soatlar)

Oldindan hisoblangan Parquet keshi bilan bektestlar uchun, bunda bitta ishga tushirish ~1 soniya oladi, odatda TPE afzal ko'riladi: u modelni tezroq quradi va 500+ iteratsiyaga yaxshiroq masshtablanadi.

Optuna bilan amaliy integratsiya

Optuna optimization framework: iterative parameter search

To'liq ishlaydigan misol

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)}")

Bektest boshiga ~1 soniyada (oldindan hisoblangan kesh bilan):

T500=500×1s8 daqiqaT_{500} = 500 \times 1\text{s} \approx 8 \text{ daqiqa}

To'liq qidiruvning 8 950 yiliga nisbatan sakkiz daqiqa. Va TPE 500 iteratsiyada OAT 96 tada o'tkazib yuboradigan kombinatsiyalarni topadi, chunki u parametrlar maydonini bir vaqtda bitta o'q o'rniga bir vaqtda o'rganadi.

Tadqiqotni saqlash va davom ettirish

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)

Cheklovlar qo'shish

Barcha parametr kombinatsiyalari to'g'ri emas. Masalan, chiqish chegarasi kirish chegarasidan oshmasligi kerak:

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"]

Samplerlarni solishtirish

Sampler convergence comparison over iterations

Optuna bir nechta samplerlarni qo'llab-quvvatlaydi. Har birining o'z kuchli tomonlari bor.

TPESampler (standart)

sampler = optuna.samplers.TPESampler(
    n_startup_trials=20,  # random trials before modeling begins
    seed=42,
)
  • Printsip: Tree-structured Parzen Estimator
  • Kuchli tomonlari: aralash parametr turlari uchun yaxshi, 1000+ iteratsiyagacha masshtablanadi
  • Zaif tomonlari: parametrlarning kuchli o'zaro ta'siri bilan samaradorligi past bo'lishi mumkin
  • Qachon ishlatish kerak: boshqasini tanlashga sabab bo'lmasa, standart bo'yicha

CmaEsSampler

sampler = optuna.samplers.CmaEsSampler(seed=42)
  • Printsip: Covariance Matrix Adaptation Evolution Strategy — kovariatsiya matritsasini moslashtiruvchi evolyutsion algoritm
  • Kuchli tomonlari: uzluksiz parametrlar orasidagi o'zaro ta'sirlarni topishda a'lo, korrelyatsiyalarni hisobga oladi
  • Zaif tomonlari: kategorik parametrlarni qo'llab-quvvatlamaydi, initsializatsiya uchun ko'proq iteratsiya talab qiladi
  • Qachon ishlatish kerak: barcha parametrlar uzluksiz bo'lsa va kuchli o'zaro ta'sirlardan shubhalansangiz

GPSampler

sampler = optuna.samplers.GPSampler(seed=42)
  • Printsip: acquisition funksiyasi bilan Gauss jarayoni
  • Kuchli tomonlari: eng yaxshi namuna olish samaradorligi (yaxshi natija uchun kamroq iteratsiya), noaniqlik baholarini beradi
  • Zaif tomonlari: iteratsiya sonida O(n3)O(n^3)n>200n > 200 bo'lganda sekin
  • Qachon ishlatish kerak: bitta bektest qimmat bo'lsa (daqiqalar) va byudjet 100-200 iteratsiya bilan cheklangan bo'lsa

RandomSampler (bazaviy chiziq)

sampler = optuna.samplers.RandomSampler(seed=42)
  • Printsip: bir xil tasodifiy namuna olish
  • Kuchli tomonlari: mahalliy optimumlarda qotib qolmaydi, maydonning to'liq qamrovi
  • Zaif tomonlari: oldingi natijalarni ishlatmaydi
  • Qachon ishlatish kerak: solishtirish uchun bazaviy chiziq sifatida yoki tadqiqot tahlili uchun

QMCSampler

sampler = optuna.samplers.QMCSampler(seed=42)
  • Printsip: Quasi-Monte Carlo (Sobol/Halton ketma-ketliklari) — tasodifiy samplerga qaraganda maydonni bir xilroq to'ldiradi
  • Kuchli tomonlari: RandomSampler ga qaraganda maydonning yaxshiroq qamrovi, takrorlanuvchanlik
  • Zaif tomonlari: natijalarga moslashmaydi
  • Qachon ishlatish kerak: TPE ga o'tishdan oldin dastlabki 50-100 iteratsiya uchun

Umumlashtiruvchi jadval

Sampler Turi O'zaro ta'sir Kategorik Eng yaxshi byudjet
TPE Bayes Qisman Ha 100-1000
CmaEs Evolyutsion Ha Yo'q 200-2000
GP Bayes Ha Cheklangan 50-200
Random Tasodifiy Yo'q Ha Har qanday (bazaviy)
QMC Kvazi-tasodifiy Yo'q Yo'q 50-500

Amaliy benchmark

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 ta parametrga ega strategiya uchun tipik natijalar:

Sampler Eng yaxshi PnL Iteratsiyada topildi Sampler xarajati
TPE ~51% ~180 Past
CmaEs ~49% ~250 O'rtacha
GP ~48% ~90 n>200n > 200 da yuqori
Random ~42% ~270 Minimal
QMC ~43% ~200 Minimal

TPE va CmaEs yakuniy PnL bo'yicha tasodifiy qidiruvdan doimiy ravishda 15-20% yaxshiroq natija ko'rsatadi. GP yaxshi natijalarni ertaroq topadi, lekin iteratsiyalar soni ko'p bo'lganda hisoblash chegarasiga duch keladi.

Ko'p maqsadli optimallashtirish: PnL va MaxDD

Pareto front: tradeoff between PnL and maximum drawdown

Nega bitta mezon yetarli emas

Drawdown cheklovlarisiz PnL ni maksimallashtirish — falokatga olib boradigan yo'l. Zarar-foyda asimmetriyasi tufayli, PnL +80% va MaxDD -30% bo'lgan strategiya PnL +50% va MaxDD -5% bo'lgan strategiyaga qaraganda ancha xavflidir.

Optimallashtirish masalasi aslida ko'p maqsadli:

maxθ  PnL(θ)quyidagi shart bilanMaxDD(θ)min\max_{\theta} \; \text{PnL}(\theta) \quad \text{quyidagi shart bilan} \quad \text{MaxDD}(\theta) \to \min

Bu maqsadlar bir-biriga zid: tajovuzkor parametrlar ham PnL, ham drawdown ni oshiradi. Yechim bitta nuqta emas, balki Pareto fronti: bir metrikani yaxshilamasdan, ikkinchisini yomonlashtirmasdan bo'lmaydigan yechimlar to'plami.

Optuna dagi 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}%")

Pareto frontidan nuqta tanlash

Pareto fronti bir nechta yechim beradi. Qay birini tanlash kerak?

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

Eslatma: maksimal leveragedagi PnL ni hisoblashda funding stavkalarini hisobga olish kerak, aks holda nazariy jihatdan yuqori leverage real bozorda zararga aylanadi. Bundan tashqari, yakuniy PnL — bitta nuqta bahosi, natijaning barqarorligini baholash uchun esa Monte-Karlo bootstrap kerak.

Misol: Pareto frontidagi uchta strategiya

Strategiya PnL MaxDD MaxLev PnL@MaxLev Savdo vaqti
Strategiya A ~55% ~0.9% ~55x ~3025% ~15%
Strategiya B ~25% ~0.75% ~66x ~1650% ~5%
Strategiya C ~300% ~17% ~3x ~900% ~45%

+300% ta'sirchan PnL ga ega strategiya C yuqori drawdown tufayli PnL@MaxLev bo'yicha eng kam jozibali bo'lib chiqadi. Strategiya A sof leveraged daromad bo'yicha yetakchi, lekin faol vaqt bo'yicha PnL ni hisobga olganda strategiya B afzal bo'lishi mumkin — bo'sh vaqtning 95% i boshqa strategiyalar bilan to'ldirilishi mumkin.

Kontur grafiklari va parametr ahamiyati

Contour plots: visualizing parameter interactions and plateaus

Landshaftni vizuallashtirish

Optimallashtirishdan keyin — vizuallashtirish. Optuna o'rnatilgan vositalarni taklif qiladi:

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()

Kontur grafigi: o'zaro ta'sirlarni o'qish

Kontur grafigi parametr jufti uchun maqsad funksiyasining ikki o'lchovli kesimini quradi. Agar izoliniyalar o'qlardan biriga parallel bo'lsa — parametrlar o'zaro ta'sir qilmaydi va OAT xuddi shu optimumni topgan bo'lardi. Agar izoliniyalar diagonal bo'lsa — o'zaro ta'sir mavjud va OAT uni o'tkazib yuboradi.

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")

Agar kontur grafigi platoni ko'rsatsa — maqsad funksiyasi ozgina o'zgaradigan hudud — bu yaxshi belgi. Plato natijaning kichik parametr og'ishlariga chidamli ekanligini bildiradi. Plato tahlili va uning ortiqcha moslashish bilan bog'liqligi haqida ko'proq — kelgusi Plato tahlili maqolasida.

Parametr ahamiyati

importance = optuna.importance.get_param_importances(study)
for param, imp in importance.items():
    print(f"{param:20s}: {imp:.4f}")

Tipik natija:

htf_entry_sell      : 0.2841
mtf_entry_sell      : 0.2103
ltf_entry_sell      : 0.1567
trail_pct           : 0.1204
htf_entry_buy       : 0.0892
...

Ahamiyati < 0,01 bo'lgan parametrlarni ularning standart qiymatida belgilash mumkin — bu masalaning o'lchamliligini kamaytiradi va optimallashtirishni tezlashtiradi. Lekin ehtiyot bo'ling: past ahamiyat parametrning faqat boshqalar bilan o'zaro ta'sirda muhim ekanligini ham bildirishi mumkin. Kontur grafiklari orqali tekshiring.

Oldindan hisoblangan kesh: nega bektest boshiga 1 soniya hammasini o'zgartiradi

Precomputed Parquet cache: accelerating backtests from hours to seconds

Bitta bektestning tezligi qaysi optimallashtirish usulini qo'llay olishingizni belgilaydi.

Bektest vaqti 96 OAT 500 TPE 2000 CmaEs
60 soniya 1,6 soat 8,3 soat 33 soat
10 soniya 16 daqiqa 83 daqiqa 5,5 soat
1 soniya 1,5 daqiqa 8 daqiqa 33 daqiqa
0,1 soniya 10 soniya 50 soniya 3,3 daqiqa

Bektest boshiga 60 soniyada, 500 TPE iteratsiyasi 8 soat oladi. Hali ham chidamli, lekin iteratsiya qilish (maqsad funksiyasini o'zgartirish, qayta ishga tushirish) qimmatga tushadi. 1 soniyada — 8 daqiqa, va siz kuniga o'nlab tajribalar o'tkazishingiz mumkin.

Aynan shu sababdan Parquet keshiga oldindan hisoblash shunchaki tezlikni optimallashtirish emas, balki mavjud usullar maydonini kengaytirish. Keshsiz siz OAT yoki 100 GP iteratsiyasi bilan cheklanasiz. Kesh bilan — siz 2000 CmaEs iteratsiyasini yoki to'liq ko'p maqsadli NSGA-III ni qo'llay olasiz.

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

Amaliy tavsiyalar

Hybrid approach: combining OAT and Bayesian optimization

OAT ni qachon ishlatish kerak

OAT quyidagi hollarda oqlanadi:

  1. Tadqiqot tahlili. Siz endigina strategiyani o'rganishni boshladingiz va qaysi parametrlar natijaga umuman ta'sir qilishini tushunmoqchisiz. 1,5 daqiqada 96 ta ishga tushirish — ajoyib boshlang'ich nuqta.

  2. Additiv parametrlar. Bir-biriga ustma-ust tushmaydigan bitimlar to'plamida ishlaydigan parametrlar uchun (sotish va sotib olish yo'nalishlari, turli instrumentlar), OAT to'g'ri natijani tezroq beradi.

  3. Juda qimmat bektest. Agar bitta ishga tushirish 10+ daqiqa olsa va tezlashtirilmasa, 96 ta ishga tushirish (16 soat) bilan OAT 500 TPE iteratsiyasidan (3,5 kun) afzal.

Optuna ni qachon ishlatish kerak

Ko'p hollarda Optuna afzal:

  1. 3 tadan ko'p parametr. O'zaro ta'sirlar amalda kafolatlangan — OAT optimumni o'tkazib yuboradi.

  2. Ko'p timeframe li strategiyalar. Turli timeframe lardagi chegaralar deyarli har doim bir-biri bilan bog'liq.

  3. Yakuniy optimallashtirish. Strategiya Monte-Karlo bootstrap dan o'tganda va uning barqarorligiga ishonch hosil qilganingizda — Optuna eng yaxshi parametrlarni topadi.

  4. Ko'p maqsadli masalalar. PnL va MaxDD va savdo vaqti — OAT bu masalani printsipial jihatdan hal qila olmaydi.

Gibrid yondashuv: additivga OAT + bog'langanga Optuna

OAT va Optuna orasida tanlov qilish shart emas — ularni birlashtirish yaxshiroq:

  1. Parametrlarni tasniflash. Additiv (mustaqil) va bog'langan (interaktiv) ga bo'ling. 12 ta ajratish parametri uchun misol:

    • Additiv: htf_entry_sell <-> htf_entry_buy, mtf_entry_sell <-> mtf_entry_buy, ltf_entry_sell <-> ltf_entry_buy (sotish/sotib olish — turli yo'nalishlar, bir-biriga ustma-ust tushmaydigan bitimlarda ishlaydi)
    • Bog'langan guruh, sotish: htf_entry_sell, mtf_entry_sell, ltf_entry_sell (filtrlash zanjiri: sotish signallari uchun HTF -> MTF -> LTF)
    • Bog'langan guruh, sotib olish: htf_entry_buy, mtf_entry_buy, ltf_entry_buy
  2. Additivga OAT. Sotish va sotib olish guruhlarini mustaqil ravishda optimallashtiring. Agar sotish parametrlari sotib olish bitimlariga ta'sir qilmasa — OAT bir necha daqiqada to'g'ri natija beradi.

  3. Bog'langanga Optuna. Har bir guruh ichida (sotish: kirish+chiqish 6 ta parametri) TPE dan foydalaning. 12 o'rniga 6 ta parametr — byudjet ikki barobar kamayadi.

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

To'liq optimallashtirish quvuri

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-qadam — walk-forward optimallashtirish — ortiqcha moslashishdan himoyalanish uchun juda muhim. Bu haqda ko'proq kelgusi Walk-Forward maqolasida.

Optimallashtirish tuzoqlari

Ortiqcha moslashish (overfitting). Parametrlar qancha ko'p bo'lsa va optimallashtirish qancha aniq bo'lsa — strategiyani tarixiy ma'lumotlarga moslashtirish xavfi shuncha yuqori. 12 ta parametrga ega 500 ta Optuna iteratsiyasi o'quv to'plamida mukammal ishlaydigan, lekin yangi ma'lumotlarda foydasiz kombinatsiyani topadi.

Himoya:

  • Ma'lumotlarni train/test (70/30) ga bo'ling
  • Barqarorlikni baholash uchun Monte-Karlo bootstrap dan foydalaning
  • Walk-forward orqali tekshiring
  • Platolardagi yechimlarga ustunlik bering (bu haqda ko'proq Plato tahlili da)

Ko'p taqqoslash muammosi. Agar siz 500 ta kombinatsiyani sinasangiz, tasodifan "yaxshi" natijani topish ehtimoli oshadi. Bonferroni tuzatishi yoki FDR (False Discovery Rate) nazorati yordam beradi, lekin oddiyroq yondashuv — namunadan tashqari tekshirish.

Yetarli bo'lmagan byudjet. 12 ta parametr uchun 50 ta iteratsiyaga ega TPE juda kam. Dastlabki 20 ta iteratsiya tasodifiy (startup), modellashtirish uchun faqat 30 tasi qoladi. Minimal byudjet: 12 ta parametr uchun 10×K=12010 \times K = 120 iteratsiya, tavsiya etiladi: 3050×K30\text{--}50 \times K.

Freqtrade: ishlab chiqarish freymvorkida qanday ishlaydi

Freqtrade: automated trading framework with Optuna integration

Freqtrade — mashhur algotrading freymvorklaridan biri — Hyperopt moduli orqali ichki jihatdan Optuna dan foydalanadi. Uning tajribasi bizning tavsiyalarimizni tasdiqlaydi:

  • Samplerlar: TPE (standart), GP, CmaEs, NSGA-II, QMC — barchasi konfiguratsiya orqali mavjud
  • Yo'qotish funksiyalari: 12 ta o'rnatilgan yo'qotish funksiyasi, jumladan ShortTradeDurHyperOptLoss, SharpeHyperOptLoss, MaxDrawDownHyperOptLoss
  • Ko'p maqsadli: bir nechta metrikani bir vaqtda optimallashtirish uchun NSGA-II va NSGA-III ni qo'llab-quvvatlash
  • Moslashtirilgan samplerlar: har qanday Optuna bilan mos samplerni ulash imkoniyati

Freqtrade ekotizimidan asosiy saboq: o'rnatilgan yo'qotish funksiyalari odatiy stsenariylarni qamrab oladi, lekin jiddiy optimallashtirish uchun sizga strategiyangizning o'ziga xos xususiyatlarini hisobga oladigan moslashtirilgan maqsad funksiyasi kerak — faol vaqt, funding xarajatlari, aniq bajarilish simulyatsiyasi uchun moslashuvchan drill-down.

Xulosa

Complete optimization pipeline: from data to validated parameters

Coordinate Descent (OAT) — tez va intuitiv usul. 12 ta parametr uchun unga faqat 96 ta ishga tushirish kerak va bir yarim daqiqada tugaydi. Lekin u parametrlarning o'zaro ta'siriga ko'r — ko'p timeframe li strategiyalarda esa o'zaro ta'sirlar deyarli har doim mavjud.

Optuna (TPE, GP, CmaEs) orqali Bayes optimallashtirishi parametrlar maydonini butunligicha o'rganadi. Oldindan hisoblangan Parquet keshi bilan 8 daqiqada 500 ta iteratsiya OAT ga ko'rinmaydigan kombinatsiyalarni topadi.

Ko'p maqsadli optimallashtirish (NSGA-III) "PnL ni maksimallashtirish" masalasini "PnL va MaxDD ning Pareto frontini qurish" masalasiga aylantiradi — va turli tavakkalchilik-daromad muvozanatiga ega yechimlar to'plamini taklif qiladi.

Lekin optimallashtirish quvurning faqat bir qismi. Topilgan parametrlar Monte-Karlo bootstrap orqali tasdiqlanishi, funding stavkalariga tuzatilishi, faol vaqtni hisobga olgan holda qayta hisoblanishi va walk-forward tekshiruvidan o'tishi kerak. Bu haqda ko'proq turkumning kelgusi maqolalarida.


Foydali havolalar

  1. Optuna: A Next-generation Hyperparameter Optimization Framework (Akiba et al., 2019)
  2. Algorithms for Hyper-Parameter Optimization (Bergstra et al., 2011) — the original TPE paper
  3. Optuna Documentation — Samplers
  4. Optuna Visualization Module
  5. Hansen, N. — The CMA Evolution Strategy: A Tutorial
  6. Deb, K. et al. — NSGA-II: A Fast and Elitist Multiobjective Genetic Algorithm (2002)
  7. Snoek, J. et al. — Practical Bayesian Optimization of Machine Learning Algorithms (2012)
  8. Freqtrade Documentation — Hyperopt
  9. Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12
  10. Bergstra, J. & Bengio, Y. — Random Search for Hyper-Parameter Optimization (2012)

Iqtibos

@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.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Bozordan bir qadam oldinda bo'ling

Sun'iy intellekt savdo tahlillari, bozor tahlili va platforma yangiliklari uchun bizning xabarnomaga obuna bo'ling.

Biz sizning maxfiyligingizni hurmat qilamiz. Istalgan vaqtda obunadan chiqishingiz mumkin.