← Makalelere geri dön
March 11, 2026
5 dakikalık okuma

Koordinat İnişi vs Bayesian Optimizasyonu: Hangi Yöntem Daha İyi Parametreler Bulur

Koordinat İnişi vs Bayesian Optimizasyonu: Hangi Yöntem Daha İyi Parametreler Bulur
#algotrading
#backtest
#optimizasyon
#Optuna
#TPE
#Bayesian optimizasyonu
#koordinat inişi
#hiperparametreler

Bu, "Yanılsamasız Backtestler" serisinin beşinci makalesidir. Önceki makalelerde kayıp-kar asimetrisini, Monte Carlo bootstrap, fonlama oranlarının etkisini ve daha hızlı backtestler için Parquet önbelleğini ele aldık. Şimdi strateji parametrelerini bulmaya ilişkin süreci konuşalım — sezginin en sık yanıltığı görev.

12 parametreli bir stratejiniz var. Her parametre yaklaşık 9 değer alıyor. PnL'i sınırlı düşüşle maksimize eden kombinasyonu bulmak istiyorsunuz. Bunu nasıl yaparsınız?

Cevabınız "tüm kombinasyonları iterasyon yaparım" ise — bir probleminiz var. Cevabınız "bir seferde bir parametreyi değiştiririm" ise — farklı bir probleminiz var. Bu makale, her yaklaşımın ardında ne tür sorunlar yattığını ve bunların nasıl çözüleceğini anlatmaktadır.

Kapsamlı Arama Neden İmkansız?

Boyutsallık laneti: arama uzayının üstel büyümesi

Boyutsallık Laneti

Kapsamlı arama (ızgara araması), her parametrenin her değer kombinasyonunu test eder. 9 değerli iki parametre için bu 92=819^2 = 81 çalışma demektir — tamamen uygulanabilir. Üç parametre için: 93=7299^3 = 729 — kabul edilebilir.

Ancak 12 parametreli gerçek bir strateji için:

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

İki yüz seksen iki milyar çalışma. Tek bir backtest 1 saniye alsa bile (ki bu zaten iyimser bir tahmin), kapsamlı arama şu kadar sürer:

T=282×1093600×24×3658,950 yılT = \frac{282 \times 10^{9}}{3600 \times 24 \times 365} \approx 8{,}950 \text{ yıl}

Bu üstel büyümedir: her yeni parametre arama uzayını 9 ile çarpar. 13. parametreyi ekleyin — 9.000 yıl yerine 80.000 yıla ihtiyacınız olur.

import math

def grid_search_cost(n_params: int, values_per_param: int, seconds_per_trial: float) -> dict:
    """Kapsamlı aramanın maliyetini tahmin et."""
    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"Deneme sayısı: {cost['total_trials']:,.0f}")      # 282,429,536,481
print(f"Yıl:  {cost['total_years']:,.0f}")                # 8,950

Ön Hesaplama ile Bile

Parquet önbelleği hakkındaki makalede, zaman dilimlerini ve göstergeleri önceden hesaplamanın tek bir backtest'i nasıl ~1 saniyeye hızlandırdığını gösterdik. Ancak çalışma başına 0,1 saniyede bile, 12 parametreli kapsamlı arama 895 yıl gerektirir. Ön hesaplama yardımcı olur, ancak üstel büyümenin temel sorununu çözmez.

Parametre uzayını kapsamlı aramadan daha akıllıca keşfeden yöntemlere ihtiyacımız var.

Koordinat İnişi ve OAT: Hızlı ama Kör

Parametre uzayı keşfi: OAT vs Bayesian optimizasyonu

Aynı Fikrin İki Varyantı

İki ilgili yaklaşım var — her ikisi de bir seferde bir parametreyi optimize eder, ancak geçiş sayısında farklılık gösterir:

OAT (Tek Seferlik) taraması — tüm parametrelerden tek bir geçiş. İlk parametrenin değerlerini iterasyon yap, en iyisini sabitle, ikinciye geç — ve böyle devam et. Bir kez. Hızlı ve ucuz.

Koordinat İnişi — çok geçişli. Son parametreyi optimize ettikten sonra, birinciye geri dön ve optimumun değişip değişmediğini kontrol et (bağlam değiştiğinden — diğer parametre değerleri artık farklı). Yakınsayana kadar turları tekrarla. Daha pahalı ama daha hassas — her tur çözümü iyileştirebilir.

