Tôi đã dành 6 tháng qua để xây dựng pipeline dữ liệu tick cho một quỹ crypto tại TP.HCM. Trong quá trình đó, tôi đã đốt khoảng 2.300 USD tiền S3 storage chỉ vì giữ CSV gzipped thô — cho tới khi tôi chuyển sang Parquet + ZSTD. Bài viết này chia sẻ lại toàn bộ kiến trúc pipeline, code production, và benchmark thực tế khi xuất dữ liệu từ Tardis.dev — nguồn dữ liệu tick crypto mà cộng đồng r/algotrading trên Reddit đánh giá là "gold standard" (bài review Reddit tháng 3/2024 đạt 487 upvotes) — sang định dạng Parquet cột nén cho phân tích.
Tại sao CSV thô không đủ cho tick data BTC perpetual
Một ngày của Binance BTCUSDT-PERP có thể sinh ra 5-8 triệu lệnh vào ngày bình thường, và vọt lên 15-20 triệu lệnh khi thị trường biến động (ví dụ ngày 12/03/2024 BTC pump 12%). CSV.gz nén gzip chỉ giảm được ~70% dung lượng, nhưng Parquet với ZSTD level 19 giảm tới 85-92%, đồng thời cho phép query cột trực tiếp mà không cần load toàn bộ file.
Đây là bảng so sánh tôi đo được trên 1 ngày dữ liệu trade Binance BTCUSDT-PERP (08/11/2024, ~7.2M lệnh):
| Định dạng | Dung lượng | Compression ratio | Query VWAP 1h (DuckDB) | Chi phí S3 lưu 1 năm |
|---|---|---|---|---|
| CSV thô | 1.42 GB | 1.0x | 18.4 giây | $312.40 |
| CSV.gz (gzip -9) | 412 MB | 3.4x | 11.7 giây | $90.64 |
| Parquet Snappy | 296 MB | 4.8x | 0.42 giây | $65.12 |
| Parquet ZSTD lv3 | 184 MB | 7.7x | 0.41 giây | $40.48 |
| Parquet ZSTD lv19 | 128 MB | 11.1x | 0.39 giây | $28.16 |
Lưu ý quan trọng: DuckDB nhanh hơn CSV tới 47 lần khi dùng Parquet, vì engine chỉ đọc đúng cột price và amount thay vì parse cả 7 cột. Đây là lý do cốt lõi để chuyển đổi.
Kiến trúc pipeline 4 lớp
- Lớp 1 — Ingestion: Tải file CSV.gz từ Tardis S3 (miễn phí qua HTTP datasets endpoint, hoặc qua Tardis API có phí cho dữ liệu real-time).
- Lớp 2 — Transformation: Polars LazyFrame để stream CSV gzip → Arrow Table → ghi Parquet với ZSTD.
- Lớp 3 — Validation: DuckDB chạy truy vấn cross-check (đếm dòng, kiểm tra gap, validate timestamp).
- Lớp 4 — AI layer: Gọi HolySheep AI để sinh nhận xét order flow từ sample data, dùng model DeepSeek V3.2 chỉ $0.42/MTok — rẻ hơn GPT-4.1 ($8/MTok) tới 19 lần.
Code production: Download + Convert
Đây là class chính tôi chạy trong môi trường thật. Tôi dùng asyncio + aiohttp để parallel download 5 luồng, retry 3 lần với exponential backoff, sink_parquet để ghi trực tiếp không load full vào RAM.
import asyncio
import aiohttp
import polars as pl
import pyarrow.parquet as pq
from datetime import datetime, timedelta
from pathlib import Path
import logging
from tenacity import retry, stop_after_attempt, wait_exponential
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("tardis-pipeline")
TARDIS_BASE = "https://datasets.tardis.dev/v1"
DEFAULT_SYMBOL = "BTCUSDT"
DEFAULT_EXCHANGE = "binance"
MAX_CONCURRENT = 5
TIMEOUT_SEC = 300
class TardisPipeline:
def __init__(
self,
symbol: str = DEFAULT_SYMBOL,
exchange: str = DEFAULT_EXCHANGE,
output_dir: str = "./data",
):
self.symbol = symbol
self.exchange = exchange
self.output_dir = Path(output_dir)
self.raw_dir = self.output_dir / "raw"
self.parquet_dir = self.output_dir / "parquet"
self.raw_dir.mkdir(parents=True, exist_ok=True)
self.parquet_dir.mkdir(parents=True, exist_ok=True)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=2, min=2, max=60),
)
async def _download_one(self, session, date_str: str):
url = (
f"{TARDIS_BASE}/trades/{self.exchange}_"
f"{self.symbol}-PERP/{date_str}.csv.gz"
)
output_path = self.raw_dir / f"{date_str}.csv.gz"
logger.info(f"GET {url}")
try:
async with session.get(
url,
timeout=aiohttp.ClientTimeout(total=TIMEOUT_SEC),
) as resp:
if resp.status == 404:
logger.warning(f"404 - skip {date_str}")
return None
resp.raise_for_status()
with open(output_path, "wb") as f:
async for chunk in resp.content.iter_chunked(8 * 1024 * 1024):
f.write(chunk)
except aiohttp.ClientError as e:
logger.error(f"Network error: {e}")
raise
return output_path
async def fetch_range(
self,
start_date: datetime,
end_date: datetime,
max_concurrent: int = MAX_CONCURRENT,
):
dates = []
current = start_date
while current <= end_date:
dates.append(current.strftime("%Y-%m-%d"))
current += timedelta(days=1)
semaphore = asyncio.Semaphore(max_concurrent)
async with aiohttp.ClientSession() as session:
async def limited(date_str):
async with semaphore:
return await self._download_one(session, date_str)
results = await asyncio.gather(*[limited(d) for d in dates])
return [r for r in results if r]
def csv_to_parquet(self, csv_path: Path, parquet_path: Path):
df = pl.scan_csv(
csv_path,
schema_overrides={
"timestamp": pl.Int64,
"local_timestamp": pl.Int64,
"id": pl.Int64,
"side": pl.Categorical,
"price": pl.Float64,
"amount": pl.Float64,
},
)
df.sink_parquet(
parquet_path,
compression="zstd",
compression_level=19,
row_group_size=100_000,
use_pyarrow=True,
)
logger.info(f"Wrote {parquet_path.name}")
async def process_all(self, start_date: datetime, end_date: datetime):
files = await self.fetch_range(start_date, end_date)
logger.info(f"Downloaded {len(files)} files")
loop = asyncio.get_event_loop()
for csv_path in files:
parquet_path = self.parquet_dir / f"{csv_path.stem}.parquet"
await loop.run_in_executor(
None, self.csv_to_parquet, csv_path, parquet_path
)
if __name__ == "__main__":
pipeline = TardisPipeline()
asyncio.run(
pipeline.process_all(
datetime(2024, 11, 1),
datetime(2024, 11, 30),
)
)
Code production: Validate + AI insight với HolySheep
Sau khi convert xong, tôi dùng DuckDB để validate integrity, rồi gọi HolySheep AI để tự động sinh nhận xét order flow. Đây là pattern tôi thấy hiệu quả nhất khi muốn cộng đồng analyst không cần biết SQL vẫn đọc được insight từ data.
import duckdb
import httpx
import json
import polars as pl
from pathlib import Path
HOLYSHEEP_BASE = "https://api.holysheep.ai/v1"
HOLYSHEEP_KEY = "