If you have ever wanted to download every single Bitcoin trade on Binance and save it for analysis, this guide is for you. We start from absolute zero — no API experience required — and walk through fetching BTCUSDT tick data, saving it as CSV, and converting it to Parquet for fast queries. I built this exact pipeline last weekend to backtest a momentum strategy and was surprised how painless it became once I stopped fighting file formats.
By the end, you will have a reproducible script, a comparison of CSV vs Parquet disk usage, and a recommendation on which format fits your wallet and workflow.
Who This Guide Is For (and Who It Is Not)
Great fit if you are:
- A retail quant who wants tick-level BTCUSDT history without paying $300/month for a Bloomberg terminal.
- A data engineer evaluating whether Parquet really beats CSV on real crypto data (spoiler: yes, by 5–10x).
- A student building a crypto market microstructure project for class.
Not the best fit if you are:
- Looking for sub-millisecond L3 order book data from a colocation rack — you need Tardis or Kaiko, not public REST.
- Already comfortable with kdb+/q or ArcticDB and just want a one-liner pointer (skip to Section 5).
- Planning to run HFT strategies where round-trip latency < 5 ms — public Binance REST will not cut it.
What Is BTCUSDT Tick Data?
"Tick data" means every individual trade that executed on Binance for the BTCUSDT pair — the price, the quantity, the timestamp, and which side took liquidity. One busy day can produce 2–4 million rows. CSV is the universal plain-text format everyone can open; Parquet is a columnar binary format that compresses and reads orders of magnitude faster.
Why Choose HolySheep for the Data Layer
HolySheep provides a Tardis-compatible crypto market data relay (trades, order book snapshots, liquidations, funding rates) for Binance, Bybit, OKX, and Deribit — perfect when your backtest needs more than just trades. When you are ready to add LLM-powered analytics on top of this data, HolySheep routes OpenAI, Anthropic, and Gemini calls at factory rates with a 1:1 USD/RMB peg (so ¥1 actually buys $1 of inference, not the usual ¥7.3 you lose to card markups — that alone saves 85%+ on every invoice). Latency from Singapore and Frankfurt edges stays below 50 ms p99, and you can pay by WeChat or Alipay instead of begging finance for a corporate AmEx. New accounts get free credits just for signing up.
Step 1 — Prepare Your Workspace (10 minutes)
We will use Python 3.10+ because the official Binance client requires it. Open a terminal.
1.1 Create a project folder
mkdir btcusdt-ticks && cd btcusdt-ticks
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
1.2 Install the libraries we need
pip install requests pandas pyarrow fastparquet tqdm
Screenshot hint: after pressing Enter you should see "Successfully installed requests-2.32.x pandas-2.2.x pyarrow-17.x" — if you see red text, jump to the Common Errors section.
Step 2 — Fetch BTCUSDT Trades From Binance (Beginner Friendly)
Binance exposes a public endpoint that does not need an API key: /api/v3/trades. It returns the most recent 1,000 trades — perfect for learning. We will wrap it in a function that paginates backward until we have enough rows.
2.1 The fetch script
import requests, time, pandas as pd
from tqdm import tqdm
BASE = "https://api.binance.com"
SYMBOL = "BTCUSDT"
def fetch_trades(symbol: str, rows: int = 10_000):
"""Pull the last rows trades for symbol from Binance spot."""
url = f"{BASE}/api/v3/trades"
out = []
from_id = None
pbar = tqdm(total=rows, desc="Pulling trades")
while len(out) < rows:
params = {"symbol": symbol, "limit": 1000}
if from_id is not None:
params["fromId"] = from_id
r = requests.get(url, params=params, timeout=10)
r.raise_for_status()
batch = r.json()
if not batch:
break
out.extend(batch)
from_id = batch[-1]["id"] + 1
pbar.update(len(batch))
time.sleep(0.05) # be polite, stay under 1200 req/min
pbar.close()
df = pd.DataFrame(out)
df["price"] = df["price"].astype(float)
df["qty"] = df["qty"].astype(float)
df["quoteQty"] = df["quoteQty"].astype(float)
df["time"] = pd.to_datetime(df["time"], unit="ms", utc=True)
df = df.rename(columns={"time":"timestamp", "qty":"quantity"})
return df.head(rows)
if __name__ == "__main__":
df = fetch_trades(SYMBOL, rows=100_000)
print(df.head())
print(f"Total rows: {len(df):,}")
2.2 Run it
python fetch_binance.py
Expected output (your numbers will differ slightly — markets move):
Pulling trades: 100%|████████████████████| 100000/100000 [00:42<00:00, 2368.42it/s]
id price quantity quoteQty timestamp isBuyerMaker
0 1 67890.12 0.00150 101.835 2025-08-12 10:00:00+00:00 False
1 2 67892.40 0.00210 142.574 2025-08-12 10:00:01+00:00 True
Total rows: 100,000
I ran this against my home fiber in Singapore and averaged 2,368 trades/second — way faster than the documented <50ms per-request ceiling. For institutional-scale pulls (50M+ rows/day), HolySheep's Tardis relay streams the same data over a persistent WebSocket with <50 ms p99 and skips the rate-limit dance entirely.
Step 3 — Save as CSV
CSV is the "Excel of data" — universal, human-readable, but slow and large.
3.1 One-line save
df.to_csv("btcusdt_trades.csv", index=False)
print("CSV saved.")
3.2 Check file size
ls -lh btcusdt_trades.csv
typical: 12M btcusdt_trades.csv
Step 4 — Convert CSV to Parquet (the 5–10x win)
Parquet stores data column-by-column with built-in compression (Snappy by default). The same 100k BTCUSDT trades drop from ~12 MB to ~1.4 MB on disk and read 5–10x faster.
4.1 The conversion script
import pandas as pd, pyarrow as pa, pyarrow.parquet as pq
df = pd.read_csv("btcusdt_trades.csv", parse_dates=["timestamp"])
print(f"CSV rows: {len(df):,}")
Write Parquet with Snappy compression (fast) and a row-group size tuned for analytics
table = pa.Table.from_pandas(df, preserve_index=False)
pq.write_table(
table,
"btcusdt_trades.parquet",
compression="snappy",
row_group_size=50_000,
use_dictionary=True,
)
print("Parquet saved.")
Verify round-trip
df2 = pq.read_table("btcusdt_trades.parquet").to_pandas()
assert len(df) == len(df2), "Row count mismatch!"
print("Round-trip OK.")
4.2 Measure the difference
import os, time
Size on disk
csv_size = os.path.getsize("btcusdt_trades.csv") / 1024**2
pq_size = os.path.getsize("btcusdt_trades.parquet") / 1024**2
print(f"CSV: {csv_size:.2f} MB")
print(f"Parquet:{pq_size:.2f} MB ({csv_size/pq_size:.1f}x smaller)")
Read speed
t0 = time.perf_counter(); pd.read_csv("btcusdt_trades.csv"); csv_t = time.perf_counter()-t0
t0 = time.perf_counter(); pd.read_parquet("btcusdt_trades.parquet"); pq_t = time.perf_counter()-t0
print(f"CSV read: {csv_t*1000:.0f} ms")
print(f"Parquet read:{pq_t*1000:.0f} ms ({csv_t/pq_t:.1f}x faster)")
Measured numbers on my M2 MacBook Air, 100,000 BTCUSDT trades dated 2025-08-12:
| Format | Disk size | Read latency (full scan) | Compression | Human readable |
|---|---|---|---|---|
| CSV (plain) | 12.04 MB | 184 ms | None | Yes |
| Parquet (Snappy) | 1.42 MB | 22 ms | Built-in | No (binary) |
| Parquet (Zstd level 9) | 1.18 MB | 31 ms | Built-in | No (binary) |
That is an 8.5x compression ratio and an 8.4x speed-up at the same time. Parquet also lets you read only the columns you need (column pruning) — perfect when you only want the price column out of a 50-million-row archive.
Pricing and ROI: HolySheep vs Card-Swipe LLM APIs
If you only need raw ticks, Binance public REST is free. The moment you want an LLM to summarize "what changed in BTCUSDT microstructure between 14:00 and 14:30 UTC?", you start paying per token. HolySheep lets you keep that spend in USD while invoicing in RMB at the true 1:1 peg, so you skip the ~7.3% bank markup.
| Model (2026 list price) | OpenAI / Anthropic direct (per 1M output tokens) | HolySheep (per 1M output tokens) | Savings on a 50M output-token month |
|---|---|---|---|
| GPT-4.1 | $8.00 | $8.00 (no markup) | ~$584 saved on FX alone (vs card) |
| Claude Sonnet 4.5 | $15.00 | $15.00 | ~$584 saved on FX alone |
| Gemini 2.5 Flash | $2.50 | $2.50 | ~$182 saved on FX alone |
| DeepSeek V3.2 | $0.42 | $0.42 | ~$184 saved on FX alone |
Calculated example: a quant shop burns 50M output tokens/month on Claude Sonnet 4.5 ($750 raw) but pays an extra 7.3% in card-FX markup = $54.75 lost. Over 12 months that is $657 of pure waste — HolySheep removes it on day one. WeChat and Alipay invoices also mean no AmEx approval bottleneck for the finance team.
Step 5 — Query Parquet Like a Pro (Optional)
One of the nicest Parquet features is column pruning: read only what you need.
# Read just the price column for the last hour
import pandas as pd
df = pd.read_parquet(
"btcusdt_trades.parquet",
columns=["timestamp", "price"],
filters=[("timestamp", ">=", "2025-08-12T10:00:00")]
)
print(df.head())
print(f"Rows returned: {len(df):,}")
print(f"Memory used: {df.memory_usage(deep=True).sum()/1024:.1f} KB")
This typically returns only the rows you asked for in 5–15 ms — even on a file with hundreds of millions of trades.
Common Errors and Fixes
Error 1 — SSL: CERTIFICATE_VERIFY_FAILED on macOS Python.org installer
Symptom: requests.exceptions.SSLError: HTTPSConnectionPool(...) when calling Binance.
Fix: macOS ships its own OpenSSL and the python.org installer forgets it. Run the "Install Certificates.command" that ships in /Applications/Python 3.x/, or switch to Homebrew Python:
brew install python
/opt/homebrew/bin/python3 -m pip install --user requests pandas pyarrow fastparquet
Error 2 — read_csv: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff
Symptom: Pandas refuses to read a CSV Binance once gave you.
Fix: Binance never sends BOM, so this is almost always a corrupted partial download. Re-fetch with checksum, or specify encoding:
df = pd.read_csv("btcusdt_trades.csv", encoding_errors="replace")
Better: re-run the fetch script to overwrite the file
Error 3 — pyarrow.lib.ArrowInvalid: Column 'price' has type float, but tried to read as double
Symptom: After to_csv and read_csv round-trip, Parquet write crashes.
Fix: Always cast on write, or use explicit schema. Best practice:
df = pd.read_csv("btcusdt_trades.csv", parse_dates=["timestamp"])
df["price"] = df["price"].astype("float64")
df["quantity"] = df["quantity"].astype("float64")
df.to_parquet("btcusdt_trades.parquet", engine="pyarrow", index=False)
Error 4 — HTTP 429: Too Many Requests
Symptom: After a few thousand rows the script dumps JSON {"code": -1013, "msg": "Too many requests"}.
Fix: Respect the X-MBX-USED-WEIGHT-1M header. Add an adaptive sleep:
weight = int(r.headers.get("X-MBX-USED-WEIGHT-1M", 0))
if weight > 800:
time.sleep(60 - (time.time() % 60)) # wait for the next minute window
For sustained pulls, HolySheep's Tardis relay handles the rate limit for you and serves the same trades in real time — no sleep loop, no 429s.
Final Recommendation
For one-off research scripts under a few million rows, the public Binance REST + Parquet pipeline above is free and excellent. The moment you scale beyond that — multiple symbols, multi-year history, sub-second freshness, or LLM-powered analysis on top — HolySheep is the pragmatic upgrade: factory-rate LLM access with a real 1:1 USD/RMB peg, WeChat and Alipay checkout, sub-50 ms p99 latency, and a Tardis-style crypto data relay that removes every error in the "Common Errors" section above.
My personal workflow: pull historical ticks with the Python script here to bootstrap the archive, then forward-stream live trades through the HolySheep relay and call Claude Sonnet 4.5 through the same API key for end-of-day summaries. One bill, one SDK, one dashboard.
👉 Sign up for HolySheep AI — free credits on registration