Pratikte, backtestler için OAT daha sık kullanılır: 12 parametreden tek bir geçiş — 96 çalışma. 3-5 turlu koordinat inişi — 300-500 çalışma, bu zaten Optuna ile karşılaştırılabilir, ancak avantajları olmadan.

~8 değerli 12 parametre için:

NOAT=K×N=12×8=96 c¸alıs¸maN_{OAT} = K \times N = 12 \times 8 = 96 \text{ çalışma}

Izgara araması için 282×109282 \times 10^9 ile karşılaştırın. OAT doğrusaldır: O(NK)O(N^K) yerine O(KN)O(K \cdot N). Bu hem ana avantajı hem de ana sorunudur.

def oat_sweep(
    param_grid: dict[str, list],
    run_backtest_fn,
    initial_params: dict,
    metric: str = "effective_score",
) -> dict:
    """
    OAT taraması: tek geçiş, bir seferde bir parametreyi optimize eder.

    param_grid: {"htf_entry_sell": [0.0, 0.005, ..., 0.05], ...}
    initial_params: tüm parametreler için başlangıç değerleri
    metric: optimize edilecek metrik (effective_score önerilir —
            yıla çevrilmiş aktif süre başına PnL)
    """
    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

Optimizasyon için hangi metrik seçilmeli? Ham PnL veya PnL@MaxLev yerine, yıla çevrilmiş aktif süre başına PnL olan etkili skoru kullanmanız önerilir. Bu metrik, pozisyondaki süreyi hesaba katar ve farklı işlem frekanslarına sahip stratejilerin doğru karşılaştırılmasına olanak tanır.

Kör Nokta: Parametre Etkileşimleri

OAT, her parametrenin etkisinin katkı maddesi olduğunu varsayar — yani bir parametrenin optimal değeri diğerlerinin değerlerine bağlı değildir. Bu varsayım bazı parametreler için geçerlidir, ancak bağlantılı olanlar için bozulur.

Katkı Maddesi vs Bağlantılı Parametreler

Optimize etmeden önce — parametreleri sınıflandırmak faydalıdır:

Katkı maddesi (bağımsız) — birinin optimal değeri diğerine bağlı değildir. Tek tek ucuza optimize edilebilirler:

  • htf_entry_sell ve htf_entry_buy — aynı zaman diliminde farklı yönler (sat/al) için giriş eşikleri. Satış eşiği kısa sinyalleri filtreler, alış eşiği — uzunları. İşlemlerin örtüşmeyen alt kümelerinde çalışırlar.
  • tp_target ve be_trigger — çakışan çıkış koşulları oluşturmadığı takdirde kâr al ve başa baş.

Bağlantılı (etkileşimli) — birinin optimal değeri diğerine bağlıdır. Ortak optimizasyon gereklidir:

  • htf_entry_sell ve mtf_entry_sell — farklı zaman dilimlerinde aynı yön (sat) için eşikler. HTF hangi sinyallerin MTF'ye ulaştığını belirler ve MTF eşiği filtreleme etkinliğini belirler. HTF optimumu MTF değiştiğinde kayar.
  • ltf_entry_sell, mtf_entry_sell, htf_entry_sell — bir yön için tüm eşik zinciri.
  • partial_frac ve tp_target — kısmi kapanış boyutu TP seviyesine bağlıdır.

Pratik yaklaşım: önce OAT aracılığıyla katkı maddesi parametrelerini ucuza optimize edin. Ardından bağlantılı grupları Optuna aracılığıyla optimize edin. Bu bütçeyi azaltır: Optuna'ya 12 parametre yerine yalnızca 6-8 bağlantılı parametre gönderirken geri kalanlar zaten sabitlenir.

Örnek: OAT Bir Etkileşimi Nasıl Kaçırır

İki bağlantılı eşiği düşünün:

  • htf_entry_sell — daha yüksek zaman diliminde eşik (satış yönü)
  • mtf_entry_sell — orta zaman diliminde eşik (satış yönü)

OAT, mtf_entry_sell = 0.01 (başlangıç değeri) sabitler ve htf_entry_sell üzerinden iterasyon yapar. En iyi değeri bulur: htf_entry_sell = 0.02. Sabitler ve bir sonraki parametreye geçer — asla geri dönmez.

OAT'ın kaçırdığı şey:

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) kombinasyonu PnL +51% sağlar, ancak OAT bunu asla görmez çünkü sabit mtf_entry_sell = 0.01 ile htf_entry_sell = 0.03 değeri yalnızca +35% sağlar. OAT yerel optimumda (0.02, 0.01) "sıkışır" ve global optimumu (0.03, 0.02) göremez.

