3 tháng trước, mình gặp một lỗi kinh hoàng lúc 2 giờ sáng: ConnectionError: timeout - k89fh2j4. Hệ thống trading tự động của mình ngừng hoạt động hoàn toàn vì không lấy được dữ liệu lịch sử từ Binance. 47 giao dịch bị bỏ lỡ, thiệt hại khoảng 2,300 USD. Kể từ đó, mình đã nghiên cứu kỹ Binance Historical Data API và chia sẻ lại toàn bộ kiến thức thực chiến trong bài viết này.
Binance Historical Data API Là Gì?
Binance cung cấp API miễn phí để truy cập dữ liệu lịch sử, bao gồm:
- Klines (Candlestick): Dữ liệu nến OHLCV theo khung thời gian
- AggTrades: Dữ liệu giao dịch tổng hợp
- Trades: Dữ liệu từng giao dịch riêng lẻ
- Historical Trades: Lịch sử giao dịch đầy đủ
- Daily Aggregated: Tổng hợp theo ngày
- Premium Index Klines: Dữ liệu premium index
Yêu Cầu Ban Đầu
Trước khi bắt đầu, bạn cần:
- Tài khoản Binance đã xác minh KYC
- API Key (và Secret Key nếu cần giao dịch)
- Python 3.8+ hoặc Node.js 16+
- Thư viện requests (Python) hoặc axios (Node.js)
Code Mẫu Hoàn Chỉnh - Python
#!/usr/bin/env python3
"""
Binance Historical Data API - Hướng dẫn toàn diện
Author: HolySheep AI Blog
Version: 2026.01
"""
import requests
import time
import json
from datetime import datetime, timedelta
from typing import List, Dict, Optional
class BinanceHistoricalData:
"""Class truy cập dữ liệu lịch sử từ Binance API"""
BASE_URL = "https://api.binance.com"
MAX_REQUESTS_PER_MINUTE = 1200 # Giới hạn rate limit
def __init__(self, api_key: str = None, secret_key: str = None):
self.api_key = api_key
self.secret_key = secret_key
self.session = requests.Session()
if api_key:
self.session.headers.update({"X-MBX-APIKEY": api_key})
def get_klines(
self,
symbol: str,
interval: str,
start_time: int = None,
end_time: int = None,
limit: int = 500
) -> List[Dict]:
"""
Lấy dữ liệu nến (candlestick) từ Binance
Args:
symbol: Cặp tiền (VD: 'BTCUSDT')
interval: Khung thời gian ('1m', '5m', '1h', '1d'...)
start_time: Thời gian bắt đầu (milliseconds timestamp)
end_time: Thời gian kết thúc (milliseconds timestamp)
limit: Số lượng nến tối đa (1-1000)
Returns:
List chứa dữ liệu nến
"""
endpoint = "/api/v3/klines"
params = {
"symbol": symbol.upper(),
"interval": interval,
"limit": min(limit, 1000)
}
if start_time:
params["startTime"] = start_time
if end_time:
params["endTime"] = end_time
try:
response = self.session.get(
f"{self.BASE_URL}{endpoint}",
params=params,
timeout=10
)
response.raise_for_status()
data = response.json()
# Chuyển đổi sang dict format
return self._parse_klines(data)
except requests.exceptions.Timeout:
raise ConnectionError(f"Timeout khi lấy dữ liệu {symbol}")
except requests.exceptions.HTTPError as e:
if response.status_code == 429:
raise RateLimitError("Rate limit exceeded. Chờ 60 giây...")
elif response.status_code == 401:
raise AuthError("API Key không hợp lệ")
else:
raise APIError(f"HTTP Error: {e}")
except Exception as e:
raise APIError(f"Lỗi không xác định: {e}")
def _parse_klines(self, data: List) -> List[Dict]:
"""Parse dữ liệu kline từ array sang dict format"""
parsed = []
for k in data:
parsed.append({
"open_time": k[0],
"open": float(k[1]),
"high": float(k[2]),
"low": float(k[3]),
"close": float(k[4]),
"volume": float(k[5]),
"close_time": k[6],
"quote_volume": float(k[7]),
"trades": int(k[8]),
"taker_buy_volume": float(k[9]),
"taker_buy_quote_volume": float(k[10]),
})
return parsed
def get_historical_klines(
self,
symbol: str,
interval: str,
start_date: str,
end_date: str = None
) -> List[Dict]:
"""
Lấy dữ liệu lịch sử dài (tự động paginate)
Hỗ trợ ngày tháng dạng '2023-01-01'
"""
all_klines = []
# Chuyển đổi ngày sang timestamp
start_ts = int(datetime.strptime(start_date, "%Y-%m-%d").timestamp() * 1000)
end_ts = int(
datetime.strptime(end_date, "%Y-%m-%d").timestamp() * 1000
) if end_date else int(time.time() * 1000)
current_ts = start_ts
while current_ts < end_ts:
try:
klines = self.get_klines(
symbol=symbol,
interval=interval,
start_time=current_ts,
end_time=end_ts,
limit=500
)
if not klines:
break
all_klines.extend(klines)
current_ts = klines[-1]["close_time"] + 1
print(f"Đã lấy {len(all_klines)} nến... ({symbol})")
# Tránh rate limit
time.sleep(0.2)
except RateLimitError:
print("Rate limit - chờ 60 giây...")
time.sleep(60)
except Exception as e:
print(f"Lỗi: {e}")
break
return all_klines
def get_aggregated_trades(
self,
symbol: str,
from_id: int = None,
start_time: int = None,
end_time: int = None,
limit: int = 500
) -> List[Dict]:
"""Lấy dữ liệu giao dịch tổng hợp"""
endpoint = "/api/v3/aggTrades"
params = {
"symbol": symbol.upper(),
"limit": min(limit, 1000)
}
if from_id:
params["fromId"] = from_id
if start_time:
params["startTime"] = start_time
if end_time:
params["endTime"] = end_time
response = self.session.get(
f"{self.BASE_URL}{endpoint}",
params=params,
timeout=10
)
response.raise_for_status()
return response.json()
class RateLimitError(Exception):
"""Exception khi vượt rate limit"""
pass
class AuthError(Exception):
"""Exception khi xác thực thất bại"""
pass
class APIError(Exception):
"""Exception chung của API"""
pass
================== VÍ DỤ SỬ DỤNG ==================
if __name__ == "__main__":
# Khởi tạo client (không cần API key cho dữ liệu public)
client = BinanceHistoricalData()
# Lấy 500 nến 1 giờ gần nhất của BTCUSDT
print("Lấy dữ liệu BTCUSDT 1h...")
btc_1h = client.get_klines(
symbol="BTCUSDT",
interval="1h",
limit=500
)
print(f"Tổng cộng: {len(btc_1h)} nến")
print(f"Nến mới nhất: {btc_1h[-1]}")
# Lấy dữ liệu 30 ngày gần đây
print("\nLấy dữ liệu ETHUSDT 1 ngày trong 30 ngày...")
eth_daily = client.get_historical_klines(
symbol="ETHUSDT",
interval="1d",
start_date="2025-11-01",
end_date="2025-12-01"
)
print(f"Tổng cộng: {len(eth_daily)} nến")
Code Mẫu - Node.js (TypeScript)
/**
* Binance Historical Data API - Node.js/TypeScript Implementation
* Author: HolySheep AI Blog
*/
interface Kline {
openTime: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
closeTime: number;
quoteVolume: number;
trades: number;
takerBuyBaseVolume: number;
takerBuyQuoteVolume: number;
}
interface ApiError {
code: number;
msg: string;
}
class BinanceApiError extends Error {
constructor(
message: string,
public code?: number,
public statusCode?: number
) {
super(message);
this.name = 'BinanceApiError';
}
}
class BinanceHistoricalClient {
private baseUrl = 'https://api.binance.com';
private requestCount = 0;
private lastReset = Date.now();
private async request(
endpoint: string,
params: Record = {}
): Promise {
// Rate limit check
this.requestCount++;
if (this.requestCount >= 1200) {
const elapsed = Date.now() - this.lastReset;
if (elapsed < 60000) {
await new Promise(r => setTimeout(r, 60000 - elapsed));
}
this.requestCount = 0;
this.lastReset = Date.now();
}
// Build query string
const queryParams = Object.entries(params)
.filter(([_, v]) => v !== undefined)
.map(([k, v]) => ${encodeURIComponent(k)}=${encodeURIComponent(v)})
.join('&');
const url = ${this.baseUrl}${endpoint}${queryParams ? '?' + queryParams : ''};
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
if (response.status === 429) {
throw new BinanceApiError('Rate limit exceeded', undefined, 429);
}
if (response.status === 401) {
throw new BinanceApiError('Unauthorized - Invalid API Key', undefined, 401);
}
if (response.status === 418) {
throw new BinanceApiError('IP banned - Too many requests', undefined, 418);
}
throw new BinanceApiError(HTTP Error: ${response.status}, undefined, response.status);
}
const data = await response.json();
// Check for API error response
if ('code' in data && 'msg' in data) {
throw new BinanceApiError(data.msg, data.code);
}
return data as T;
} catch (error) {
if (error instanceof BinanceApiError) {
throw error;
}
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new BinanceApiError('Connection failed - Check internet', undefined, undefined);
}
throw error;
}
}
async getKlines(
symbol: string,
interval: '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' | '6h' | '8h' | '12h' | '1d' | '3d' | '1w' | '1M',
limit: number = 500,
startTime?: number,
endTime?: number
): Promise {
const rawData = await this.request(
'/api/v3/klines',
{
symbol: symbol.toUpperCase(),
interval,
limit: Math.min(limit, 1000),
startTime,
endTime,
}
);
return rawData.map(k => ({
openTime: k[0],
open: parseFloat(k[1]),
high: parseFloat(k[2]),
low: parseFloat(k[3]),
close: parseFloat(k[4]),
volume: parseFloat(k[5]),
closeTime: k[6],
quoteVolume: parseFloat(k[7]),
trades: parseInt(k[8]),
takerBuyBaseVolume: parseFloat(k[9]),
takerBuyQuoteVolume: parseFloat(k[10]),
}));
}
async getHistoricalKlines(
symbol: string,
interval: string,
startDate: string,
endDate?: string
): Promise {
const allKlines: Kline[] = [];
const startTs = new Date(startDate).getTime();
const endTs = endDate ? new Date(endDate).getTime() : Date.now();
let currentTs = startTs;
while (currentTs < endTs) {
try {
const klines = await this.getKlines(
symbol,
interval as any,
500,
currentTs,
endTs
);
if (klines.length === 0) break;
allKlines.push(...klines);
currentTs = klines[klines.length - 1].closeTime + 1;
console.log(Progress: ${allKlines.length} klines loaded...);
// Delay to avoid rate limit
await new Promise(r => setTimeout(r, 200));
} catch (error) {
if (error instanceof BinanceApiError && error.statusCode === 429) {
console.log('Rate limited. Waiting 60 seconds...');
await new Promise(r => setTimeout(r, 60000));
} else {
throw error;
}
}
}
return allKlines;
}
async getAggregatedTrades(
symbol: string,
limit: number = 500,
startTime?: number,
endTime?: number
): Promise {
return this.request('/api/v3/aggTrades', {
symbol: symbol.toUpperCase(),
limit: Math.min(limit, 1000),
startTime,
endTime,
});
}
async getSymbolPrice(symbol: string): Promise {
const data = await this.request<{ price: string }>('/api/v3/ticker/price', {
symbol: symbol.toUpperCase(),
});
return parseFloat(data.price);
}
}
// ================== VÍ DỤ SỬ DỤNG ==================
async function main() {
const client = new BinanceHistoricalClient();
try {
// Lấy 500 nến 15 phút của BTCUSDT
console.log('Fetching BTCUSDT 15m data...');
const btcKlines = await client.getKlines('BTCUSDT', '15m', 500);
console.log(Received: ${btcKlines.length} candles);
console.log('Latest candle:', btcKlines[btcKlines.length - 1]);
// Lấy dữ liệu 1 năm
console.log('\nFetching ETHUSDT daily for 1 year...');
const ethKlines = await client.getHistoricalKlines(
'ETHUSDT',
'1d',
'2025-01-01',
'2025-12-31'
);
console.log(Total: ${ethKlines.length} candles);
// Tính toán indicator đơn giản
const closes = ethKlines.map(k => k.close);
const sma20 = calculateSMA(closes, 20);
console.log(ETH SMA(20): ${sma20[sma20.length - 1]});
} catch (error) {
if (error instanceof BinanceApiError) {
console.error(API Error [${error.statusCode}]: ${error.message});
} else {
console.error('Unexpected error:', error);
}
}
}
function calculateSMA(prices: number[], period: number): number[] {
const sma: number[] = [];
for (let i = period - 1; i < prices.length; i++) {
const sum = prices.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
sma.push(sum / period);
}
return sma;
}
main();
Bảng So Sánh Các Khung Thời Gian
| Khung thời gian | Mã viết tắt | Limit tối đa/lần gọi | Dữ liệu có sẵn | Phù hợp cho |
|---|---|---|---|---|
| 1 phút | 1m | 1000 | ~7 ngày gần nhất | Scalping, phân tích intra-day |
| 5 phút | 5m | 1000 | ~60 ngày | Day trading |
| 15 phút | 15m | 1000 | ~120 ngày | Swing trading ngắn |
| 1 giờ | 1h | 1000 | ~500 ngày | Swing trading |
| 4 giờ | 4h | 1000 | ~2 năm | Phân tích trung hạn |
| 1 ngày | 1d | 1000 | Toàn bộ lịch sử | Investment, backtesting dài hạn |
| 1 tuần | 1w | 1000 | Toàn bộ lịch sử | Phân tích xu hướng dài hạn |
Xử Lý Dữ Liệu Với Pandas (Python)
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
class BinanceDataProcessor:
"""Xử lý và phân tích dữ liệu từ Binance"""
def __init__(self, klines: list):
"""
Khởi tạo với dữ liệu klines từ BinanceHistoricalData
Args:
klines: List các dict chứa dữ liệu nến
"""
self.df = pd.DataFrame(klines)
self._prepare_dataframe()
def _prepare_dataframe(self):
"""Chuẩn bị DataFrame với các cột cần thiết"""
# Chuyển đổi timestamp
self.df['datetime'] = pd.to_datetime(self.df['open_time'], unit='ms')
self.df.set_index('datetime', inplace=True)
# Đảm bảo các cột numeric
numeric_cols = ['open', 'high', 'low', 'close', 'volume']
for col in numeric_cols:
self.df[col] = pd.to_numeric(self.df[col])
def add_sma(self, periods: list) -> 'BinanceDataProcessor':
"""Thêm Simple Moving Average"""
for period in periods:
self.df[f'sma_{period}'] = self.df['close'].rolling(window=period).mean()
return self
def add_ema(self, periods: list) -> 'BinanceDataProcessor':
"""Thêm Exponential Moving Average"""
for period in periods:
self.df[f'ema_{period}'] = self.df['close'].ewm(span=period, adjust=False).mean()
return self
def add_rsi(self, period: int = 14) -> 'BinanceDataProcessor':
"""Thêm RSI (Relative Strength Index)"""
delta = self.df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
self.df['rsi'] = 100 - (100 / (1 + rs))
return self
def add_macd(
self,
fast: int = 12,
slow: int = 26,
signal: int = 9
) -> 'BinanceDataProcessor':
"""Thêm MACD (Moving Average Convergence Divergence)"""
exp1 = self.df['close'].ewm(span=fast, adjust=False).mean()
exp2 = self.df['close'].ewm(span=slow, adjust=False).mean()
self.df['macd'] = exp1 - exp2
self.df['macd_signal'] = self.df['macd'].ewm(span=signal, adjust=False).mean()
self.df['macd_hist'] = self.df['macd'] - self.df['macd_signal']
return self
def add_bollinger_bands(
self,
period: int = 20,
std_dev: float = 2.0
) -> 'BinanceDataProcessor':
"""Thêm Bollinger Bands"""
self.df['bb_middle'] = self.df['close'].rolling(window=period).mean()
std = self.df['close'].rolling(window=period).std()
self.df['bb_upper'] = self.df['bb_middle'] + (std * std_dev)
self.df['bb_lower'] = self.df['bb_middle'] - (std * std_dev)
return self
def get_technical_summary(self) -> dict:
"""Trả về tóm tắt các chỉ báo kỹ thuật"""
latest = self.df.iloc[-1]
return {
'symbol': 'BTCUSDT', # Cần truyền thêm symbol
'current_price': latest['close'],
'sma_20': latest.get('sma_20', None),
'sma_50': latest.get('sma_50', None),
'sma_200': latest.get('sma_200', None),
'rsi_14': latest.get('rsi', None),
'macd': latest.get('macd', None),
'macd_signal': latest.get('macd_signal', None),
'bb_upper': latest.get('bb_upper', None),
'bb_middle': latest.get('bb_middle', None),
'bb_lower': latest.get('bb_lower', None),
'volume_24h': latest['volume'],
}
def plot_chart(self, show_bb: bool = True, show_sma: bool = True):
"""Vẽ biểu đồ giá với các chỉ báo"""
fig, axes = plt.subplots(2, 1, figsize=(14, 10),
gridspec_kw={'height_ratios': [3, 1]})
# Biểu đồ giá
ax1 = axes[0]
ax1.plot(self.df.index, self.df['close'], label='Close', linewidth=1.5)
if show_sma:
if 'sma_20' in self.df.columns:
ax1.plot(self.df.index, self.df['sma_20'], label='SMA 20', alpha=0.7)
if 'sma_50' in self.df.columns:
ax1.plot(self.df.index, self.df['sma_50'], label='SMA 50', alpha=0.7)
if show_bb:
if 'bb_upper' in self.df.columns:
ax1.fill_between(
self.df.index,
self.df['bb_lower'],
self.df['bb_upper'],
alpha=0.1,
label='Bollinger Bands'
)
ax1.plot(self.df.index, self.df['bb_upper'], 'r--', alpha=0.5)
ax1.plot(self.df.index, self.df['bb_lower'], 'r--', alpha=0.5)
ax1.set_title('Giá và Chỉ báo Kỹ thuật', fontsize=14)
ax1.set_ylabel('Giá (USDT)')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Biểu đồ RSI
ax2 = axes[1]
if 'rsi' in self.df.columns:
ax2.plot(self.df.index, self.df['rsi'], 'purple', linewidth=1)
ax2.axhline(y=70, color='r', linestyle='--', alpha=0.5)
ax2.axhline(y=30, color='g', linestyle='--', alpha=0.5)
ax2.fill_between(self.df.index, 70, 100, alpha=0.2, color='red')
ax2.fill_between(self.df.index, 0, 30, alpha=0.2, color='green')
ax2.set_ylabel('RSI')
ax2.set_ylim(0, 100)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('chart.png', dpi=150)
plt.show()
print("Đã lưu biểu đồ vào chart.png")
def export_to_csv(self, filename: str = 'data.csv'):
"""Xuất dữ liệu ra file CSV"""
self.df.to_csv(filename)
print(f"Đã xuất {len(self.df)} dòng vào {filename}")
def export_to_json(self, filename: str = 'data.json'):
"""Xuất dữ liệu ra file JSON"""
self.df.to_json(filename, orient='records', date_format='iso')
print(f"Đã xuất {len(self.df)} dòng vào {filename}")
================== VÍ DỤ SỬ DỤNG ==================
if __name__ == "__main__":
from binance_api import BinanceHistoricalData
# Lấy dữ liệu
client = BinanceHistoricalData()
klines = client.get_historical_klines(
symbol="BTCUSDT",
interval="1h",
start_date="2025-10-01",
end_date="2025-12-01"
)
# Xử lý và phân tích
processor = BinanceDataProcessor(klines)
processor.add_sma([20, 50, 200])
processor.add_rsi(14)
processor.add_macd(12, 26, 9)
processor.add_bollinger_bands(20, 2)
# In tóm tắt
summary = processor.get_technical_summary()
print("=== TÓM TẮT PHÂN TÍCH KỸ THUẬT ===")
for key, value in summary.items():
if value is not None:
print(f"{key}: {value:.2f}" if isinstance(value, float) else f"{key}: {value}")
# Xuất dữ liệu
processor.export_to_csv('btcusdt_1h.csv')
processor.export_to_json('btcusdt_1h.json')
# Vẽ biểu đồ
# processor.plot_chart(show_bb=True, show_sma=True)
Tối Ưu Hóa Hiệu Suất Với Batch Processing
#!/usr/bin/env python3
"""
Batch Processing cho nhiều cặp tiền cùng lúc
Sử dụng asyncio cho hiệu suất cao
"""
import asyncio
import aiohttp
import time
from datetime import datetime
from typing import List, Dict, Tuple
from dataclasses import dataclass
@dataclass
class TradingPair:
symbol: str
interval: str
start_date: str
end_date: str
class BatchBinanceFetcher:
"""Fetch dữ liệu cho nhiều cặp tiền song song"""
BASE_URL = "https://api.binance.com"
MAX_CONCURRENT = 5 # Số request đồng thời tối đa
RATE_LIMIT = 1200 # requests/phút
def __init__(self):
self.semaphore = asyncio.Semaphore(self.MAX_CONCURRENT)
self.request_times: List[float] = []
async def fetch_klines_async(
self,
session: aiohttp.ClientSession,
symbol: str,
interval: str,
limit: int = 1000
) -> Dict:
"""Fetch klines cho một cặp tiền"""
async with self.semaphore:
url = f"{self.BASE_URL}/api/v3/klines"
params = {
"symbol": symbol.upper(),
"interval": interval,
"limit": limit
}
try:
async with session.get(url, params=params) as response:
if response.status == 429:
await asyncio.sleep(60)
return await self.fetch_klines_async(
session, symbol, interval, limit
)
data = await response.json()
self.request_times.append(time.time())
return {
"symbol": symbol,
"interval": interval,
"count": len(data),
"data": data,
"success": True
}
except aiohttp.ClientError as e:
return {
"symbol": symbol,
"error": str(e),
"success": False
}
async def fetch_multiple(
self,
pairs: List[Tuple[str, str]]
) -> List[Dict]:
"""Fetch nhiều cặp tiền song song"""
async with aiohttp.ClientSession() as session:
tasks = [
self.fetch_klines_async(session, symbol, interval)
for symbol, interval in pairs
]
results = await asyncio.gather(*tasks)
return results
def get_rate_limit_status(self) -> Dict:
"""Kiểm tra trạng thái rate limit"""
now = time.time()
# Lọc các request trong 1 phút gần nhất
recent = [t for t in self.request_times if now - t < 60]
return {
"requests_last_minute": len(recent),
"limit": self.RATE_LIMIT,
"remaining": self.RATE_LIMIT - len(recent),
"reset_needed": len(recent) >= self.RATE_LIMIT
}
class DataAggregator:
"""Tổng hợp dữ liệu từ nhiều nguồn"""
def __init__(self):
self.data_cache: Dict[str, List] = {}
def add_data(self, key: str, data: List):
"""Thêm dữ liệu vào cache"""
self.data_cache[key] = data
def calculate_correlation(
self,
symbol1: str,
symbol2: str,
metric: str = "close"
) -> float:
"""Tính correlation giữa 2 symbol"""
if symbol1 not in self.data_cache or symbol2 not in self.data_cache:
raise ValueError("Symbol not found in cache")
data1 = self.data_cache[symbol1]
data2 = self.data_cache[symbol2]
# Lấy
Tài nguyên liên quan
Bài viết liên quan