Yig'ilgan Parquet Kesh: Ko'p Vaqt Oralig'idagi Backtestlarni Yuzlab Marta Tezlashtirish

Daqiqalik sham (candle) ma'lumotlaridan vaqt oraliqlari va indikatorlarni oldindan qanday hisoblash, ularni parquet formatida saqlash va ortiqcha qayta hisoblashlarsiz strategiyalarni ommaviy sinovdan o'tkazish uchun qanday foydalanish.

Ko'p vaqt oralig'idagi (multi-timeframe) strategiya bir vaqtning o'zida bir nechta vaqt oralig'idan foydalanadi: kunlik trend yo'nalishini belgilaydi, soatlik kirish nuqtalarini aniqlaydi, 5 daqiqalik esa bajarish vaqtini aniq belgilaydi. Har bir vaqt oralig'i o'z indikatorlarini talab qiladi: harakatlanuvchi o'rtachalar, ostsillyatorlar, darajalar.

Yagona backtest uchun hamma narsa oddiy — daqiqalik ma'lumotlardan vaqt oraliqlarini qayta hisoblash, indikatorlarni hisoblash, strategiyani ishga tushirish. Ammo ommaviy optimallashtirish paytida — parametrlarning minglab kombinatsiyalarini sinash kerak bo'lganda — har bir iteratsiyada vaqt oraliqlari va indikatorlarni qayta hisoblash tor joyga aylanadi. Ikki yillik daqiqalik ma'lumotlar bo'yicha bitta o'tish million martadan ortiq barni qayta ishlashni anglatadi, buni ming marta takrorlash esa isrofgarchilikdir.

Yechim: hamma narsani bir marta oldindan hisoblab, parquet faylida keshlash.

Muammo: Optimallashtirish Paytidagi Ortiqcha Hisob-kitoblar

Odatiy ko'p vaqt oralig'idagi backtest quvur liniyasi (pipeline) quyidagicha ko'rinadi:

for params in parameter_grid:
    # 1. Load minute candles
    df_1m = load_candles("ETHUSDT", "1m", start, end)

    # 2. Resample to higher timeframes
    df_5m = resample_ohlcv(df_1m, "5m")
    df_1h = resample_ohlcv(df_1m, "1h")
    df_4h = resample_ohlcv(df_1m, "4h")
    df_1d = resample_ohlcv(df_1m, "D")

    # 3. Compute indicators for each timeframe
    ma_1h = compute_ma(df_1h["close"], length=params["ma_1h_len"])
    ma_4h = compute_ma(df_4h["close"], length=params["ma_4h_len"])
    ma_1d = compute_ma(df_1d["close"], length=params["ma_1d_len"])

    # 4. Run the strategy
    result = run_strategy(df_1m, ma_1h, ma_4h, ma_1d, params)

Har bir iteratsiyada, ma'lumotlar bir xil bo'lsa-da, 1-3-qadamlar qayta hisoblanadi. Faqat strategiyaning chegara parametrlari o'zgaradi (4-qadam). Bu devor rangini o'zgartirmoqchi bo'lganingizda har safar butun uyni qayta qurishga o'xshaydi.

G'oya: Bir Marta Hisoblash, Saqlash, Ko'p Marta Qayta Foydalanish

Asosiy kuzatuv: vaqt oraliqlari va indikatorlar faqat daqiqalik ma'lumotlarga va indikator parametrlariga bog'liq, strategiya parametrlariga emas. Agar kerakli indikatorlar to'plamini belgilasak, ularni bir marta hisoblab saqlashimiz mumkin.

Sxema:

Step 1 (once):
  Minute candles -> Timeframe resampling -> Indicator computation -> Parquet file

Step 2 (many times):
  Parquet file -> Strategy with different parameters -> Result

Daqiqalik Shamlardan Vaqt Oraliqlarini Emulyatsiya Qilish

Real-time timeframe emulation visualization

Bizda daqiqalik shamlarning to'liq arxivi mavjud. Undan biz istalgan yuqori vaqt oralig'ini aniq tiklashimiz mumkin. Ammo bir nozik jihat bor: standart resample bilan biz har davr uchun bitta qator olamiz (har soat uchun bitta qator, har 4 soat uchun bitta va h.k.). Bu daqiqama-daqiqa backtesting uchun ishlamaydi — bizga indikator qiymatini har daqiqada bilish kerak.

Shu sababli biz botning ma'lumotlarni real vaqtda qanday ko'rishini modellashtirib, har bir daqiqalik sham uchun yuqori vaqt oralig'i qiymatlarini emulyatsiya qilamiz:

  1. Bot navbatdagi daqiqalik shamni oladi
  2. Yuqori vaqt oralig'ining joriy (yopilmagan) barini yangilaydi — High, Low, Close, Volume qayta hisoblanadi
  3. Barcha yopilgan barlar va joriy qisman bar bo'yicha indikatorni qayta hisoblaydi
  4. Davr tugaganda — bar yakunlanadi va yangisi boshlanadi