Bu klasik bir problemdir: amaç fonksiyonu manzarası çapraz sırtlar içeriyorsa (bir parametrenin optimumu diğeri değiştiğinde kayarsa), OAT bunları kaçırır.

Problemi Formalize Etme

f(θ1,θ2,,θK)f(\theta_1, \theta_2, \ldots, \theta_K) amaç fonksiyonu (PnL) olsun. OAT şu noktayı bulur:

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

Ancak bu, global optimum için gerekli ancak yeterli bir koşul değildir. Hessian matrisi Hij=2fθiθjH_{ij} = \frac{\partial^2 f}{\partial \theta_i \partial \theta_j} önemli köşegen dışı elemanlara sahipse — OAT, iji \neq j olduğunda çapraz türevleri 2fθiθj\frac{\partial^2 f}{\partial \theta_i \partial \theta_j} hesaba katmaz.

Bağlantılı parametreler için (birden fazla zaman dilimindeki aynı yönün eşikleri) — etkileşimler istisna değil, kuraldır. Daha yüksek zaman dilimindeki giriş eşiği hangi sinyallerin ortadakine ulaştığını belirler ve ortadaki eşik alt kısımdaki filtreleme etkinliğini belirler. Katkı maddesi parametreler için (farklı yönler, bağımsız filtreler) çapraz türevler sıfıra yakındır — ve OAT iyi çalışır.

Bayesian Optimizasyonu: Akıllı Arama

Bayesian optimizasyonu: amaç fonksiyonunun vekil modeli

Fikir

Kör numaralandırma veya açgözlü arama yerine, Bayesian optimizasyonu amaç fonksiyonunun bir vekil modeli oluşturur ve her adımda beklenen iyileşmenin maksimum olduğu noktayı seçer.

