Strasmore Research
İncelemeler Matt ConnorYazan Matt Connor

Python ile tekrarlanabilir geriye dönük test yapımı

API anahtarı gerektirmeyen Python tabanlı geriye dönük test yöntemlerini inceleyin. Sabit sürüm kurulumu ve deterministik verilerle hisse senedi eğrilerini analiz etmenin yolları.

Python ile tekrarlanabilir bir geriye dönük test (backtest), yabancı bir kişinin temiz bir bilgisayarda çalıştırdığında, herhangi bir hesap veya API anahtarı engeline takılmadan, sizinle birebir aynı sonuçları elde edebildiği testtir. Çoğu eğitim içeriği, ilk satırda bu testi geçemez; çünkü canlı veri indirme işlemi, bir sonraki okuyucuya yazarın elde ettiğinden biraz farklı bir fiyat geçmişi sunar. Bu kılavuz, quantjourney-bt motorunun 0.12.4 sürümünü temel alarak, herhangi bir kimlik bilgisi gerektirmeyen örnek bir uygulamayı çalıştırır ve ardından tek bir temiz çalışmanın size söyleyemeyeceği gerçek piyasa verilerini gösterir.

Bir geriye dönük testin (backtest) tekrarlanabilir olmasını sağlayan nedir?

Tekrarlanabilirlik burada dar ve test edilebilir bir anlama sahiptir: ikinci bir kişi temiz bir makinede tek bir komutu çalıştırır ve sizin rakamlarınıza virgülden sonrasına kadar ulaşır. İki sıradan durum bunu bozar.

Birincisi koddur. 0.x sürümündeki bir kütüphane, küçük sürümler arasında hiçbir uyumluluk vaadi taşımaz. Bir yeniden adlandırma, değiştirilmiş bir varsayılan değer, yeniden sıralanmış bir sütun; betiğiniz çalışmaya devam ederken sessizce başka bir şey rapor etmeye başlar.

İkincisi veridir. İlk satırı canlı indirme olan bir eğitim materyali hiçbir zaman tekrarlanabilir olmamıştır. Veri sağlayıcıları geçmişi revize eder, bölünmelere göre düzeltme yapar ve boşlukları doldurur; aynı betik bir ay sonra farklı rakamlar üretir. İş işten geçtikten sonra, kod değişikliğini veri değişikliğinden ayıramazsınız.

Bu projeden kopyalanmaya değer model, sabit bir motor sürümünün paketle birlikte gelen küçük bir veri setiyle eşleştirilmesidir.

Kurulumu sabitleyin: pip install quantjourney-bt==0.12.4

quantjourney-bt, Apache Lisansı 2.0 altında yayımlanan ve Python 3.11 veya daha yeni bir sürüm gerektiren QuantJourney geriye dönük test aracıdır (backtester). Sürüm 0.12.4, 21 Temmuz 2026 tarihinde yayımlanmıştır ve Ağustos 2026'da belgelendiği üzere, aşağıdaki her komutun referans aldığı sürümdür.

İzole bir ortamda çalışın. python3 -m venv .venv bir ortam oluşturur, source .venv/bin/activate bu ortama giriş yapar ve python -m pip install -U pip ortam içindeki yükleyiciyi günceller. Ardından tam sürümü alın: pip install quantjourney-bt==0.12.4.

Proje, sabitlenmemiş biçimi pip install quantjourney-bt olarak belgeler. ==0.12.4 kısmı sizin sorumluluğunuzdadır ve 0.x sürümünde işlevini yerine getirmektedir. Sabitlemeyi bir sonraki kişinin bulabileceği bir yere yazın: pip freeze > requirements.txt, adını hiç anmadıklarınız dahil olmak üzere çözümlenmiş tüm bağımlılıkları yakalar.

İki isteğe bağlı eklenti mevcuttur. pip install "quantjourney-bt[wf]", ileriye dönük test (walk-forward) ve optimizasyon örnekleri için Optuna'yı ekler. pip install "quantjourney-bt[data]", kıyaslamalar (benchmarks) için kullanılan bir yfinance yedekleme seçeneği ekler.

