Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Optimizing Time-Series Data Storage for Backtesting

Recommended resources

Hello! Welcome to the fourth lesson in our course on low-frequency crypto trading.

In our previous lesson, we built a pipeline to fetch historical OHLCV data from exchange APIs, tackling the practical challenges of pagination and rate limiting. We now have the raw material for our backtesting engine. The logical next step, and the focus of today's lesson, is to decide how to store this data.

This lesson is designed to meet the learning outcome: Design a data storage schema (e.g., Parquet, TimescaleDB) optimized for efficient time-series queries in a backtesting context.

Given your extensive background in data engineering, we will approach this not as an introduction to data storage, but as a strategic design discussion. We'll analyze the specific read patterns of a backtesting system and evaluate two leading solutions—a file-based approach with Apache Parquet and a specialized time-series database, TimescaleDB—assessing their trade-offs in this particular context.

1. The Core Problem: Efficiently Querying Time-Series Data

Before choosing a tool, we must define the problem. A backtesting engine primarily performs read-heavy, analytical queries on time-series data. Common query patterns include:

  • Single-instrument time slice: "Fetch all daily OHLCV data for BTC-USDT from 2018 to the present."
  • Multi-instrument time slice: "Fetch 1-hour OHLCV data for BTC-USDT, ETH-USDT, and SOL-USDT for the month of May 2023."
  • Cross-sectional snapshot: "Fetch the closing prices for all assets in our universe on January 1st, 2022." (Less common for pure time-series momentum, but important for other strategies).

The central debate for storing this kind of data often boils down to using a traditional database versus a structured system of flat files. This is a classic data engineering dilemma. Let's explore the arguments from practitioners who have faced this exact problem.

Building Financial Data Time Series Database from scratch

This Stack Exchange thread, 'Building Financial Data Time Series Database from scratch', provides an excellent, high-level discussion on the pros and cons of different storage strategies for financial data. It will frame our entire lesson.

First, read the original question to understand the typical requirements. Then, read the two highest-voted answers: The answer by 'drobertson' argues for a file-based system, claiming databases add unnecessary overhead for immutable, sequentially-accessed time-series data. The answer by 'madilyn' provides a counter-argument, emphasizing that the choice depends on the use case and highlights the breaking points of flat files (e.g., concurrency, developer time). Focus on the trade-offs they discuss. This isn't about finding a single right answer, but understanding the design landscape.

This discussion highlights that for a single researcher focused on backtesting, a well-designed file-based system can be extremely performant. However, as complexity grows (e.g., multiple users, real-time updates), the features of a database system become more compelling. Let's explore both paths.


2. Option 1: A File-Based Lakehouse with Apache Parquet

If we choose a file-based approach, the format is critical. Plain text formats like CSV are a poor choice for performance-sensitive applications. A binary, columnar format is far superior.

This INCREDIBLE trick will speed up your data processes.

This video, 'This INCREDIBLE trick will speed up your data processes', provides a concise and compelling benchmark comparing various file formats. It clearly demonstrates why formats like Parquet are the standard in analytical data systems.

Watch the following segments to see the performance differences: 02:00 - 06:06: Note the issues with CSV: large file size, slow I/O, and—most importantly—the loss of data types upon reading. 07:14 - 08:57: See how Parquet dramatically reduces file size and improves read/write speed while preserving data types. 10:58 - 12:43: The final summary provides a clear comparison across all formats.

The video makes it clear that Parquet is a strong candidate. But why is it so effective for our backtesting use case? The answer lies in three key optimizations.

Python and Parquet Performance - Data Syndrome Blog

The blog post 'Python and Parquet Performance' offers a superb deep dive into the mechanics of Parquet and how to use it effectively with Python.

Read the first three sections: 'Introduction', 'Column-Oriented Storage and Compression', and 'Columnar Partitioning'. These explain the core concepts that make Parquet so powerful for analytical queries.

Designing a Parquet-Based Schema