Algoritma:

  1. Birkaç rastgele nokta seç, amaç fonksiyonunu değerlendir
  2. Vekil model oluştur (gözlemlenen noktalardan f(θ)f(\theta)'yı yaklaşık olarak hesaplar)
  3. Maksimum beklenen iyileşme noktasını bul (edinim fonksiyonu)
  4. O noktada amaç fonksiyonunu değerlendir
  5. Vekil modeli güncelle
  6. 3-5 adımlarını tekrarla

OAT'dan temel fark: Bayesian optimizasyonu tüm parametreleri aynı anda göz önünde bulundurur ve parametre uzayındaki çapraz sırtları keşfedebilir.

TPE (Ağaç Yapılı Parzen Tahmincisi)

TPE örnekleyici: iyi ve kötü parametre dağılımlarını modelleme

TPE, Optuna'daki varsayılan örnekleyicidir. f(θ)f(\theta)'yı doğrudan modellemek yerine TPE iki dağılımı modeller:

  • l(θ)l(\theta) — amaç fonksiyonunun yy^* eşiğinden daha iyi olduğu parametrelerin dağılımı
  • g(θ)g(\theta) — amaç fonksiyonunun yy^* eşiğinden daha kötü olduğu parametrelerin dağılımı

TPE'nin edinim fonksiyonu — oran:

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

TPE, l(θ)l(\theta)'nın büyük olduğu ("iyi" olanlara benzer parametreler) ve g(θ)g(\theta)'nın küçük olduğu ("kötü" olanlara benzemeyen parametreler) noktaları seçer.

TPE'nin backtestler için neden uygun olduğu:

  • Parametreler arasındaki koşullu bağımlılıkları ele alır
  • Amaç fonksiyonunun sürekliliğini gerektirmez
  • Orta bütçelerle verimlidir (100-1000 iterasyon)
  • Kategorik ve ayrık parametreleri destekler

Gauss Süreci (GP)

TPE'ye bir alternatif — Gauss Süreci. GP, f(θ)f(\theta)'yı çok değişkenli normal süreç olarak modeller ve yalnızca bir değer tahmini değil, her noktada belirsizlik de sağlar.

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

burada m(θ)m(\theta) ortalama, k(θ,θ)k(\theta, \theta') kovaryans fonksiyonudur (çekirdek).

GP şu durumlarda iyi çalışır:

  • Az parametre var (10-15'e kadar)
  • Amaç fonksiyonu düzgündür
  • Her çalışma pahalıdır (dakikalar, saatler)

Tek bir çalışmanın ~1 saniye aldığı önceden hesaplanmış Parquet önbelleği olan backtestler için TPE genellikle tercih edilir: modeli daha hızlı oluşturur ve 500+ iterasyona daha iyi ölçeklenir.

Optuna ile Pratik Entegrasyon

Optuna optimizasyon çerçevesi: yinelemeli parametre araması

Tam Çalışma Örneği

import optuna
from optuna.samplers import TPESampler
import numpy as np


def run_backtest(htf_pre, mtf_pre, ltf_pre, **params) -> dict:
    """
    Verilen parametrelerle backtest çalıştırır.
    Metriklerle dict döndürür: pnl, max_dd, n_trades, trading_time, sharpe.
    Önceden hesaplanmış Parquet önbelleği kullanır — çalışma başına ~1 saniye.
    """
    pass


def objective(trial: optuna.Trial) -> float:
    """Optuna için amaç fonksiyonu."""
    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"En iyi PnL: {-study.best_value:.2f}%")
print(f"En iyi parametreler: {study.best_params}")
print(f"Toplam deneme: {len(study.trials)}")

Backtest başına ~1 saniyede (önceden hesaplanmış önbellekle):

T500=500×1s8 dakikaT_{500} = 500 \times 1\text{s} \approx 8 \text{ dakika}

Kapsamlı aramanın 8.950 yılına karşılık sekiz dakika. Ve TPE 500 iterasyonda OAT'ın 96'da kaçırdığı kombinasyonları bulur, çünkü parametre uzayını tek seferde bir eksen yerine eş zamanlı olarak keşfeder.

Çalışmayı Kaydetme ve Devam Ettirme

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,  # çalışma zaten varsa devam et
)

study.optimize(objective, n_trials=300)


study.optimize(objective, n_trials=200)

Kısıtlamalar Ekleme

Tüm parametre kombinasyonları geçerli değildir. Örneğin, çıkış eşiği giriş eşiğini aşmamalıdır:

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

Örnekleyici Karşılaştırması

Örnekleyicilerin iterasyonlar üzerindeki yakınsama karşılaştırması

Optuna birkaç örnekleyiciyi destekler. Her birinin kendine özgü güçlü yönleri vardır.

TPESampler (varsayılan)

sampler = optuna.samplers.TPESampler(
    n_startup_trials=20,  # modelleme başlamadan önce rastgele denemeler
    seed=42,
)
  • İlke: Ağaç Yapılı Parzen Tahmincisi
  • Güçlü yönleri: karma parametre türleri için iyi, 1000+ iterasyona ölçeklenir
  • Zayıf yönleri: güçlü parametre etkileşimleriyle daha az verimli olabilir
  • Ne zaman kullanılır: varsayılan olarak, başka birini seçmek için bir neden yoksa

CmaEsSampler

sampler = optuna.samplers.CmaEsSampler(seed=42)
  • İlke: Kovaryans Matrisi Adaptasyon Evrim Stratejisi — kovaryans matrisini uyarlayan evrimsel bir algoritma
  • Güçlü yönleri: sürekli parametreler arasındaki etkileşimleri bulmada mükemmel, korelasyonları hesaba katar
  • Zayıf yönleri: kategorik parametreleri desteklemez, başlatma için daha fazla iterasyon gerektirir
  • Ne zaman kullanılır: tüm parametreler sürekli ise ve güçlü etkileşimlerden şüpheleniyorsanız

GPSampler

sampler = optuna.samplers.GPSampler(seed=42)
  • İlke: edinim fonksiyonlu Gauss Süreci
  • Güçlü yönleri: en iyi örnek verimliliği (iyi sonuç için daha az iterasyon), belirsizlik tahminleri sağlar
  • Zayıf yönleri: iterasyon sayısında O(n3)O(n^3)n>200n > 200 olduğunda yavaş
  • Ne zaman kullanılır: tek bir backtest pahalıysa (dakikalar) ve bütçe 100-200 iterasyonla sınırlıysa

RandomSampler (temel)

sampler = optuna.samplers.RandomSampler(seed=42)
  • İlke: tekdüze rastgele örnekleme
  • Güçlü yönleri: yerel optimumlarda sıkışmaz, tam uzay kapsamı
  • Zayıf yönleri: önceki sonuçları kullanmaz
  • Ne zaman kullanılır: karşılaştırma için temel olarak veya keşif analizi için

QMCSampler

sampler = optuna.samplers.QMCSampler(seed=42)
  • İlke: Yarı-Monte Carlo (Sobol/Halton dizileri) — uzayı rastgele bir örnekleyiciden daha düzgün doldurur
  • Güçlü yönleri: RandomSampler'dan daha iyi uzay kapsamı, tekrarlanabilirlik
  • Zayıf yönleri: sonuçlara adapte olmaz
  • Ne zaman kullanılır: TPE'ye geçmeden önce ilk 50-100 iterasyon için

Özet Tablo

Örnekleyici Tür Etkileşimler Kategorik En İyi Bütçe
TPE Bayesian Kısmi Evet 100-1000
CmaEs Evrimsel Evet Hayır 200-2000
GP Bayesian Evet Sınırlı 50-200
Random Rastgele Hayır Evet Herhangi (temel)
QMC Yarı-rastgele Hayır Hayır 50-500

Pratik Benchmark

import optuna
import time

def benchmark_sampler(sampler, n_trials=300):
    """Örnekleyicileri aynı görevde karşılaştır."""
    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}: en iyi PnL={result['best_value']:.2f}%, "
          f"deneme #{result['best_trial']}'de bulundu, "
          f"süre={result['elapsed_sec']:.1f}s")

