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

Uyarlanabilir Drill-Down: Dakikadan Ham İşlemlere Değişken Granülerlikle Backtest

Uyarlanabilir Drill-Down: Dakikadan Ham İşlemlere Değişken Granülerlikle Backtest
#algotrading
#backtest
#parquet
#optimizasyon
#granülerlik
#drill-down
#uyarlanabilir çözünürlük

Dakika mum çubukları, backtestler için standart granülerlik düzeyidir. Ancak tek bir dakika mum çubuğu içinde fiyat farklı şekillerde hareket edebilir: bazen %0,01, bazen %2. Hem stop-loss hem de take-profit tek bir dakika mum çubuğunun [low, high] aralığına düştüğünde, backtest hangisinin önce tetiklendiğini bilemez. Bu, emir gerçekleşme belirsizliği sorunudur.

Saf çözüm, tüm backtest için saniye düzeyindeki verilere geçmektir. Ancak iki yıl boyunca bu, ~1 milyon dakika çubuğu yerine ~63 milyon saniye çubuğu anlamına gelir. Depolama 60 kat artar, hız orantılı olarak düşer.

Uyarlanabilir drill-down bu sorunu çözer: ince granülerliği yalnızca gerçekten gerekli olan yerde kullanın.

Emir gerçekleşme belirsizliği: hem SL hem de TP tek bir mum aralığına düşüyor

Sorun: Büyük Mum Çubuklarında Emir Gerçekleşme Belirsizliği

Belirli bir durumu ele alalım. Strateji 3000 USDT'de long açtı. Stop-loss: 2970 (-%1). Take-profit: 3060 (+%2).

14:37'deki dakika mum çubuğu:

  • Açılış: 3010
  • Yüksek: 3065
  • Düşük: 2965
  • Kapanış: 3050

Hem SL (2970) hem de TP (3060), [2965, 3065] aralığına düşüyor. Hangisi önce tetiklendi?

Olası sonuçlar:

  • Fiyat önce aşağı gitti -> SL tetiklendi -> -%1 zarar
  • Fiyat önce yukarı gitti -> TP tetiklendi -> +%2 kâr

Tek bir işlemdeki fark: 3 yüzde puanı. 10x kaldıraçla — %30. Yüzlerce işlem içeren bir backtestte, yanlış emir gerçekleşme belirsizliği çözümü sonuçları sistematik olarak çarpıtır.

Frameworkler Bunu Varsayılan Olarak Nasıl Ele Alır

Çoğu backtest motoru iki sezgisel yöntemden birini kullanır:

  1. İyimser: TP önce tetiklenir -> şişirilmiş sonuçlar
  2. Kötümser: SL önce tetiklenir -> düşürülmüş sonuçlar

Her iki yaklaşım da tahmindir. Gerçek veri saniye hatta milisaniye düzeyinde mevcuttur ve bakabilecekken tahmin etmeye gerek yoktur.

Drill-Down: Dört Seviyeli Strateji

Uyarlanabilir dört seviyeli drill-down çözünürlük piramidi

Drill-down fikri: dakika düzeyinde başla ve yalnızca belirsizlik olduğunda — fiyat hareketi ya da hacim ani yükselişi nedeniyle — daha alt seviyeye "in".

Seviye 1: 1m (dakika mum çubukları)
  -> SL veya TP [low, high] aralığının kesin olarak dışındaysa — yerinde çöz
  -> Her ikisi de aralık içindeyse — drill-down yap

Seviye 2: 1s (saniye mum çubukları)
  -> Bu dakika için 60 saniye çubuğu yükle
  -> Saniye saniye geç: hangisi önce tetiklendi?
  -> Bir saniye çubuğu belirsizse, VEYA price_move >= min_pct, VEYA volume >= median_1s * vol_mult — drill-down yap

Seviye 3: 100ms (milisaniye mum çubukları)
  -> Bu saniye için 10'a kadar 100ms çubuğu yükle
  -> 100ms'den 100ms'ye geç
  -> Bir 100ms çubuğu belirsizse, VEYA price_move >= min_pct, VEYA volume >= median_100ms * vol_mult — drill-down yap

