Tôi đã dành ba tuần cuối tháng 11 để đối chiếu dữ liệu tick-by-tick từ ba sàn lớn nhất cho một chiến lược grid trading chạy trên máy chủ riêng. Vấn đề lớn nhất không phải là lấy dữ liệu thế nào mà là làm sao để mọi sàn nói cùng một ngôn ngữ: schema khác nhau, múi giờ khác nhau, rate-limit khác nhau và quan trọng nhất là API của Bybit gần như không có endpoint public cho lịch sử trades quá 200 dòng. Bài viết này tổng hợp lại toàn bộ pipeline tôi đã chạy ổn định suốt 14 ngày liên tục, từ cách kéo CSV batch cho đến cách đổ vào TimescaleDB / DuckDB để truy vấn tức thì.

Tổng quan ba sàn và endpoint khả dụng

SànEndpoint lịch sử tradesGiới hạn / lần gọiPhạm vi lùi tối đaĐịnh dạng
Binance Spot/api/v3/trades, /api/v3/historicalTrades1000 dòng / requestKhông giới hạn qua historicalTrades (cần API key)JSON
OKX Spot/api/v5/market/trades-history500 dòng / request~3 thángJSON (CSV cần tự chuyển)
Bybit Spot v5/v5/market/recent-trade, /v5/market/history-trade1000 dòng / requestKhông giới hạn (cần auth cho lượng lớn)JSON

Đánh giá thực tế của tôi sau 14 ngày chạy song song ba pipeline trên VPS Frankfurt (1 vCPU, 2GB RAM, giá 4 USD/tháng):

Code 1 — Batch download CSV từ Binance Spot

Đoạn script dưới dùng historicalTrades để lấy tick lịch sử từng trang một, ghi nối tiếp vào một file CSV duy nhất. Bạn cần API key có quyền đọc lịch sử.

import csv
import time
import requests
from pathlib import Path

API_KEY = "YOUR_BINANCE_API_KEY"
BASE = "https://api.binance.com"
SYMBOL = "BTCUSDT"
OUT = Path(f"{SYMBOL.lower()}_binance_trades.csv")

def fetch_page(symbol: str, from_id: int | None = None, limit: int = 1000):
    params = {"symbol": symbol, "limit": limit}
    if from_id is not None:
        params["fromId"] = from_id
    r = requests.get(f"{BASE}/api/v3/historicalTrades",
                     params=params,
                     headers={"X-MBX-APIKEY": API_KEY},
                     timeout=10)
    r.raise_for_status()
    return r.json()

def stream_to_csv(symbol: str, max_pages: int = 500):
    write_header = not OUT.exists()
    with OUT.open("a", newline="") as f:
        w = csv.writer(f)
        if write_header:
            w.writerow(["trade_id", "price", "qty", "quote_qty",
                        "time_ms", "is_buyer_maker", "symbol"])
        last_id = None
        for page in range(max_pages):
            data = fetch_page(symbol, last_id)
            if not data:
                break
            for d in data:
                w.writerow([d["id"], d["price"], d["qty"], d["quoteQty"],
                            d["time"], int(d["isBuyerMaker"]), symbol])