12 parametreli bir strateji için tipik sonuçlar:

Örnekleyici En İyi PnL Bulunduğu İterasyon Örnekleyici Yükü
TPE ~51% ~180 Düşük
CmaEs ~49% ~250 Orta
GP ~48% ~90 n>200n > 200 olduğunda Yüksek
Random ~42% ~270 Minimum
QMC ~43% ~200 Minimum

TPE ve CmaEs, rastgele aramayı son PnL'de sürekli olarak %15-20 geride bırakır. GP erken iyi sonuçlar bulur ancak çok sayıda iterasyonla hesaplama tavanına çarpar.

Çok Hedefli Optimizasyon: PnL vs MaxDD

Pareto cephesi: PnL ve maksimum düşüş arasındaki takas

Neden Tek Bir Kriter Yeterli Değil

Düşüş kısıtlaması olmadan PnL'i maksimize etmek felakete giden bir yoldur. PnL +80% ve MaxDD -30% olan bir strateji, kayıp-kar asimetrisi nedeniyle PnL +50% ve MaxDD -5% olan bir stratejiden çok daha risklidir.

Optimizasyon problemi aslında çok hedeflidir:

maxθ  PnL(θ)subject toMaxDD(θ)min\max_{\theta} \; \text{PnL}(\theta) \quad \text{subject to} \quad \text{MaxDD}(\theta) \to \min

Bu hedefler çakışır: agresif parametreler hem PnL'i hem de düşüşü artırır. Çözüm tek bir nokta değil, Pareto cephesidir: bir metriği diğerini kötüleştirmeden iyileştiremeyeceğiniz çözümler kümesi.

Optuna'da NSGA-II / NSGA-III

import optuna

def multi_objective(trial: optuna.Trial) -> tuple[float, float]:
    """Çok hedefli fonksiyon: (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"]          # maksimize et
    max_dd = result["max_dd"]    # minimize et (zaten negatif bir sayı)

    return pnl, max_dd  # Optuna: her iki yön create_study'de ayarlanır


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 cephesi: {len(pareto_trials)} çözüm")

for t in pareto_trials[:5]:
    print(f"  PnL={t.values[0]:.2f}%, MaxDD={t.values[1]:.2f}%")

Pareto Cephesinde Bir Nokta Seçme

Pareto cephesi birden fazla çözüm sunar. Nasıl seçilir?

def select_from_pareto(
    pareto_trials: list,
    max_dd_limit: float = -5.0,
    min_pnl: float = 20.0,
) -> list:
    """
    Pareto cephesini kısıtlamalarla filtrele.

    max_dd_limit: maksimum kabul edilebilir düşüş (örn., -5%)
    min_pnl: minimum kabul edilebilir 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

Not: maksimum kaldıraçtaki PnL'i hesaplarken fonlama oranlarını hesaba katmalısınız, aksi takdirde teorik olarak yüksek kaldıraç gerçek piyasada zarara dönüşür. Ayrıca son PnL tek noktalı bir tahmindir ve sonuç kararlılığını değerlendirmek için Monte Carlo bootstrap'e ihtiyacınız var.

Örnek: Pareto Cephesinde Üç Strateji

Strateji PnL MaxDD MaxLev PnL@MaxLev İşlem süresi
Strateji A ~55% ~0.9% ~55x ~3025% ~15%
Strateji B ~25% ~0.75% ~66x ~1650% ~5%
Strateji C ~300% ~17% ~3x ~900% ~45%