Seviye 4: Ham işlemler
  -> Bu 100ms dilimi için tekil işlemleri yükle
  -> Emir gerçekleşmesini işlem bazında çöz — maksimum olası hassasiyet

Drill-Down'ın Gerekmediği Durumlar

Vakaların %95'inde drill-down gerekmez. Tipik senaryolar:

Kesin SL: mum çubuğu yükseği TP'ye ulaşmıyor, düşüğü SL'yi kırıyor -> SL tetiklendi, drill-down gerekmiyor.

Kesin TP: düşük SL'ye ulaşmıyor, yüksek TP'yi kırıyor -> TP tetiklendi, drill-down gerekmiyor.

Hiçbiri tetiklenmedi: her iki seviye de aralığın dışında -> pozisyon açık kalır.

Gap tespiti: bir sonraki mum çubuğunun açılışı SL veya TP'yi atlıyor -> açılış fiyatında gerçekleşme, drill-down gerekmiyor.

Drill-down yalnızca yaklaşık %5 çubuğunda gereklidir — her iki seviye de tek bir mum çubuğunun aralığına düştüğünde.

class AdaptiveFillSimulator:
    """
    Emir gerçekleşme sırası belirlemek için dört seviyeli drill-down.
    """
    def __init__(self, data_loader):
        self.loader = data_loader
        self.cache_1s = {}  # Aya göre saniye verisi önbelleği

    def check_fill(self, timestamp, candle_1m, sl_price, tp_price, side):
        """
        Verilen dakika mum çubuğunda SL veya TP'nin tetiklenip tetiklenmediğini kontrol eder.

        Döndürür: ('sl', fill_price) | ('tp', fill_price) | None
        """
        low, high = candle_1m['low'], candle_1m['high']

        open_price = candle_1m['open']
        if side == 'long':
            if open_price <= sl_price:
                return ('sl', open_price)
            if open_price >= tp_price:
                return ('tp', open_price)
        else:
            if open_price >= sl_price:
                return ('sl', open_price)
            if open_price <= tp_price:
                return ('tp', open_price)

        sl_hit = self._level_hit(sl_price, low, high, side, 'sl')
        tp_hit = self._level_hit(tp_price, low, high, side, 'tp')

        if sl_hit and not tp_hit:
            return ('sl', sl_price)
        if tp_hit and not sl_hit:
            return ('tp', tp_price)
        if not sl_hit and not tp_hit:
            return None

        return self._drill_down_1s(timestamp, sl_price, tp_price, side)

    def _drill_down_1s(self, minute_ts, sl_price, tp_price, side):
        """Seviye 2: saniye saniye geçiş."""
        bars_1s = self.loader.load_1s_for_minute(minute_ts)

        if bars_1s is None or len(bars_1s) == 0:
            return self._pessimistic_fill(side, sl_price, tp_price)

        for bar in bars_1s:
            sl_hit = self._level_hit(sl_price, bar['low'], bar['high'], side, 'sl')
            tp_hit = self._level_hit(tp_price, bar['low'], bar['high'], side, 'tp')

            if sl_hit and not tp_hit:
                return ('sl', sl_price)
            if tp_hit and not sl_hit:
                return ('tp', tp_price)
            if sl_hit and tp_hit:
                result = self._drill_down_100ms(bar['timestamp'], sl_price, tp_price, side)
                if result:
                    return result

        return self._pessimistic_fill(side, sl_price, tp_price)

    def _pessimistic_fill(self, side, sl_price, tp_price):
        """Kötümser varsayım: longlar için SL, shortlar için TP."""
        if side == 'long':
            return ('sl', sl_price)
        else:
            return ('sl', sl_price)

Performans

Mod Emir gerçekleşme kontrolü başına süre Ne zaman kullanılır
1m (drill-down yok) ~0ms Vakaların ~%95'i
1s drill-down ~5ms (aya ilk erişimde) Vakaların ~%5'i
100ms drill-down ~1ms <%0,5 vaka
Ham işlem drill-down ~0,5ms <%0,1 vaka

