← लेखों की सूची पर वापस जाएँ
March 16, 2026
5 मिनट का पठन

एग्रीगेटेड पार्क्वेट कैश: मल्टी-टाइमफ्रेम बैकटेस्ट को सैकड़ों गुना तेज़ कैसे करें

एग्रीगेटेड पार्क्वेट कैश: मल्टी-टाइमफ्रेम बैकटेस्ट को सैकड़ों गुना तेज़ कैसे करें
#algotrading
#backtest
#multi-timeframe
#parquet
#optimization
#caching
Part 3 of 10 · Collection
High-Performance Backtest Engines

एक मल्टी-टाइमफ्रेम रणनीति एक साथ कई टाइमफ्रेम का उपयोग करती है: डेली ट्रेंड की दिशा तय करता है, ऑवरली एंट्री पॉइंट्स की पहचान करता है, और 5-मिनट एग्ज़िक्यूशन का सटीक समय निर्धारित करता है। हर टाइमफ्रेम को अपने खुद के इंडिकेटर चाहिए होते हैं: मूविंग एवरेज, ऑसिलेटर, लेवल।

एक अकेले बैकटेस्ट के लिए सब कुछ सीधा है — मिनट डेटा से टाइमफ्रेम को दोबारा गणना करना, इंडिकेटर कैलकुलेट करना, रणनीति चलाना। लेकिन मास ऑप्टिमाइज़ेशन के दौरान — जब हज़ारों पैरामीटर कॉम्बिनेशन का परीक्षण करना हो — हर इटरेशन पर टाइमफ्रेम और इंडिकेटर को दोबारा गणना करना एक बॉटलनेक बन जाता है। दो साल के मिनट डेटा से एक भी पास करने में दस लाख से अधिक बार को प्रोसेस करना पड़ता है, और इसे हज़ार बार दोहराना बर्बादी है।

समाधान: सब कुछ एक बार पहले से गणना करें और इसे parquet फ़ाइल में कैश करें

समस्या: ऑप्टिमाइज़ेशन के दौरान दोहराई गई गणनाएँ

एक विशिष्ट मल्टी-टाइमफ्रेम बैकटेस्ट पाइपलाइन इस तरह दिखती है:

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)

हर इटरेशन पर, स्टेप्स 1-3 को दोबारा गणना किया जाता है भले ही डेटा वही हो। केवल रणनीति के थ्रेशोल्ड पैरामीटर बदलते हैं (स्टेप 4)। यह वैसा ही है जैसे हर बार सिर्फ दीवार का रंग बदलने के लिए पूरा घर फिर से बनाना।

विचार: एक बार गणना करें, सेव करें, कई बार पुनः उपयोग करें

मुख्य अवलोकन: टाइमफ्रेम और इंडिकेटर केवल मिनट डेटा और इंडिकेटर पैरामीटर पर निर्भर करते हैं, रणनीति के पैरामीटर पर नहीं। यदि हम आवश्यक इंडिकेटर के सेट को तय कर दें, तो हम उन्हें एक बार गणना करके सेव कर सकते हैं।

योजना:

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

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

मिनट कैंडल से टाइमफ्रेम का एमुलेशन

Real-time timeframe emulation visualization

हमारे पास मिनट कैंडल का एक पूरा आर्काइव है। इससे हम किसी भी उच्च टाइमफ्रेम को सटीकता से पुनः बना सकते हैं। लेकिन एक बारीकी है: एक स्टैंडर्ड resample के साथ, हमें प्रति पीरियड एक रो मिलती है (प्रति घंटे एक रो, प्रति 4 घंटे एक, आदि)। यह मिनट-दर-मिनट बैकटेस्टिंग के लिए काम नहीं करता — हमें हर मिनट पर इंडिकेटर वैल्यू जानने की ज़रूरत होती है।

इसलिए हम हर मिनट कैंडल के लिए उच्च टाइमफ्रेम वैल्यू को एमुलेट करते हैं, यह मॉडलिंग करते हुए कि बॉट रियल टाइम में डेटा कैसे देखता है:

  1. बॉट अगली मिनट कैंडल प्राप्त करता है
  2. उच्च टाइमफ्रेम के मौजूदा (अनक्लोज़्ड) बार को अपडेट करता है — High, Low, Close, Volume को दोबारा गणना करता है
  3. सभी क्लोज़्ड बार और मौजूदा आंशिक बार पर इंडिकेटर को दोबारा गणना करता है
  4. जब पीरियड समाप्त होता है — बार फाइनल हो जाता है और एक नया शुरू होता है

यह तरीका गारंटी देता है कि बैकटेस्ट बिल्कुल वही डेटा देखता है जो बॉट रियल टाइम में देखता है। भविष्य में झांकना नहीं — हर मिनट कैंडल को उन्हीं डेटा के साथ सख़्ती से प्रोसेस किया जाता है जो उस पल पर उपलब्ध होते।

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]

हर उच्च टाइमफ्रेम के लिए एक अलग RunningCandleBuffer बनाया जाता है। हर मिनट कैंडल पर, सभी बफर अपडेट होते हैं, जिससे हमें हर टाइमफ्रेम की मौजूदा स्थिति मिलती है — जैसे बॉट रियल टाइम में चल रहा हो।