Etkileyici PnL +300% olan Strateji C, yüksek düşüş nedeniyle PnL@MaxLev açısından en az çekici olanı olduğu ortaya çıkar. Strateji A net kaldıraçlı getiri açısından önde gider, ancak aktif süre başına PnL hesaba katıldığında, Strateji B tercih edilebilir — zamanın %95'i diğer stratejilerle doldurulabilir.

Kontur Grafikleri ve Parametre Önemi

Kontur grafikleri: parametre etkileşimlerini ve platoları görselleştirme

Manzara Görselleştirme

Optimizasyondan sonra — görselleştirme. Optuna yerleşik araçlar sunar:

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 Grafiği: Etkileşimleri Okuma

Kontur grafiği, bir çift parametre için amaç fonksiyonunun iki boyutlu bir kesitini oluşturur. İzohipsler eksenlerden birine paralelse — parametreler etkileşmez ve OAT aynı optimumu bulurdu. İzohipsler çaprazsa — etkileşim var ve OAT bunu kaçırır.

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

Kontur grafiği bir plato gösteriyorsa — amaç fonksiyonunun az değiştiği bir bölge — bu iyi bir işarettir. Plato, sonucun küçük parametre sapmalarına karşı güçlü olduğu anlamına gelir. Plato analizi ve aşırı uyum ile ilişkisi hakkında daha fazlası — yaklaşan Plato analizi makalesinde.

Parametre Önemi

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

Tipik çıktı:

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

Önemi < 0.01 olan parametreler varsayılan değerlerinde sabitlenebilir — bu problemin boyutsallığını azaltır ve optimizasyonu hızlandırır. Ancak dikkatli olun: düşük önem, parametrenin yalnızca diğerleriyle etkileşimde önemli olduğu anlamına da gelebilir. Kontur grafikleri aracılığıyla doğrulayın.

Önceden Hesaplanmış Önbellek: Backtest Başına 1 Saniye Neden Her Şeyi Değiştirir

Önceden hesaplanmış Parquet önbelleği: backtest'leri saatlerden saniyelere hızlandırma

Tek bir backtest'in hızı, hangi optimizasyon yöntemini karşılayabileceğinizi belirler.

Backtest Süresi 96 OAT 500 TPE 2000 CmaEs
60 saniye 1.6 saat 8.3 saat 33 saat
10 saniye 16 dakika 83 dakika 5.5 saat
1 saniye 1.5 dakika 8 dakika 33 dakika
0.1 saniye 10 saniye 50 saniye 3.3 dakika

Backtest başına 60 saniyede, 500 TPE iterasyonu 8 saat sürer. Zaten tolere edilebilir, ancak iterasyon yapmak (amaç fonksiyonunu değiştirme, yeniden başlatma) pahalıdır. 1 saniyede — 8 dakika ve günde düzinelerce deney yapabilirsiniz.

Bu tam olarak Parquet önbelleğine ön hesaplamanın yalnızca bir hız optimizasyonu değil, aynı zamanda mevcut yöntemlerin alanının genişlemesi olmasının nedenidir. Önbellek olmadan OAT veya 100 GP iterasyonuyla sınırlısınız. Önbellekle — 2000 CmaEs iterasyonu veya tam çok hedefli NSGA-III'ü karşılayabilirsiniz.

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"Önbellek {time.time() - t0:.2f}s'de yüklendi")  # ~0.3s

t1 = time.time()
result = run_backtest(htf_pre, mtf_pre, ltf_pre, htf_entry_sell=0.02, ...)
print(f"Backtest {time.time() - t1:.2f}s'de")  # ~1.0s

Pratik Öneriler

Hibrit yaklaşım: OAT ve Bayesian optimizasyonunu birleştirme

OAT Ne Zaman Kullanılır

OAT şu durumlarda haklıdır:

  1. Keşif analizi. Bir stratejiyi keşfetmeye yeni başlıyorsunuz ve hangi parametrelerin sonucu etkilediğini anlamak istiyorsunuz. 1.5 dakikada 96 çalışma — mükemmel bir başlangıç noktası.

  2. Katkı maddesi parametreler. Örtüşmeyen işlem alt kümelerinde (sat vs al yönleri, farklı araçlar) çalışan parametreler için OAT daha hızlı doğru sonuç verir.

  3. Çok pahalı backtest. Tek bir çalışma 10+ dakika sürüyorsa ve hızlandırılamıyorsa, 96 çalışmalı OAT (16 saat) 500 TPE iterasyonuna (3.5 gün) tercih edilir.