Bu yondashuv backtest bot real vaqtda ko'radigan xuddi shu ma'lumotlarni ko'rishini kafolatlaydi. Kelajakka qarash yo'q — har bir daqiqalik sham o'sha paytda mavjud bo'lgan ma'lumotlar bilan qat'iy tarzda qayta ishlanadi.

class RunningCandleBuffer:
    """
    Emulates real-time updates of a higher timeframe bar
    using 1-minute candles.
    """
    def __init__(self, period_seconds: int):
        self.period = period_seconds  # 86400 for Daily, 3600 for 1h
        self.closed_bars = []
        self.current_bar = None

    def update(self, timestamp, open_, high, low, close, volume):
        bar_start = self._align_to_period(timestamp)

        if self.current_bar is None or bar_start != self.current_bar['start']:
            # New period — finalize the previous bar
            if self.current_bar is not None:
                self.closed_bars.append(self.current_bar)
            self.current_bar = {
                'start': bar_start,
                'open': open_, 'high': high,
                'low': low, 'close': close,
                'volume': volume,
            }
        else:
            # Update the current (unclosed) bar
            self.current_bar['high'] = max(self.current_bar['high'], high)
            self.current_bar['low'] = min(self.current_bar['low'], low)
            self.current_bar['close'] = close
            self.current_bar['volume'] += volume

        # All closed bars + current running bar
        return self.closed_bars + [self.current_bar]

Har bir yuqori vaqt oralig'i uchun alohida RunningCandleBuffer yaratiladi. Har bir daqiqalik shamda barcha buferlar yangilanadi, bu bizga har bir vaqt oralig'ining joriy holatini beradi — xuddi bot real vaqtda ishlayotgandek.

Parquet Keshining Tuzilishi

Oldindan hisoblashning natijasi — har bir qator bitta daqiqalik shamga mos keladigan yagona parquet fayli bo'lib, ustunlarda quyidagilar mavjud:

timestamp              — minute candle timestamp
open, high, low,       — minute candle OHLCV
close, volume

close_5m               — Close of the emulated 5m candle at this moment
close_1h               — Close of the emulated 1h candle
close_4h               — Close of the emulated 4h candle
close_1d               — Close of the emulated daily candle

ma_20_1h               — MA(20) on 1h, recalculated at this minute
ma_50_1h               — MA(50) on 1h
ma_20_4h               — MA(20) on 4h
ma_50_4h               — MA(50) on 4h
ma_6_1d                — MA(6) on Daily
ma_12_1d               — MA(12) on Daily

cross_ma_1h            — MA crossover signal on 1h ('buy'/'sell'/None)
cross_ma_4h            — MA crossover signal on 4h
cross_ma_1d            — MA crossover signal on Daily

separation_1h          — MA divergence in % on 1h
separation_4h          — MA divergence in % on 4h
separation_1d          — MA divergence in % on Daily

Har bir qiymat tegishli daqiqalik sham paytidagi indikatorning haqiqiy holatini aks ettiradi — yuqori vaqt oraliqlarining yopilmagan barlarini hisobga olgan holda.

Precompute: Keshni Qurish

def precompute_cache(
    df_1m: pd.DataFrame,
    timeframes: dict[str, int],   # {"5m": 300, "1h": 3600, "4h": 14400, "D": 86400}
    indicators: dict,              # {"ma_20": 20, "ma_50": 50}
) -> pd.DataFrame:
    """
    Single pass through all minute candles.
    Returns a DataFrame with emulated timeframes and indicators.
    """
    # Create buffers for each timeframe
    buffers = {tf: RunningCandleBuffer(secs) for tf, secs in timeframes.items()}

    n = len(df_1m)
    result = {}

    for tf_name, buf in buffers.items():
        closes = np.zeros(n)
        ma_values = {name: np.full(n, np.nan) for name in indicators}

        for i in range(n):
            row = df_1m.iloc[i]
            bars = buf.update(
                df_1m.index[i],
                row['open'], row['high'], row['low'], row['close'], row['volume']
            )

            all_closes = [b['close'] for b in bars]
            closes[i] = all_closes[-1]

            # Compute indicators from emulated bars
            for ind_name, length in indicators.items():
                if len(all_closes) >= length:
                    ma_values[ind_name][i] = np.mean(all_closes[-length:])

        result[f'close_{tf_name}'] = closes
        for ind_name in indicators:
            result[f'{ind_name}_{tf_name}'] = ma_values[ind_name]

    cache_df = pd.DataFrame(result, index=df_1m.index)
    cache_df = pd.concat([df_1m[['open', 'high', 'low', 'close', 'volume']], cache_df], axis=1)

    return cache_df
# Build and save the cache
cache = precompute_cache(
    df_1m,
    timeframes={"5m": 300, "1h": 3600, "4h": 14400, "D": 86400},
    indicators={"ma_20": 20, "ma_50": 50, "ma_6": 6, "ma_12": 12},
)

cache.to_parquet("cache_ETHUSDT_2024_2026.parquet")

