Khi backtest một chiến lược grid trading trên BTCUSDT perpetual, điều khiến tôi mất nhiều thời gian nhất không phải là logic code — mà là việc lấy đủ dữ liệu giao dịch lịch sử một cách không bị "rách". Bài viết này tổng hợp hai hướng tiếp cận phổ biến nhất hiện nay: REST polling trực tiếp từ Bybitđồng bộ incremental qua Tardis, kèm số liệu benchmark thực tế đo được trong 14 ngày. Ở cuối bài tôi sẽ chỉ cách dùng thêm lớp AI để phân tích kết quả backtest, cùng phần so sánh chi phí mô hình và ROI khi tích hợp vào workflow.

Vì sao việc chọn nguồn dữ liệu quan trọng hơn bạn nghĩ

Một backtest đáng tin phải đảm bảo 3 tiêu chí:

Phương án 1: REST API trực tiếp từ Bybit

REST là lựa chọn mặc định vì không cần đăng ký bên thứ ba. Bạn chỉ cần gọi /v5/market/recent-trade hoặc /v5/market/history-trade, phân trang theo fromto rồi ghi vào database.

"""
Backtest fetcher — REST trực tiếp Bybit v5
Tác giả: blog.holysheep.ai — Đo trên BTCUSDT linear 2026-01-15
"""
import requests
import time
import pandas as pd

BASE_URL = "https://api.bybit.com"
SYMBOL = "BTCUSDT"
CATEGORY = "linear"
WINDOW_MS = 60 * 60 * 1000   # backfill theo từng giờ
BATCH_LIMIT = 1000            # max theo tài liệu Bybit


def fetch_bybit_trades_rest(start_ts_ms: int, end_ts_ms: int,
                             max_retries: int = 5) -> pd.DataFrame:
    records = []
    cursor = start_ts_ms
    gap_count = 0

    while cursor < end_ts_ms:
        params = {
            "category": CATEGORY,
            "symbol": SYMBOL,
            "limit": BATCH_LIMIT,
            "startTime": cursor,
            "endTime": min(cursor + WINDOW_MS, end_ts_ms),
        }
        for attempt in range(max_retries):
            try:
                resp = requests.get(
                    f"{BASE_URL}/v5/market/history-trade",
                    params=params, timeout=10,
                )
                resp.raise_for_status()
                payload = resp.json()
                if payload.get("retCode") != 0:
                    raise ValueError(payload.get("retMsg"))
                break
            except Exception as e:
                # Exponential backoff — tránh 429
                wait = (2 ** attempt) + 0.5
                print(f"[{cursor}] retry {attempt+1}/{max_retries}: {e}")
                time.sleep(wait)
        else:
            gap_count += 1
            cursor += WINDOW_MS
            continue

        trades = payload["result"]["list"]
        if not trades:
            gap_count += 1
            cursor += WINDOW_MS
            continue

        for t in trades:
            records.append({
                "ts":    int(t["time"]),
                "price": float(t["price"]),
                "qty":   float(t["size"]),
                "side":  t["side"],            # 'Buy' | 'Sell'
                "tid":   t["execId"],
            })
        cursor += WINDOW_MS
        time.sleep(0.055)   # giữ dưới 100 req / 5s

    df = pd.DataFrame(records).drop_duplicates("tid")
    print(f"Thu được {len(df):,} trade — gap window: {gap_count}")
    return df


if __name__ == "__main__":
    # Backfill 30 ngày qua BTCUSDT
    end = int(time.time() * 1000)
    start = end - 30 * 24 * 3600 * 1000
    df = fetch_bybit_trades_rest(start, end)
    df.to_parquet("bybit_btcusdt_30d.parquet")

Số liệu thực đo trong 14 ngày của tôi (server Singapore, ping ~38ms):

Nhược điểm lớn nhất: lịch sử Bybit chỉ giữ khoảng 6 tháng cho market trade. Nếu bạn cần backtest từ 2020 hoặc 2021 (như giai đoạn COVID, FTX collapse), REST đơn thuần bó tay.

Phương án 2: Tardis Incremental + Historical S3 Sync

Tardis Machine lưu trữ mọi message raw từ Bybit (và 12 sàn khác) từ 2018, chia thành file Parquet theo giờ trên S3. Bạn có thể:

  1. Backfill lịch sử bằng cách tải file Parquet tương ứng.
  2. Đồng bộ real-time incremental qua WebSocket để cập nhật local DB liên tục với độ trễ cực thấp.
"""
Tardis incremental sync + historical replay
Tác giả: blog.holysheep.ai — chạy trên Ubuntu 22.04
"""
import asyncio
import json
import websocket   # pip install websocket-client
import boto3
import pandas as pd
from io import BytesIO

TARDIS_API_KEY = "YOUR_TARDIS_KEY"
S3_BUCKET = "tardis-public-data"
SYMBOL = "BTCUSDT"


def pull_historical_parquet(date_str: str, exchange: str = "bybit") -> pd.DataFrame:
    """Tải 1 file parquet theo ngày từ S3 bucket public của Tardis."""
    key = f"{exchange}/trades_v2/{date_str}/{SYMBOL}.parquet"
    s3 = boto3.client("s3", region_name="eu-central-1")
    obj = s3.get_object(Bucket=S3_BUCKET, Key=key)
    df = pd.read_parquet(BytesIO(obj["Body"].read()))
    print(f"[{date_str}] {len(df):,} trade loaded")
    return df


def on_message(ws, raw):
    """Incremental real-time trade update."""
    msg = json.loads(raw)
    if msg.get("type