~400 işlem içeren 2 yıllık bir backtestte, drill-down yaklaşık 20 mum çubuğu için çağrılır. Toplam ek yük — tüm backtest için 1 saniyeden az.

Uyarlanabilir Veri Depolama

Drill-down, saniye ve milisaniye verisi gerektirir. Ancak her şeyi maksimum granülerlikte depolamak pratik değildir:

Granülerlik 2 yıllık çubuk sayısı Parquet boyutu
1m ~1,05M ~15 MB
1s ~63M ~550 MB/ay
100ms ~630M ~5 GB/ay

2 yıllık eksiksiz 1s arşivi yaklaşık 13 GB'dir. 100ms — 100 GB'ın üzerinde. Her şeyi depolamak mümkündür ancak drill-down'ın bu verinin %1'inden azını kullandığı düşünüldüğünde israftır.

Sıcak Saniye Tespiti

Sıcak saniye tespiti ve uyarlanabilir depolama tasarrufları

Temel gözlem: fiyatın önemli ölçüde hareket ettiği saniyeler küçük bir kesimi oluşturur. Bir saniye içinde fiyat %0,1'den az değiştiyse — o saniye için 100ms dağılımını depolamanın anlamı yoktur.

Sıcak saniye tespiti: veri indirip işlerken her saniyeyi analiz ederiz ve 100ms mum çubuklarını yalnızca "sıcak" saniyeler için oluştururuz — fiyat hareketi eşiği aşan saniyeler.

def process_trades_adaptive(
    trades: pd.DataFrame,
    min_price_change_pct: float = 1.0,
) -> tuple[pd.DataFrame, pd.DataFrame]:
    """
    Ham işlemleri uyarlanabilir bir yapıya işler:
    - Tüm saniyeler için 1s mum çubukları
    - Yalnızca "sıcak" saniyeler için 100ms mum çubukları

    Args:
        trades: [timestamp, price, quantity] sütunlarına sahip DataFrame
        min_price_change_pct: 100ms'ye drill-down için eşik

    Döndürür:
        (df_1s, df_100ms_hot) — saniye mum çubukları ve sıcak saniyeler için 100ms
    """
    trades['second'] = trades['timestamp'].dt.floor('1s')
    df_1s = trades.groupby('second').agg(
        open=('price', 'first'),
        high=('price', 'max'),
        low=('price', 'min'),
        close=('price', 'last'),
        volume=('quantity', 'sum'),
    )

    df_1s['price_change_pct'] = (df_1s['high'] - df_1s['low']) / df_1s['open'] * 100
    hot_seconds = df_1s[df_1s['price_change_pct'] >= min_price_change_pct].index

    hot_trades = trades[trades['second'].isin(hot_seconds)]
    hot_trades['bucket_100ms'] = hot_trades['timestamp'].dt.floor('100ms')

    df_100ms = hot_trades.groupby('bucket_100ms').agg(
        open=('price', 'first'),
        high=('price', 'max'),
        low=('price', 'min'),
        close=('price', 'last'),
        volume=('quantity', 'sum'),
    )

    return df_1s, df_100ms

Depolama Tasarrufları

Örneğin — tipik bir ayda ETHUSDT:

Yaklaşım Boyut Granülerlik
Yalnızca 1m ~1 MB 1 dakika
Tüm 1s ~550 MB 1 saniye
Tüm 100ms ~5 GB 100 ms
Uyarlanabilir ~600 MB 1s + yalnızca sıcak saniyeler için 100ms

min_price_change_pct = 1,0% eşiğiyle, sıcak saniyeler tüm saniyelerin %1'inden azını oluşturur. Onlar için 100ms verisi, 550 MB saniye verisine ~50 MB ekler — ihmal edilebilir bir ek yük.