Optuna Ne Zaman Kullanılır

Optuna çoğu durumda tercih edilir:

  1. 3'ten fazla parametre. Etkileşimler neredeyse garantidir — OAT optimumu kaçıracak.

  2. Çok zaman dilimli stratejiler. Farklı zaman dilimleri boyunca eşikler neredeyse her zaman birbirine bağlıdır.

  3. Son optimizasyon. Strateji Monte Carlo bootstrap'ten geçtiğinde ve sağlamlığından emin olduğunuzda — Optuna en iyi parametreleri bulacaktır.

  4. Çok hedefli problemler. PnL vs MaxDD vs işlem süresi — OAT bu problemi prensipte çözemez.

Hibrit Yaklaşım: Katkı Maddesi için OAT + Bağlantılı için Optuna

OAT ve Optuna arasında seçmek zorunda değilsiniz — ikisini birleştirmek daha iyidir:

  1. Parametreleri sınıflandırın. Katkı maddesi (bağımsız) ve bağlantılı (etkileşimli) olarak ayırın. 12 ayırma parametresi için örnek:

    • Katkı maddesi: htf_entry_sell <-> htf_entry_buy, mtf_entry_sell <-> mtf_entry_buy, ltf_entry_sell <-> ltf_entry_buy (sat/al — farklı yönler, örtüşmeyen işlemlerde çalışır)
    • Bağlantılı grup satış: htf_entry_sell, mtf_entry_sell, ltf_entry_sell (filtreleme zinciri: HTF -> MTF -> LTF sat sinyalleri için)
    • Bağlantılı grup alış: htf_entry_buy, mtf_entry_buy, ltf_entry_buy
  2. Katkı maddesi için OAT. Sat ve al gruplarını bağımsız olarak optimize edin. Sat parametreleri al işlemlerini etkilemiyorsa — OAT dakikalar içinde doğru sonuç verir.

  3. Bağlantılı için Optuna. Her grup içinde (satış: 6 parametre giriş+çıkış) TPE kullanın. 12 yerine 6 parametre — bütçe yarıya iner.

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 parametre → 300 yeterli

Tam Optimizasyon Boru Hattı

1. Parquet önbelleğini önceden hesapla (bir kez)
2. Parametreleri sınıflandır: katkı maddesi vs bağlantılı
3. Katkı maddesi için OAT (~50 çalışma, ~1 dk) → sabitle
4. Bağlantılı gruplar için Optuna TPE (300 iterasyon x 2 grup, ~10 dk)
5. Meta-parametreler için Optuna NSGA-III (500 iterasyon, ~8 dk) → Pareto cephesi
6. Kontur grafikleri → etkileşimleri görselleştir
7. En iyi noktaların Monte Carlo bootstrap'i → güven aralıkları
8. Walk-Forward → örnek dışı doğrulama
  1. adım — walk-forward optimizasyonu — aşırı uyuma karşı koruma için kritik öneme sahiptir. Bununla ilgili daha fazlası yaklaşan Walk-Forward makalesinde.

Optimizasyon Tuzakları

Aşırı uyum. Parametre ne kadar fazla ve optimizasyon ne kadar hassas olursa — stratejiyi tarihsel verilere uydurma riski o kadar yüksek. 12 parametreli 500 Optuna iterasyonu, eğitim setinde mükemmel çalışan ancak yeni verilerde işe yaramayan bir kombinasyon bulacaktır.

Koruma:

  • Verileri eğitim/test olarak bölün (70/30)
  • Kararlılığı değerlendirmek için Monte Carlo bootstrap kullanın
  • Walk-forward ile doğrulayın
  • Platodaki çözümleri tercih edin (Plato analizi hakkında daha fazlası)

Çoklu karşılaştırma problemi. 500 kombinasyonu test ederseniz, rastgele "iyi" bir sonuç bulma olasılığı artar. Bonferroni düzeltmesi veya FDR (Yanlış Keşif Oranı) kontrolü yardımcı olur, ancak daha basit yaklaşım örnek dışı doğrulamadır.

Yetersiz bütçe. 12 parametre için 50 iterasyonlu TPE çok az. İlk 20 iterasyon rastgele (başlangıç), modelleme için yalnızca 30 kalır. Minimum bütçe: 12 parametre için 10×K=12010 \times K = 120 iterasyon, önerilen: 3050×K30\text{--}50 \times K.