Optimallashtirish Paytida Keshdan Foydalanish

Cache-based optimization speedup comparison

Endi optimallashtirish quyidagicha ko'rinadi:

# Load the cache once
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")

for params in parameter_grid:
    # No timeframe or indicator recalculations!
    # All data is already in the cache — only strategy logic remains
    result = run_strategy(cache, params)

Strategiya oldindan tayyorlangan ustunlar bilan ishlaydi — million barlarni takroran aylanib chiqish yo'q, harakatlanuvchi o'rtachalarni qayta hisoblash yo'q, vaqt oralig'ini emulyatsiya qilish yo'q. Faqat DataFrame'dan o'qish va kirish/chiqish shartlarini tekshirish.

Nega Aynan Parquet

Parquet — bu vazifa uchun optimal bo'lgan ustunli ma'lumotlarni saqlash formati:

  • Siqish. Parquet raqamli ma'lumotlarni 5-10 marta siqadi. 1,1 million qator va 30 ustunli kesh CSV formatidagi ~500 MB o'rniga ~50 MB egallaydi.
  • Ustunli o'qish. Agar strategiya faqat ma_20_4h va ma_50_4hdan foydalansa, parquet faqat shu ustunlarni o'qib, qolganini o'tkazib yuboradi.
  • Turlarni saqlash. Ma'lumot turlari (float64, int64, string) yo'qotishsiz saqlanadi — yuklashda satrlarni tahlil qilish (parse) shart emas.
  • O'qish tezligi. Parquetni pandas'ga yuklash o'nlab millisekund vaqt oladi, bu CSV'dan bir tartibga tezroq.

Keshni Kengaytirish: Yangi Indikatorlar Qo'shish

Agar strategiyaga yangi indikator kerak bo'lsa (RSI, MACD, Bollinger tasmalari), shunchaki:

  1. O'sha daqiqalik ma'lumotlardan faqat yangi indikatorni qayta hisoblang
  2. Ustunlarni mavjud parquet fayliga qo'shing
  3. Oldin hisoblangan barcha ustunlar o'zgarishsiz qoladi
# Add RSI to the existing cache
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")

# Compute only RSI (requires a pass through 1m data)
rsi_cols = compute_rsi_for_timeframes(df_1m, timeframes, length=14)

cache = pd.concat([cache, rsi_cols], axis=1)
cache.to_parquet("cache_ETHUSDT_2024_2026.parquet")

Xulosa: Yondashuvlarni Taqqoslash

Sodda YondashuvYig'ilgan Kesh
Vaqt oralig'ini qayta namunalashHar bir iteratsiyaBir marta
Indikatorni hisoblashHar bir iteratsiyaBir marta
Iteratsiya boshiga sarflanadigan vaqtDaqiqalarBir soniyadan kam
1000 iteratsiyaKunlarDaqiqalar
Xotira sarfi1m yuklash + qayta hisoblashYagona DataFrame
Backtest-live moslikAmalga oshirishga bog'liqKafolatlangan (emulyatsiya = real vaqt)

Xulosa

Yig'ilgan parquet kesh yondashuvi ikkita muammoni bir vaqtning o'zida hal qiladi:

  1. To'g'rilik. RunningCandleBuffer orqali daqiqalik shamlardan vaqt oraliqlarini emulyatsiya qilish backtest bot real vaqtda ko'radigan xuddi shu ma'lumotlarni ko'rishini kafolatlaydi — kelajakka qarash va sun'iy kechikishlar yo'q.

  2. Tezlik. Oldindan hisoblangan vaqt oraliqlari va indikatorlar parametrlarning minglab kombinatsiyalarini kunlar o'rniga daqiqalarda sinash imkonini beradi.

G'oya oddiy: bir marta hisoblash — ko'p marta qayta foydalanish. Daqiqalik shamlar manba ma'lumotdir. Qolgan hamma narsa hosila bo'lib, oldindan hisoblanishi va keshlanishi mumkin. Parquet bu keshni ixcham, tez va qulay qiladi.

Bajarilishni simulyatsiya qilish aniqligini daqiqalardan soniya va millisekundlargacha moslashuvchan chuqurlashtirish (drill-down) orqali qanday yaxshilash mumkinligi haqida ko'proq ma'lumot uchun Moslashuvchan chuqurlashtirish: o'zgaruvchan granulyarlikdagi backtest maqolasiga qarang.


Foydali Havolalar

  1. Apache Parquet — data storage format
  2. pandas — working with parquet
  3. Lopez de Prado — Advances in Financial Machine Learning
  4. Ernest Chan — Quantitative Trading

Iqtibos

@article{soloviov2026parquetcache,
  author = {Soloviov, Eugen},
  title = {Aggregated Parquet Cache: How to Speed Up Multi-Timeframe Backtests by Hundreds of Times},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/parquet-cache-multitimeframe-backtest},
  description = {How to precompute timeframes and indicators from minute candles, save them to parquet, and use them for mass strategy testing without redundant recalculations.}
}