The key to an efficient Parquet-based store is partitioning. By structuring our data in a directory tree based on symbol and time, we allow query engines to perform "partition pruning"—skipping enormous amounts of data by only reading the relevant folders.

A robust schema for our OHLCV data would be a Hive-style partitioned layout:

base_path/{timeframe}/{symbol}/year={YYYY}/month={MM}/data.snappy.parquet

For example:

  • data/1h/BTC-USDT/year=2023/month=01/data.snappy.parquet
  • data/1h/BTC-USDT/year=2023/month=02/data.snappy.parquet
  • data/1h/ETH-USDT/year=2023/month=01/data.snappy.parquet

Why this structure?

  • timeframe: Separates different candle granularities (e.g., 1h, 1d).
  • symbol: The primary filter for most queries.
  • year/month: Allows for efficient time-based filtering. A query for "BTC-USDT in Q1 2023" would only need to touch three folders. The choice of partitioning by month vs. day is a trade-off between the number of files and the granularity of pruning. For LFT data, monthly is often a good balance.

Implementation with Python

The pyarrow library, which integrates with pandas, makes it straightforward to implement this.

Python and Parquet Performance - Data Syndrome Blog

Let's return to the 'Python and Parquet Performance' blog post to see the code for writing and reading partitioned datasets.

Review the sections 'Parquet Partitions using Pandas & PyArrow' and 'Parquet Partitions with PySpark'. Focus on the to_parquet and read_parquet examples. Notice how the partition_cols argument in the write function creates the directory structure, and the filters argument in the read function leverages it for efficient loading.

Here is a conceptual example of how you would write your fetched data:

import pandas as pd

# Assume 'df' is a DataFrame from the previous lesson's downloader
# It contains OHLCV data for multiple symbols and has a datetime index

# Add year and month columns for partitioning
df['year'] = df.index.year
df['month'] = df.index.month

# Write to a partitioned Parquet dataset
df.to_parquet(
    path='data/1h/',
    engine='pyarrow',
    compression='snappy',
    partition_cols=['symbol', 'year', 'month']
)

Reading the data for a specific symbol and time range becomes highly efficient:

# Read data for a single symbol for two months of 2023
df_filtered = pd.read_parquet(
    path='data/1h/',
    engine='pyarrow',
    filters=[
        ('symbol', '=', 'BTC-USDT'),
        ('year', '=', 2023),
        ('month', 'in', [1, 2])
    ]
)

This approach is simple, requires no extra services, and is extremely fast for a single-user backtesting workflow.


3. Option 2: A Time-Series Database with TimescaleDB

The file-based approach is powerful, but a database offers benefits like a rich query language (SQL), easier data management, and built-in support for concurrency. For time-series data, a standard PostgreSQL or MySQL database can become slow as tables grow. This is where specialized time-series databases shine.

TimescaleDB is a PostgreSQL extension that adds powerful time-series capabilities to a standard, robust SQL database.

TimescaleDB for Algotrading - Full course in Python

This video, 'TimescaleDB for Algotrading', provides a fantastic introduction to why TimescaleDB is a great fit for financial data and how to use its core features.

Watch the first four minutes (00:00 - 03:51) to understand the value proposition. Pay close attention to the explanation of its two key features: Hypertables: This is TimescaleDB's form of automatic partitioning by time. Understand how it chunks data behind the scenes. Continuous Aggregates: These are essentially materialized views that automatically stay up-to-date. This is incredibly useful for creating different timeframes (e.g., daily candles from minute data) without manual effort.

Designing a TimescaleDB Schema