Freqtrade: Bir Üretim Çerçevesinde Nasıl Çalışır

Freqtrade: Optuna entegrasyonlu otomatik alım satım çerçevesi

Freqtrade — popüler algotrading çerçevelerinden biri — Hyperopt modülü aracılığıyla Optuna'yı kullanır. Deneyimi önerilerimizi doğruluyor:

  • Örnekleyiciler: TPE (varsayılan), GP, CmaEs, NSGA-II, QMC — yapılandırma yoluyla hepsi mevcut
  • Kayıp fonksiyonları: ShortTradeDurHyperOptLoss, SharpeHyperOptLoss, MaxDrawDownHyperOptLoss dahil 12 yerleşik kayıp fonksiyonu
  • Çok hedefli: birden fazla metriğin eş zamanlı optimizasyonu için NSGA-II ve NSGA-III desteği
  • Özel örnekleyiciler: Optuna uyumlu herhangi bir örnekleyiciyi bağlama yeteneği

Freqtrade ekosisteminden temel ders: yerleşik kayıp fonksiyonları tipik senaryoları kapsar, ancak ciddi optimizasyon için stratejinizin özelliklerini hesaba katan bir özel amaç fonksiyonu gerekir — aktif süre, fonlama maliyetleri, doğru doldurma simülasyonu için adaptif ayrıntılı inceleme.

Sonuç

Tam optimizasyon boru hattı: verilerden doğrulanmış parametrelere

Koordinat inişi (OAT) hızlı ve sezgisel bir yöntemdir. 12 parametre için yalnızca 96 çalışma gerektirir ve bir buçuk dakikada tamamlanır. Ancak parametre etkileşimlerine kördür — ve çok zaman dilimli stratejilerde etkileşimler neredeyse her zaman mevcuttur.

Optuna aracılığıyla Bayesian optimizasyonu (TPE, GP, CmaEs) parametre uzayını bütünüyle keşfeder. Önceden hesaplanmış Parquet önbelleğiyle 8 dakikada 500 iterasyon — OAT'a görünmez kombinasyonları bulur.

Çok hedefli optimizasyon (NSGA-III), "PnL'i maksimize et" problemini "PnL vs MaxDD Pareto cephesi oluştur" problemine dönüştürür — ve farklı risk-getiri takaslarıyla bir çözüm kümesi sunar.

Ancak optimizasyon boru hattının yalnızca bir parçasıdır. Bulunan parametrelerin Monte Carlo bootstrap ile doğrulanması, fonlama oranları için düzeltilmesi, aktif süre hesaba katılarak yeniden hesaplanması ve walk-forward doğrulamasından geçirilmesi gerekir. Bunun hakkında daha fazlası serinin yaklaşan makalelerinde.


Faydalı Bağlantılar

  1. Optuna: A Next-generation Hyperparameter Optimization Framework (Akiba et al., 2019)
  2. Algorithms for Hyper-Parameter Optimization (Bergstra et al., 2011) — orijinal TPE makalesi
  3. Optuna Belgeleri — Örnekleyiciler
  4. Optuna Görselleştirme Modülü
  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 Belgeleri — Hyperopt
  9. Marcos Lopez de Prado — Advances in Financial Machine Learning, Bölüm 12
  10. Bergstra, J. & Bengio, Y. — Random Search for Hyper-Parameter Optimization (2012)

Atıf

@article{soloviov2026optuna,
  author = {Soloviov, Eugen},
  title = {Coordinate Descent vs Bayesian Optimization: Which Finds Better Parameters},
  year = {2026},
  url = {https://marketmaker.cc/tr/blog/post/optuna-vs-coordinate-descent},
  description = {12+ parametre için kapsamlı aramanın neden imkansız olduğu, koordinat inişinin etkileşimleri nasıl kaçırdığı ve Optuna'nın TPE örnekleyicisiyle 500 iterasyonda OAT'ın 96'da bulamadığını nasıl bulduğu.}
}
Sorumluluk Reddi: Bu makalede sağlanan bilgiler yalnızca eğitim ve bilgilendirme amaçlıdır ve finansal, yatırım veya ticaret tavsiyesi niteliği taşımaz. Kripto para ticareti önemli bir kayıp riski içerir.

Yazarlar

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

Piyasanın Önünde Olun

Özel yapay zeka ticaret içgörüleri, piyasa analizi ve platform güncellemeleri için bültenimize abone olun.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.