पार्क्वेट कैश की संरचना

पूर्व-गणना का परिणाम एक ही parquet फ़ाइल है जहाँ हर रो एक मिनट कैंडल के अनुरूप होती है, और कॉलम में शामिल हैं:

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

हर वैल्यू संबंधित मिनट कैंडल के पल पर इंडिकेटर की वास्तविक स्थिति को दर्शाती है — उच्च टाइमफ्रेम के अनक्लोज़्ड बार को ध्यान में रखते हुए।

Precompute: कैश बनाना

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

ऑप्टिमाइज़ेशन के दौरान कैश का उपयोग

Cache-based optimization speedup comparison

अब ऑप्टिमाइज़ेशन इस तरह दिखता है:

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

for params in parameter_grid:
    result = run_strategy(cache, params)

रणनीति पहले से बने कॉलम के साथ काम करती है — दस लाख बार पर बार-बार पास करने की ज़रूरत नहीं, MA की दोबारा गणना नहीं, टाइमफ्रेम एमुलेशन नहीं। बस DataFrame से पढ़ना और एंट्री/एग्ज़िट कंडीशन की जांच करना।

Parquet क्यों

Parquet एक कॉलमनार डेटा स्टोरेज फॉर्मेट है, जो इस काम के लिए बेहतरीन है:

  • कम्प्रेशन। Parquet न्यूमेरिकल डेटा को 5-10 गुना कम्प्रेस करता है। 1.1 मिलियन रो और 30 कॉलम वाला कैश CSV में ~500 MB के बजाय ~50 MB लेता है।
  • कॉलमनार रीडिंग। अगर रणनीति केवल ma_20_4h और ma_50_4h का उपयोग करती है, तो parquet केवल उन्हीं कॉलम को पढ़ता है, बाकी को छोड़ते हुए।
  • टाइप संरक्षण। डेटा टाइप्स (float64, int64, string) बिना किसी नुकसान के संरक्षित रहते हैं — लोड करते समय स्ट्रिंग पार्स करने की ज़रूरत नहीं होती।
  • पढ़ने की गति। pandas में parquet को लोड करने में कुछ दस मिलीसेकंड लगते हैं, जो CSV से एक ऑर्डर ऑफ़ मैग्निट्यूड तेज़ है।

कैश को बढ़ाना: नए इंडिकेटर जोड़ना

अगर रणनीति को किसी नए इंडिकेटर की ज़रूरत है (RSI, MACD, Bollinger Bands), तो बस:

  1. उसी मिनट डेटा से केवल नए इंडिकेटर को दोबारा गणना करें
  2. मौजूदा parquet फ़ाइल में कॉलम जोड़ें
  3. पहले से गणना किए गए सभी कॉलम बिना छेड़े रहते हैं
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")

सारांश: दृष्टिकोणों की तुलना

नैव दृष्टिकोण एग्रीगेटेड कैश
टाइमफ्रेम रिसैंपलिंग हर इटरेशन एक बार
इंडिकेटर गणना हर इटरेशन एक बार
प्रति इटरेशन समय मिनट एक सेकंड से कम
1000 इटरेशन दिन मिनट
मेमोरी खपत 1m लोड करना + दोबारा गणना एक ही DataFrame
बैकटेस्ट-लाइव पैरिटी इम्प्लीमेंटेशन पर निर्भर गारंटीड (एमुलेशन = रियल टाइम)

निष्कर्ष

एग्रीगेटेड parquet कैश दृष्टिकोण एक साथ दो समस्याओं को हल करता है:

  1. सटीकता। RunningCandleBuffer के ज़रिए मिनट कैंडल से टाइमफ्रेम एमुलेशन यह गारंटी देता है कि बैकटेस्ट वही डेटा देखता है जो बॉट रियल टाइम में देखता है — भविष्य में झांकना नहीं और कोई कृत्रिम देरी नहीं।

  2. गति। पूर्व-गणना किए गए टाइमफ्रेम और इंडिकेटर हज़ारों पैरामीटर कॉम्बिनेशन को दिनों के बजाय मिनटों में परीक्षण करने की सुविधा देते हैं।

विचार सरल है: एक बार गणना करें — कई बार पुनः उपयोग करें। मिनट कैंडल स्रोत डेटा हैं। बाकी सब कुछ व्युत्पन्न है और इसे पहले से गणना करके कैश किया जा सकता है। Parquet इस कैश को कॉम्पैक्ट, तेज़ और सुविधाजनक बनाता है।

फिल सिमुलेशन की सटीकता को मिनट से सेकंड और मिलीसेकंड तक एडेप्टिव ड्रिल-डाउन के साथ कैसे सुधारा जाए, इसके बारे में अधिक जानने के लिए लेख देखें एडेप्टिव ड्रिल-डाउन: वेरिएबल ग्रैनुलैरिटी के साथ बैकटेस्ट


उपयोगी लिंक

  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

उद्धरण

@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

बाज़ार से आगे रहें

AI ट्रेडिंग इनसाइट्स, मार्केट एनालिसिस और प्लेटफ़ॉर्म अपडेट के लिए हमारे न्यूज़लेटर को सब्सक्राइब करें।

हम आपकी गोपनीयता का सम्मान करते हैं। किसी भी समय अनसब्सक्राइब करें।