The schema design in TimescaleDB is deceptively simple. You start with a standard SQL table and then transform it into a hypertable.

  1. Create the Table:
    A single table can hold all our OHLCV data.

    CREATE TABLE ohlcv (
        time TIMESTAMPTZ NOT NULL,
        symbol TEXT NOT NULL,
        open DOUBLE PRECISION NOT NULL,
        high DOUBLE PRECISION NOT NULL,
        low DOUBLE PRECISION NOT NULL,
        close DOUBLE PRECISION NOT NULL,
        volume DOUBLE PRECISION NOT NULL
    );
    
  2. Create the Hypertable:
    This command tells TimescaleDB to automatically partition the ohlcv table by the time column.

    SELECT create_hypertable('ohlcv', 'time');
    
  3. Add an Index:
    For efficient queries, a unique index on symbol and time is essential. This allows the database to quickly find the starting point for a query for a specific asset.

    CREATE UNIQUE INDEX idx_symbol_time ON ohlcv (symbol, time DESC);
    

    (The DESC on time is a common optimization for time-series, as queries often ask for the most recent data first).

This setup is demonstrated clearly in the video.

TimescaleDB for Algotrading - Full course in Python

Let's return to the TimescaleDB video to see the schema creation in practice. While the video sets up a table for raw trades, the principle is identical for OHLCV data.

Watch the following segments: Setting up with Docker (05:12 - 09:19): This shows how easily you can get a TimescaleDB instance running locally. Given your experience, this should be very straightforward. Creating a Hypertable (24:33 - 29:28): Observe the process of creating a standard table and then converting it into a hypertable. This is the exact workflow you would follow for your OHLCV data.

The power of this approach is that you now have the full analytical capability of SQL at your disposal, supercharged for time-series data. A query like "find all days where the daily range (high - low) for BTC-USDT was greater than 5% of the opening price" is much more expressive in SQL than with a file-based API.


4. Comparison and Conclusion

Both Parquet and TimescaleDB are excellent, production-grade solutions. The choice depends on the specific constraints and future goals of your project.

Aspect Parquet with Partitioning TimescaleDB
Setup & Dependencies Minimal. Requires Python libraries (pandas, pyarrow). Data can live on local disk or cloud storage (S3). Requires a running database instance (e.g., via Docker).
Query Interface Programmatic (e.g., Pandas/PyArrow API, Spark SQL). Can be verbose for complex queries. Full SQL with time-series extensions. Highly expressive and standardized.
Performance Extremely high for batch reads of filtered data. Can be faster than a DB for simple "full slice" reads. Excellent for a wide range of queries (point-in-time, range, aggregations) due to advanced indexing and query planning.
Advanced Features Relies on the query engine (e.g., Spark, Dask) for advanced computation. Built-in features like continuous aggregates, data retention policies, and compression are very powerful.
Ecosystem & Use Case Ideal for single-user research, integration with the Python data science stack, and large-scale batch processing. Ideal for systems that may evolve towards multi-user access, real-time data ingestion, or require a flexible, powerful query interface.

Recommendation:

  • For getting started quickly with a focus purely on a single-user backtesting engine, partitioned Parquet files are arguably the simplest and most efficient path. The infrastructure is minimal, and it integrates seamlessly with the tools you'll use for analysis.
  • If you envision a system that might later incorporate live data streams, serve a dashboard, or be accessed by multiple processes, TimescaleDB is a more robust and scalable foundation. The slight increase in setup complexity pays dividends in flexibility and advanced features.

Key Takeaways:

  • For efficient backtesting, you must move beyond simple formats like CSV.
  • Apache Parquet offers superior performance through columnar storage, compression, and especially partitioning, which is the key to designing an efficient file-based schema.
  • TimescaleDB enhances PostgreSQL with time-series superpowers like hypertables (automatic time-based partitioning) and continuous aggregates, providing a full SQL interface with excellent performance.
  • The choice between them is a classic data engineering trade-off between the simplicity of a file-based system and the feature-richness of a specialized database.

In our previous lesson, you learned to fetch the data. Now, you know how to store it. The next step is to ensure the data is statistically sound for our chosen strategy.

In the next lesson, we will tackle the learning outcome: "Analyze stationarity of crypto asset returns using statistical tests (e.g., ADF, KPSS) and interpret the results for strategy selection." This will be our first deep dive into the statistical properties that underpin momentum and mean-reversion strategies.