Apache-2.0 izin verici bir lisanstır. Kodu ticari olarak kullanabilir ve değiştirebilirsiniz; herhangi bir yeniden dağıtımda lisans ve bildirim dosyalarını korumanız gerekir ve katkıda bulunanlar patent haklarını açıkça tanırlar.

API anahtarı olmadan paketlenmiş SMA örneğini çalıştırma

Depo, elli adet çalıştırılabilir örnek strateji ile birlikte bir başlatıcı betik içerir. Bu stratejiler, ağırlık tabanlı bir yol ve emir tabanlı bir yol olarak ikiye ayrılmış olup, aralarında beş adet ileriye dönük (walk-forward) iş akışı bulunur. ./strategy.sh --list kataloğu yazdırır. ./strategy.sh example_weights_01_sma_daily --check tek bir stratejiyi içe aktarır ve hiçbir veriye dokunmaz; bu, kurulumun sağlam olduğunun en hızlı teyididir.

Demo çalışması tek bir satırdan ibarettir: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample

--sample-data bayrağı işin tüm amacını taşır. Proje, arkasındaki veri setini şu şekilde tanımlar:

Örnek veri seti kasıtlı olarak küçük ve tekrarlanabilir tutulmuştur. Kurulum kontrolleri, rapor oluşturma ve motor akışını bir hesap oluşturmadan okumak için kullanışlıdır.
Kaynak: quantjourney-bt README, sürüm 0.12.4, erişim tarihi 6 Ağustos 2026.

Çalıştırma işlemi, konsolda bir hüküm yerine bir dizin yazar: summary.txt ve summary.json, bir metrics.csv, equity_curve.png dosyasının yanında bir equity_curve.csv, bir dashboard.html, bir plots/ klasörü ve çalıştırmanın nasıl yapılandırıldığını kaydeden bir run_metadata.json. Bu son dosya, çoğu kişinin atladığı ancak bir sonucu bir yıl sonra denetlenebilir kılan dosyadır.

Ortaya çıkan metrikleri dürüstçe okuyun. Paketlenmiş veri seti küçük ve açıklayıcı niteliktedir; bu nedenle summary.txt içinde yazdırılan Sharpe oranı ve maksimum kayıp sadece örnek bir dosyayı tanımlar. Bunlar bir strateji hakkında kanıt teşkil etmez ve bunları bir sonuç olarak değerlendirmek, yapabileceğiniz ilk hatadır.

Çalıştırmanın kanıtladığı şey ise değerlidir: kurulum çalışmaktadır ve sinyalden hedef ağırlıklara, oradan da yeniden yapılandırılmış portföy değerine kadar tüm motor akışı, hiçbir kimlik bilgisi gerektirmeden makinenizde çıktılarını üretir. Gerçek geçmiş veriler üzerinde yapılacak backtest işlemleri için projenin kendi veri servisine giden kimlik bilgisi gerektiren bir yol mevcuttur. Bu yol belgelenmiştir ve bu kılavuz, kimseden hiçbir şey talep etmeyen bu noktada sona ermektedir.

Tek bir örneklem içi özsermaye eğrisinin size söylemedikleri

Örnek çalışma tek bir özsermaye eğrisi çizer. Demo dosyası yerine gerçek piyasa verileriyle ölçüldüğünde, bu eğrinin yapamayacakları şunlardır.

Aşağıdaki panel, örnek stratejinin kullandığı fikri, yani yirmi seanslık hareketli ortalamanın elli seanslık olanı kesmesini alır, bunu SPY üzerinde uygular ve iki bin on yedi ile iki bin yirmi beş yılları arasındaki her takvim yılını ayrı ayrı raporlar. Her seansta alınan pozisyon bir önceki seansın kapanışıyla sabitlenir, bu nedenle kural henüz sahip olmadığı bir rakam üzerinden asla işlem yapmaz.

