Koordinat İnişi vs Bayesian Optimizasyonu: Hangi Yöntem Daha İyi Parametreler Bulur
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
Kapsamlı arama (ızgara araması), her parametrenin her değer kombinasyonunu test eder. 9 değerli iki parametre için bu çalışma demektir — tamamen uygulanabilir. Üç parametre için: — kabul edilebilir.
Ancak 12 parametreli gerçek bir strateji için:
İ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:
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

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:
Izgara araması için ile karşılaştırın. OAT doğrusaldır: yerine . 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_sellvehtf_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_targetvebe_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_sellvemtf_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_fracvetp_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
amaç fonksiyonu (PnL) olsun. OAT şu noktayı bulur:
Ancak bu, global optimum için gerekli ancak yeterli bir koşul değildir. Hessian matrisi önemli köşegen dışı elemanlara sahipse — OAT, olduğunda çapraz türevleri 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

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:
- Birkaç rastgele nokta seç, amaç fonksiyonunu değerlendir
- Vekil model oluştur (gözlemlenen noktalardan 'yı yaklaşık olarak hesaplar)
- Maksimum beklenen iyileşme noktasını bul (edinim fonksiyonu)
- O noktada amaç fonksiyonunu değerlendir
- Vekil modeli güncelle
- 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, Optuna'daki varsayılan örnekleyicidir. 'yı doğrudan modellemek yerine TPE iki dağılımı modeller:
- — amaç fonksiyonunun eşiğinden daha iyi olduğu parametrelerin dağılımı
- — amaç fonksiyonunun eşiğinden daha kötü olduğu parametrelerin dağılımı
TPE'nin edinim fonksiyonu — oran:
TPE, 'nın büyük olduğu ("iyi" olanlara benzer parametreler) ve '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, '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.
burada ortalama, 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

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):
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ı

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 — 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 | 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

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:
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

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

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

OAT Ne Zaman Kullanılır
OAT şu durumlarda haklıdır:
-
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ı.
-
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.
-
Ç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:
-
3'ten fazla parametre. Etkileşimler neredeyse garantidir — OAT optimumu kaçıracak.
-
Çok zaman dilimli stratejiler. Farklı zaman dilimleri boyunca eşikler neredeyse her zaman birbirine bağlıdır.
-
Son optimizasyon. Strateji Monte Carlo bootstrap'ten geçtiğinde ve sağlamlığından emin olduğunuzda — Optuna en iyi parametreleri bulacaktır.
-
Ç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:
-
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
- Katkı maddesi:
-
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.
-
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
- 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 iterasyon, önerilen: .
Freqtrade: Bir Üretim Çerçevesinde Nasıl Çalışır

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ç

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
- Optuna: A Next-generation Hyperparameter Optimization Framework (Akiba et al., 2019)
- Algorithms for Hyper-Parameter Optimization (Bergstra et al., 2011) — orijinal TPE makalesi
- Optuna Belgeleri — Örnekleyiciler
- Optuna Görselleştirme Modülü
- 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 Belgeleri — Hyperopt
- Marcos Lopez de Prado — Advances in Financial Machine Learning, Bölüm 12
- 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.}
}
Yazarlar
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.