← Maqolalarga qaytish
March 16, 2026
5 daqiqa o'qish

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

Yig'ilgan Parquet Kesh: Ko'p Vaqt Oralig'idagi Backtestlarni Yuzlab Marta Tezlashtirish
#algotrading
#backtest
#multi-timeframe
#parquet
#optimization
#caching
Part 3 of 10 · Collection
High-Performance Backtest Engines

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:
    df_1m = load_candles("ETHUSDT", "1m", start, end)

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

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

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

        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:

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

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

cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")

for params in parameter_grid:
    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
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")

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 Yondashuv Yig'ilgan Kesh
Vaqt oralig'ini qayta namunalash Har bir iteratsiya Bir marta
Indikatorni hisoblash Har bir iteratsiya Bir marta
Iteratsiya boshiga sarflanadigan vaqt Daqiqalar Bir soniyadan kam
1000 iteratsiya Kunlar Daqiqalar
Xotira sarfi 1m yuklash + qayta hisoblash Yagona DataFrame
Backtest-live moslik Amalga oshirishga bog'liq Kafolatlangan (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.}
}
blog.disclaimer

Authors

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

Bozordan bir qadam oldinda bo'ling

Sun'iy intellekt savdo tahlillari, bozor tahlili va platforma yangiliklari uchun bizning xabarnomaga obuna bo'ling.

Biz sizning maxfiyligingizni hurmat qilamiz. Istalgan vaqtda obunadan chiqishingiz mumkin.