SorgulaSPY üzerinde 20/50 hareketli ortalama kesişimi, yıllara göre, elde tutma stratejisi ile karşılaştırmalı
Her verinin arkasındaki tam SQL
WITH daily AS
(
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2016-01-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY d
),
averaged AS
(
    SELECT
        d,
        px,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        d,
        px,
        if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    toYear(d)                                                                   AS year,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
    round((exp(sum(log(px / px_prior))) - 1) * 100, 1)                          AS hold_pct,
    countIf(long_today != long_prior)                                           AS crossover_count
FROM positioned
WHERE px_prior > 0
  AND toYear(d) >= 2017
GROUP BY year
ORDER BY year
Run this yourself

Değişmeyen tek bir kural, 9 ayrı seferde ölçüldü. Başka hiçbir şeyi okumadan önce iki yüzde sütununu aşağıya doğru inceleyin. 2017 döneminde kural, aynı dönem boyunca SPY tutmaya karşı 16% seviyesine kıyasla yılı 19.4% ile tamamladı. 2025 döneminde ise aynı iki sütun 10.4% ve 16.4% değerlerini gösterdi. Kod her iki satırda da aynıdır. Sadece pencere kaymıştır.

Kesişim sütunu, temel kanıtların ne kadar zayıfladığını göstermektedir. 2025 seans boyunca 4 pozisyon değişikliği, tüm bir özsermaye eğrisinin bir avuç karara dayandığı anlamına gelir ki bu da bir sonuç olarak adlandırmak için çok küçük bir örneklemdir.

Aynı kural diğer hisselerde de benzer şekilde mi çalışıyor?

Tarih aralığını değiştirmek, tek bir eğriyi incelemenin bir yoludur. Evreni değiştirmek ise diğer yoldur. Aşağıdaki panel, parametreleri sabit tutarak aynı kuralı 2021'den 2025'e kadar olan beş takvim yılı boyunca beş likit hisse üzerinde uygulamaktadır.

SorgulaBeş likit hissede 20/50 kuralı, 2021-2025 dönemi
Her verinin arkasındaki tam SQL
WITH daily AS
(
    SELECT
        ticker,
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
      AND window_start >= '2020-07-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY ticker, d
),
averaged AS
(
    SELECT
        ticker,
        d,
        px,
        avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (PARTITION BY ticker ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        ticker,
        d,
        px,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    ticker                                                                      AS symbol,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
    round((exp(sum(log(px / px_prior))) - 1) * 100, 1)                          AS hold_pct,
    round(avg(long_prior) * 100, 0)                                             AS days_long_pct
FROM positioned
WHERE px_prior > 0
  AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESC
Run this yourself

QQQ, panelin en üstünde 40.2% seviyesinde yer alırken, alt satır olan KO, 3.8% seviyesinde gerçekleşmektedir. days_long_pct sütunu, her bir versiyonun ilgili dönemde ne kadar süre pozisyonda kaldığını raporlamakta olup, üst satır için bu oran 67%'tür. Tek bir parametre seti, beş farklı evren ve kazananı geçmişe dönük olarak seçmenin henüz gerçekleşmemiş bir işlem için hiçbir anlam ifade etmediği kadar geniş bir spread söz konusudur.

Bunların hiçbiri bir crossover işlemi için tavsiye niteliğinde değildir. Crossover, backtest için bir ölçü birimidir; backtest ise ölçmekte olduğumuz şeyin kendisidir.

Ağırlıklandırma geriye dönük testlerinde ileriye dönük bakış yanlılığı (look-ahead bias) nasıl sızar?

Ağırlık tabanlı bir motor, bir sinyali hedef ağırlıklara dönüştürür, bu ağırlıklara karşı gerçekleşen işlemleri simüle eder ve ardından ortaya çıkan pozisyonlardan portföy değerini yeniden oluşturur. Hata, sinyal ile ağırlık arasındaki birleşim noktasında gizlidir. Eğer bugünün ağırlığı bugünün kapanış fiyatından türetiliyor ve ardından bugünün getirisini elde ediyorsa, geriye dönük test, emir verildiği sırada mevcut olmayan bir bilgiyle işlem yapmış demektir. Bu durum ileriye dönük bakış yanlılığı olarak adlandırılır ve herhangi bir hata uyarısı vermez. Sadece her şeyin olduğundan daha iyi görünmesini sağlar.

Aşağıdaki panel, aynı SPY geçmişi üzerinde bir kuralın her iki versiyonunu da çalıştırır.

SorgulaAynı kural, önceki seans sinyali ile aynı seans sinyali karşılaştırması, SPY yıllara göre
Her verinin arkasındaki tam SQL
WITH daily AS
(
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2016-01-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY d
),
averaged AS
(
    SELECT
        d,
        px,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        d,
        px,
        if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    toYear(d)                                                                   AS year,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
    round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
    round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
              - exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
  AND toYear(d) >= 2017
GROUP BY year
ORDER BY year
Run this yourself

2017 döneminde, önceki seans versiyonu 16% sonucunu üretirken, aynı seans versiyonu 17.1% sonucunu üretmiş olup aralarında 1.1 yüzde puan fark bulunmaktadır. 2025 döneminde ise iki versiyon arasındaki mesafe 1.6 yüzde puan olarak ölçülmüştür. Bu sütunlardan yalnızca biri, seansın nerede kapandığını önceden bilmeyen bir makine tarafından üretilebilir; aralarındaki fark ise tamamen muhasebeseldir ve arkasında hiçbir fikir, yetenek veya işlem barındırmaz.

Bu motor, zamanlama sorunu konusundaki kendi duruşunu ortaya koymaktadır ve bu, bir vaatten daha değerlidir:

Açılışta gerçekleşen işlemler için, aralık duyarlı kayma (slippage) yalnızca bir önceki tamamlanmış barı görür ve hacim kapasitesi gecikmeli gözlemlerden tahmin edilir; motor, o günün daha sonraki en yüksek, en düşük, kapanış fiyatlarını veya günün toplam hacmini kullanmaz.
Kaynak: quantjourney-bt README, sürüm 0.12.4, 6 Ağustos 2026 tarihinde okundu.

Belgelenmiş bir varsayım, halihazırda kurulu olan kaynak üzerinden kontrol edilebilir. Belgelenmemiş olan ise sadece bir tahmindir.

Walk-forward eklentisinin varlık nedeni

WF01'den WF05'e kadar olan walk-forward örnekleri, [wf] eklentisi ve onun Optuna bağımlılığı ile birlikte gelir. Walk-forward yöntemi, parametreleri geçmişin bir dilimi üzerinde optimize eder, bunları takip eden dilim üzerinde ölçer, ardından bu çifti ileri kaydırarak işlemi tekrarlar. Kayan ve genişleyen varyantlar, optimizasyon penceresinin ilerledikçe en eski verileri atıp atmaması bakımından farklılık gösterir. Bir diğer örnek, her sınırda bir temizleme ve ambargo ekleyerek birleşim noktasına en yakın gözlemleri çıkarır; böylece optimize edilen bir dilimin, ölçüm yapılan dilime sızması engellenir.

Bunların hiçbiri zayıf bir fikri çalışan bir stratejiye dönüştürmez. Bu yöntem, tek bir sayıyı üzerinde tartışabileceğiniz bir sayı dağılımı ile değiştirir ve tüm iyileştirme bundan ibarettir. Bundan sonraki adım hala gerçek para ile işlem yapmak değildir: gerçek para öncesi kağıt üzerinde işlem bir backtest'in yapısal olarak göremediği unsurları ölçer; buna emirlerinizin simülatörün varsaydığı fiyatlara yakın bir seviyede gerçekleşip gerçekleşmediği de dahildir. işlem botları için devre kesiciler ise kodunuzun çalışmadığı günlerde ne yapacağını kapsar. Tüm bunların temelindeki istatistikler için, açık kaynaklı kantitatif işlem kitabı hakkındaki notlarımız bir seviye daha derine iner.

SSS

API anahtarı olmadan bir stratejiyi geriye dönük test edebilir miyim?

Evet. quantjourney-bt, bir --sample-data bayrağı arkasında paketlenmiş örnek bir veri seti ile gelir ve örnek stratejileri herhangi bir hesap veya kimlik bilgisi gerektirmeden bu veri seti üzerinde çalışır. Veri seti küçük ve açıklayıcı nitelikte olduğundan, bu çalışmayı bir strateji kanıtından ziyade kurulum ve işlem hattı kontrolü olarak değerlendirin.

Neden bir Python geriye dönük test paketinin sürümünü sabitlemeliyim?

0.x sürümündeki bir paket, ara sürümler arasında hiçbir uyumluluk garantisi taşımaz; varsayılan değerlerdeki bir değişiklik veya yeniden adlandırılan bir metrik kendini önceden duyurmaz. pip install quantjourney-bt==0.12.4 ile sabitleme yapmak ve ortamı bir gereksinim dosyasında kaydetmek, bugün elde ettiğiniz bir sonucun gelecek yıl aynı motor üzerinde yeniden üretilebileceği anlamına gelir.

quantjourney-bt hangi lisans altında yayınlanmıştır?

Apache License 2.0. Bu lisans, ticari kullanım ve değişikliğe izin verir, yeniden dağıtımlarda lisans ve bildirim dosyalarının korunmasını şart koşar ve katkıda bulunanlardan açık bir patent izni içerir. 0.12.4 sürümü 21 Temmuz 2026 tarihinde yayınlanmıştır ve Python 3.11 veya daha yeni bir sürüm gerektirir.

Güçlü bir geriye dönük test sonucu, stratejinin çalıştığı anlamına mı gelir?

Hayır. Geriye dönük test; tek bir pencere üzerinde, tek bir evrende yapılan tek bir ölçümdür. Yukarıdaki paneller, tek bir değişmez kuralın bir ticker üzerinde çok farklı yıllık rakamlar ve beş farklı isim arasında çok farklı sonuçlar ürettiğini göstermektedir; bu, ileriye dönük doğrulama (walk-forward validation) ve örneklem dışı testlerin ortaya çıkarmak için tasarlandığı boşluktur.

Yukarıdaki paneller nasıl hesaplandı?

Günlük kapanışlar, her tarih için normal seansın son dakika işlemidir (print); New York saatiyle sabah 9:30 ile akşam 4:00 arasında alınmıştır. Bu yöntem, seans uzunluğunu sabit kodlamaya gerek kalmadan erken kapanış yapılan yarım günlerin doğru hesaplanmasını sağlar. Hızlı ortalama 20 seansı, yavaş ortalama ise 50 seansı kapsar; her ikisi de basittir ve her serinin ilk 49 seansı, pozisyon tutulmayan ısınma süresidir. Yıllık rakamlar, kuralın uzun (long) pozisyonda olduğu seanslar için her seansın kapanıştan kapanışa hareketini bileşikleştirir; "hold" sütunu ise karşılaştırma amacıyla aynı yıl içindeki her seansı bileşikleştirir. Kesit içindeki beş isim, pencere içinde bölünme (split) yaşanmamış sürekli geçmişlere sahip oldukları için seçilmiştir; bu nedenle kapanış serisi herhangi bir düzeltme gerektirmez. Pencereler geçmişte sabitlendiğinden, bu paneller her yeniden oluşturmada aynı sayıları verir.


Buradaki her panel, altında tam olarak kullanılan SQL sorgusunu taşır; bu da sayfadaki sayıların, sabitlenmiş kurulum kadar yeniden çalıştırılabilir olmasını sağlar. Herhangi bir geriye dönük test kodu yazmadan önce bir kuralı kendi pencereniz üzerinde ölçmek için, Strasmore terminalinde sorunuzu yalın bir dille sorun.

#backtesting#python#open source#reproducibility#quantjourney