Saniye verisi de uyarlanabilir şekilde (yalnızca bir dakika içindeki hareket %0,1'i aşıyorsa) depolanırsa, hacim 3-5 kat daha azaltılabilir.

Uyarlanabilir Parquet depolama hiyerarşisi: dakika, saniye, sıcak milisaniye ve işlem dosyaları

Parquet Depolama Yapısı

data/{SYMBOL}/
├── source.json                # Borsa kaynağı: {"exchange": "binance"} veya {"exchange": "bybit"}
├── stats.json                 # Önceden hesaplanmış medyan hacimler: {"median_volume_1s": ..., "median_volume_100ms": ...}
├── klines_1m/
│   ├── 2024-01.parquet       # ~1 MB
│   ├── 2024-02.parquet
│   └── ...
├── klines_1s/
│   ├── 2024-01.parquet       # ~550 MB
│   └── ...
├── klines_100ms_hot/
│   ├── 2024-01.parquet       # ~50 MB (yalnızca sıcak saniyeler)
│   └── ...
├── trades_hot/
│   ├── 2024-01.parquet       # Sıcak 100ms dilimleri için ham işlemler
│   └── ...
└── states_1m.parquet          # Önceden hesaplanmış kayan durum önbelleği (~112 MB)

Her dosya bir aylık veriyi kapsar. Saniye, milisaniye ve işlem verisi tembel yüklenir — yalnızca drill-down talep ettiğinde. stats.json dosyası, hacim tabanlı drill-down tetikleyicileri için kullanılan önceden hesaplanmış medyan hacimleri içerir.

Finansal Veri için Parquet Optimizasyonu

Finansal verinin belirli özellikleri vardır: zaman damgaları monoton olarak artar, fiyatlar düzgünce değişir, hacimler önemli ölçüde değişir. Optimal ayarlar:

import pyarrow as pa
import pyarrow.parquet as pq

schema = pa.schema([
    pa.field("timestamp", pa.int32()),    # Epoch'tan saniyeler — int32 yeterli
    pa.field("open",      pa.float32()),
    pa.field("high",      pa.float32()),
    pa.field("low",       pa.float32()),
    pa.field("close",     pa.float32()),
    pa.field("volume",    pa.float32()),
])

column_encodings = {
    "timestamp": "DELTA_BINARY_PACKED",   # Monoton int -> delta sıkıştırma
    "open":      "BYTE_STREAM_SPLIT",     # Float -> bayt akışı bölme
    "high":      "BYTE_STREAM_SPLIT",
    "low":       "BYTE_STREAM_SPLIT",
    "close":     "BYTE_STREAM_SPLIT",
    "volume":    "BYTE_STREAM_SPLIT",
}

def save_optimized_parquet(df, path):
    table = pa.Table.from_pandas(df, schema=schema)
    pq.write_table(
        table, path,
        compression="zstd",
        compression_level=9,
        use_dictionary=False,
        write_statistics=False,
        column_encoding=column_encodings,
    )

Bu ayarların nedeni:

  • Zaman damgaları için DELTA_BINARY_PACKED: ardışık zaman damgaları sabit bir değer kadar farklılık gösterir (1m için 60, 1s için 1). Delta kodlaması onları neredeyse sıfıra sıkıştırır.
  • Float için BYTE_STREAM_SPLIT: float32 baytlarını akışlara böler (tüm ilk baytlar bir arada, tüm ikinci baytlar bir arada, vb.). Düzgünce değişen fiyatlar için standart kodlamaya göre 2-3 kat daha iyi sıkıştırma sağlar.
  • ZSTD seviye 9: kabul edilebilir sıkıştırma açma hızıyla iyi sıkıştırma.
  • float64 yerine float32: fiyatlar ve hacimler için yeterli, %50 bellek tasarrufu sağlar.

Önbelleklemeyle Tembel Yükleme

Drill-down, belirli bir dakika için saniye verisi talep eder. Her talep için bir parquet dosyası yüklemek yavaştır. Çözüm — aya göre LRU önbelleğiyle tembel yükleme.

from functools import lru_cache
import pyarrow.parquet as pq
import pandas as pd

class AdaptiveDataLoader:
    """
    Önbellekli tembel yükleyici: saniye verisini aya göre yükler,
    son N ayı bellekte tutar.
    """
    def __init__(self, symbol: str, data_dir: str = "data", cache_months: int = 2):
        self.symbol = symbol
        self.data_dir = data_dir
        self.cache_months = cache_months
        self._cache_1s: dict[str, pd.DataFrame] = {}

    def load_1s_for_minute(self, minute_ts: pd.Timestamp) -> pd.DataFrame | None:
        """Belirli bir dakika için 1s verisini yükle."""
        month_key = minute_ts.strftime("%Y-%m")

        if month_key not in self._cache_1s:
            self._load_month_1s(month_key)

        if month_key not in self._cache_1s:
            return None

        df = self._cache_1s[month_key]
        minute_start = minute_ts.floor('1min')
        minute_end = minute_start + pd.Timedelta(minutes=1)

        return df[(df.index >= minute_start) & (df.index < minute_end)]

    def load_100ms_for_second(self, second_ts: pd.Timestamp) -> pd.DataFrame | None:
        """Sıcak bir saniye için 100ms verisi yükle."""
        month_key = second_ts.strftime("%Y-%m")
        path = f"{self.data_dir}/{self.symbol}/klines_100ms_hot/{month_key}.parquet"

        try:
            df = pd.read_parquet(path)
            second_start = second_ts.floor('1s')
            second_end = second_start + pd.Timedelta(seconds=1)
            return df[(df.index >= second_start) & (df.index < second_end)]
        except FileNotFoundError:
            return None

    def _load_month_1s(self, month_key: str):
        """1s verisinin bir ayını yükle, önbellekten eski veriyi çıkar."""
        path = f"{self.data_dir}/{self.symbol}/klines_1s/{month_key}.parquet"
        try:
            df = pd.read_parquet(path)
            df.index = pd.to_datetime(df['timestamp'], unit='s')

            if len(self._cache_1s) >= self.cache_months:
                oldest = min(self._cache_1s.keys())
                del self._cache_1s[oldest]

            self._cache_1s[month_key] = df
        except FileNotFoundError:
            pass

Drill-Down'ın Backtesting'e Uygulanması

Backtest döngüsüne entegrasyon:

def backtest_with_adaptive_fill(
    states: pd.DataFrame,
    strategy_params: dict,
    data_loader: AdaptiveDataLoader,
) -> list:
    """
    Emir gerçekleşme simülasyonu için uyarlanabilir drill-down ile backtest.
    """
    fill_sim = AdaptiveFillSimulator(data_loader)
    trades = []
    position = None

    for i in range(len(states)):
        row = states.iloc[i]
        ts = states.index[i]

        candle_1m = {
            'open': row['open'], 'high': row['high'],
            'low': row['low'], 'close': row['close'],
            'timestamp': ts,
        }

        if position is not None:
            fill = fill_sim.check_fill(
                ts, candle_1m,
                position['sl'], position['tp'],
                position['side'],
            )

            if fill is not None:
                fill_type, fill_price = fill
                trades.append({
                    'entry_time': position['entry_time'],
                    'exit_time': ts,
                    'side': position['side'],
                    'entry_price': position['entry_price'],
                    'exit_price': fill_price,
                    'exit_type': fill_type,
                    'drill_down': fill_sim.last_drill_depth,  # 0, 1 veya 2
                })
                position = None
                continue

        signal = check_entry_signal(row, strategy_params)
        if signal and position is None:
            position = {
                'side': signal['side'],
                'entry_price': row['close'],
                'entry_time': ts,
                'sl': signal['sl'],
                'tp': signal['tp'],
            }

    return trades

Kayan Durum Önbelleğiyle İlişki

Drill-down, toplanmış parquet önbelleğini tamamlar — farklı sorunları çözerler:

Kayan durum önbelleği Uyarlanabilir drill-down
Amaç Doğru HTF gösterge değerleri Kesin SL/TP gerçekleşme sırası
Çalıştığı yer Her 1m mum çubuğu Yalnızca emir gerçekleşme belirsizliğinde (~%5)
Veri Önceden hesaplanmış, kalıcı depolanmış Tembel yüklenen, son ayların önbelleği
Etkiler Giriş/çıkış sinyalleri Gerçekleşme fiyatı ve zamanı

Her iki yaklaşım da günlük mum çubuğu düzeyinde görünmez olan ancak gerçekçi backtesting için kritik olan hataları ortadan kaldırır.

Özet: Emir Gerçekleşme Simülasyonu Yaklaşımı Karşılaştırması

Yaklaşım Doğruluk Hız Depolama
OHLC sezgisel (iyimser/kötümser) Düşük Anlık Yalnızca 1m
Tam 1s backtesti Yüksek Yavaş (x60) ~550 MB/ay
Tam 100ms backtesti Çok yüksek Çok yavaş (x600) ~5 GB/ay
Tam ham işlem backtesti Maksimum Son derece yavaş ~50 GB/ay
Uyarlanabilir drill-down (4 seviyeli) Maksimum ~Anlık 1m + 1s + 100ms sıcak + işlemler sıcak

Drill-down, tam 1s backtest doğruluğunu 1m backtest hızında sağlar. Temel gözlem: yüksek granülerlik her yerde değil — yalnızca karar noktalarında gereklidir.

Daha ince granülerlik seviyelerine drill-down'ı tetikleyen hacim ani yükselişleri

Hacim Tabanlı Drill-Down

Orijinal drill-down yalnızca fiyat hareketini tetikler — bir mum çubuğunun [low, high] aralığı emir gerçekleşme belirsizliği yaratacak kadar genişse. Ancak fiyat, bir çubuk içinde ilginç bir şeyler olduğunun tek sinyali değildir.

Hacim ani yükselişleri eşit derecede önemli bir tetikleyicidir. Hacmin medyanın 500 katı olduğu bir saniye genellikle büyük bir piyasa emrine, tasfiye zincirine veya ani çöküşe karşılık gelir. Mum gövdesi küçük görünse bile, o saniye içindeki gerçek fiyat yolu vahşi olmuş olabilir — OHLC gösteriminin gizlediği aşırı uçlara dokunmuş olabilir.

Drill-down koşulu artık VEYA tabanlı: önemli bir fiyat hareketi VEYA anormal bir hacim ani yükselişi daha ince granülerliğe inişi tetikler.

def is_hot(bar, median_volume, min_pct=0.1, vol_mult=500):
    """
    Bir çubuğun bir sonraki seviyeye drill-down'ı hak edip etmediğini belirler.
    İki bağımsız tetikleyici (VEYA mantığı):
      - fiyat çubuk içinde >= min_pct hareket etti
      - hacim medyan * vol_mult'u aştı
    """
    price_move = (bar['high'] - bar['low']) / bar['open'] * 100
    return price_move >= min_pct or bar['volume'] >= median_volume * vol_mult

Bu, yalnızca fiyat tespitinde görünmez olan senaryoları yakalar: open=3000, close=3001 ama hacmi normun 50.000 katı olan bir çubuk, milisaniyeler içinde kısaca 2950 ve 3050'ye dokunmuş olabilir. Hacim tabanlı drill-down olmadan, backtest bu saniyeyi hiçbir zaman daha yakından incelemezdi.

Ham İşlemler: Dördüncü Seviye

Orijinal üç seviyeli hiyerarşi (1m -> 1s -> 100ms) hâlâ bir boşluk bırakır: tek bir 100ms dilimi içinde, birden fazla işlem farklı fiyatlarda gerçekleşebilir. high=3060 ve low=2965 olan bir dilim için, hâlâ tam sırayı bilmiyoruz.

Çözüm: dördüncü ve son seviye olarak ham işlemlere drill-down.

1m mum çubukları (taban)
  └─> 1s mum çubukları    (1s price_move >= min_pct VEYA volume >= median_1s * vol_mult gösterdiğinde)
      └─> 100ms mum çubukları  (sıcak saniye tespit edildiğinde)
          └─> Ham işlemler     (100ms price_move >= min_pct VEYA volume >= median_100ms * vol_mult gösterdiğinde)

Ham işlemler düzeyinde belirsizlik yoktur — her işlemin tam fiyatı ve zaman damgası vardır. Emir gerçekleşmesi kesin olarak çözülür:

def resolve_from_trades(trades, sl_price, tp_price, side):
    """
    Tekil işlemleri kronolojik sırayla geç.
    SL veya TP'yi geçen ilk işlem gerçekleşmeyi belirler.
    """
    for trade in trades:
        price = trade['price']
        if side == 'long':
            if price <= sl_price:
                return ('sl', price)
            if price >= tp_price:
                return ('tp', price)
        else:  # short
            if price >= sl_price:
                return ('sl', price)
            if price <= tp_price:
                return ('tp', price)
    return None

Ham işlemler düzeyi son derece nadir çağrılır — tüm çubukların %0,1'inden az — ancak çağrıldığında, hiçbir mum tabanlı yaklaşımın eşleşemeyeceği temel gerçeği sağlar.

Geçiş Başına Ayrı Eşikler

Farklı çözünürlük geçişlerinin farklı özellikleri vardır. Bir saniye içindeki %0,1'lik fiyat hareketi önemlidir; aynı %0,1'lik hareket 100ms diliminde aşırıdır. Benzer şekilde, hacim dağılımları her zaman ölçeğinde farklılık gösterir.

Her seviye geçişinin artık kendi min_pct ve vol_mult parametreleri vardır:

1s → 100ms:   --min-pct-1s 0.1   --vol-mult-1s 500
100ms → işlemler: --min-pct-100ms 0.1 --vol-mult-100ms 500

Bu, her geçişin hassasiyetini bağımsız olarak ince ayar yapma imkânı tanır. Pratikte, tek bir 100ms dilimi için ham işlem yüklemenin maliyeti minimum olduğundan, 100ms'den işlemlere geçiş daha sıkı bir eşik kullanabilir.

@dataclass
class DrillDownConfig:
    min_pct_1s: float = 0.1
    vol_mult_1s: float = 500
    min_pct_100ms: float = 0.1
    vol_mult_100ms: float = 500

Kalıcı Medyan İstatistikleri

Hacim tabanlı drill-down, her zaman ölçeğinde medyan hacmi bilmeyi gerektirir. Her backtest için anlık medyan hesaplamak performans avantajlarını ortadan kaldırır. Çözüm: medyanları bir kez önceden hesapla ve önbelleğe al.

Her sembol için, 1s ve 100ms granülerliğindeki medyan hacimler tarihsel verilerden hesaplanır ve bir stats.json dosyasında depolanır:

{
  "ETHUSDT": {
    "median_volume_1s": 12.5,
    "median_volume_100ms": 1.8
  },
  "BTCUSDT": {
    "median_volume_1s": 0.45,
    "median_volume_100ms": 0.06
  }
}

İstatistikler veri ilk indirildiğinde sembol başına bir kez hesaplanır ve sonraki tüm backtestlerde yeniden kullanılır. Veri güncellenirse (yeni aylar indirilirse), istatistikler artımlı olarak yeniden hesaplanır.

def compute_median_stats(symbol, data_dir):
    """Bir sembol için medyan hacim istatistiklerini hesapla ve önbelleğe al."""
    stats_path = f"{data_dir}/{symbol}/stats.json"

    all_1s = load_all_months(f"{data_dir}/{symbol}/klines_1s/")
    median_1s = all_1s['volume'].median()

    all_100ms = load_all_months(f"{data_dir}/{symbol}/klines_100ms_hot/")
    median_100ms = all_100ms['volume'].median()

    stats = {
        "median_volume_1s": float(median_1s),
        "median_volume_100ms": float(median_100ms),
    }

    with open(stats_path, 'w') as f:
        json.dump(stats, f, indent=2)

    return stats

Çok borsalı veri akışı: Binance ve Bybit birleşik granülerlik katmanlarına yakınsıyor

Çok Borsa Desteği: Bybit

Tüm semboller Binance'te mevcut değildir. XAUTUSDT (altın) gibi varlıklar için veri başka borsalardan gelmek zorundadır. Drill-down sistemi artık alternatif bir veri kaynağı olarak Bybit'i desteklemektedir.

Bybit sembolleri için, tüm mum düzeyleri (1m, 1s, 100ms) ve ham işlemler Bybit'in ham işlem akışından oluşturulur. Süreç aynıdır — ham işlemler her zaman ölçeğinde mum çubuklarına toplanır — ancak veri kaynağı farklıdır.

data/{SYMBOL}/
├── source.json              # {"exchange": "bybit"} veya {"exchange": "binance"}
├── klines_1m/
│   └── ...
├── klines_1s/
│   └── ...
├── klines_100ms_hot/
│   └── ...
└── trades_hot/              # Sıcak 100ms dilimleri için ham işlemler
    └── ...

Veri yükleyici source.json'u kontrol eder ve uygun indirme pipeline'ını kullanır. Backtest motorunun bakış açısından, kaynak borsadan bağımsız olarak veri formatı aynıdır — drill-down mantığı borsadan bağımsızdır.

Bu, çapraz borsa stratejileri veya yalnızca belirli platformlarda işlem gören semboller için özellikle önemlidir.

Sonuç

Uyarlanabilir drill-down, basit bir ilkenin uygulamasıdır: hesaplama kaynakları ve depolamayı veri önemine orantılı olarak harca.

Dört granülerlik seviyesi:

  1. 1m — çubukların %95'i için temel geçiş
  2. 1s — emir gerçekleşme belirsizliğinde veya hacim ani yükselişlerinde drill-down
  3. 100ms — aşırı hareket veya anormal hacimli sıcak saniyeler için drill-down
  4. Ham işlemler — sıcak 100ms dilimleri için drill-down, tekil işlem düzeyinde gerçekleşmeleri çözme

Dört depolama seviyesi:

  1. Tüm 1m — eksiksiz arşiv, 2 yıl için ~15 MB
  2. Tüm 1s — eksiksiz veya uyarlanabilir arşiv, ~550 MB/ay
  3. Yalnızca sıcak 100ms — saniyelerin <%1'i, ~50 MB/ay
  4. Yalnızca sıcak işlemler — en aşırı 100ms dilimleri için ham işlemler

İki drill-down tetikleyicisi (VEYA mantığı):

  • Fiyat tabanlı: çubuğun fiyat aralığı min_pct'i aşıyor
  • Hacim tabanlı: çubuğun hacmi medyan * vol_mult'u aşıyor

Sonuç: dakika düzeyinde hızda tick simülatörü doğruluğuyla backtest. Üstel değil doğrusal büyüyen depolama. Ve Binance ve Bybit — borsadan bağımsız drill-down mantığıyla çoklu borsa desteği.

Çoklu zaman dilimi stratejileri için önceden hesaplanmış önbellek hakkında daha fazla bilgi için Toplanmış Parquet Önbelleği makalesine bakın. Yüksek kaldıraçla sonuçlar üzerindeki fonlama oranı etkisi için — Fonlama oranları kaldıracınızı öldürür.


Faydalı Bağlantılar

  1. Apache Parquet — veri depolama formatı
  2. Apache Arrow — BYTE_STREAM_SPLIT kodlaması
  3. Zstandard — sıkıştırma algoritması
  4. Lopez de Prado — Finansal Makine Öğrenmesinde Gelişmeler
  5. Binance — Tarihsel Piyasa Verisi

Atıf

@article{soloviov2026adaptivedrilldown,
  author = {Soloviov, Eugen},
  title = {Adaptive Drill-Down: Backtest with Variable Granularity from Minutes to Raw Trades},
  year = {2026},
  url = {https://marketmaker.cc/tr/blog/post/adaptive-resolution-drill-down-backtest},
  description = {Uyarlanabilir veri granülerliğinin backtestleri nasıl hızlandırdığı ve depolama alanını nasıl tasarruf ettirdiği: yalnızca fiyatın önemli ölçüde hareket ettiği veya hacmin ani yükseldiği yerlerde 1m'den 1s, 100ms ve ham işlemlere drill-down.}
}
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.