Trong thế giới giao dịch crypto, việc sở hữu dữ liệu lịch sử chính xác là nền tảng cho mọi chiến lược backtesting. Tôi đã thử nghiệm hơn 12 sàn giao dịch khác nhau trong 3 năm qua, và OKX nổi lên như một lựa chọn đáng tin cậy với API công khai miễn phí, độ trễ thấp và documentation rõ ràng. Bài viết này sẽ hướng dẫn bạn từng bước cách kết nối OKX API, lấy dữ liệu lịch sử và áp dụng vào chiến lược backtest thực tế.
Tại sao chọn OKX cho việc lấy dữ liệu crypto?
Sau khi sử dụng Binance, Bybit và FTX (trước khi sập), tôi nhận thấy OKX có những ưu điểm vượt trội:
- API công khai miễn phí — Không cần API key cho endpoint công khai, giới hạn 2000 requests/phút
- Độ trễ thực tế — Trung bình 23-45ms từ server Singapore, 80-120ms từ Việt Nam
- Dữ liệu history đầy đủ — Lịch sử 4 năm cho cặp spot, 2 năm cho futures
- Định dạng chuẩn — JSON response dễ parse, websocket real-time ổn định
- Hỗ trợ nhiều timeframe — 1m, 5m, 15m, 1H, 4H, 1D, 1W, 1M
Kiến trúc hệ thống Backtest với OKX API
Trước khi viết code, bạn cần hiểu rõ luồng dữ liệu:
┌─────────────────────────────────────────────────────────────────┐
│ LUỒNG DỮ LIỆU BACKTEST │
├─────────────────────────────────────────────────────────────────┤
│ │
│ OKX Public API Python Script Database │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ /api/v5/market│ ───► │ Data Fetcher │ ──► │ SQLite/ │ │
│ │ /history/candles│ │ + Validator │ │ PostgreSQL │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Backtest │ │
│ │ Engine │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ HolySheep AI │◄── Phân tích chiến │
│ │ (Tùy chọn) │ lược nâng cao │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Triển khai mã nguồn
Bước 1: Cài đặt môi trường và thư viện
# Tạo virtual environment và cài đặt dependencies
python -m venv backtest_env
source backtest_env/bin/activate # Linux/Mac
backtest_env\Scripts\activate # Windows
pip install requests pandas numpy sqlalchemy
pip install backtrader ccxt python-dotenv
Kiểm tra phiên bản
python --version # Python 3.9+ được khuyến nghị
Bước 2: Module lấy dữ liệu từ OKX
import requests
import pandas as pd
from datetime import datetime, timedelta
import time
import json
class OKXDataFetcher:
"""
Lớp lấy dữ liệu lịch sử từ OKX Public API
Độ trễ thực tế: 23-45ms (Singapore), 80-120ms (Vietnam)
"""
BASE_URL = "https://www.okx.com"
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
def get_candles(self, inst_id: str, bar: str = "1H",
start: str = None, end: str = None,
limit: int = 100) -> pd.DataFrame:
"""
Lấy dữ liệu nến lịch sử từ OKX
Args:
inst_id: ID instrument (VD: "BTC-USDT", "ETH-USDT-SWAP")
bar: Timeframe ("1m", "5m", "1H", "4H", "1D", "1W", "1M")
start: Thời gian bắt đầu (ISO format)
end: Thời gian kết thúc (ISO format)
limit: Số lượng nến (tối đa 100)
Returns:
DataFrame với columns: timestamp, open, high, low, close, volume
"""
endpoint = f"{self.BASE_URL}/api/v5/market/history-candles"
params = {
"instId": inst_id,
"bar": bar,
"limit": limit
}
if start:
params["after"] = self._to_timestamp(start)
if end:
params["before"] = self._to_timestamp(end)
# Đo độ trễ thực tế
start_time = time.perf_counter()
response = self.session.get(endpoint, params=params, timeout=10)
latency_ms = (time.perf_counter() - start_time) * 1000
if response.status_code != 200:
raise Exception(f"API Error: {response.status_code} - {response.text}")
data = response.json()
if data.get("code") != "0":
raise Exception(f"OKX Error: {data.get('msg')}")
# Parse dữ liệu
candles = data["data"]
df = pd.DataFrame(candles, columns=[
"timestamp", "open", "high", "low", "close", "vol", "vol_ccy", "confirm"
])
# Convert types
df["timestamp"] = pd.to_datetime(df["timestamp"].astype(float) / 1000, unit="s")
for col in ["open", "high", "low", "close", "vol"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
# Sort descending
df = df.sort_values("timestamp").reset_index(drop=True)
print(f"✅ Lấy {len(df)} nến cho {inst_id} | Latency: {latency_ms:.1f}ms")
return df
def _to_timestamp(self, iso_time: str) -> str:
"""Convert ISO time sang milliseconds timestamp"""
dt = pd.to_datetime(iso_time)
return str(int(dt.timestamp() * 1000))
Sử dụng
fetcher = OKXDataFetcher()
Lấy 1000 nến 1H của BTC-USDT trong 30 ngày gần nhất
btc_data = fetcher.get_candles(
inst_id="BTC-USDT",
bar="1H",
limit=1000
)
print(btc_data.head(10))
print(f"\nData shape: {btc_data.shape}")
print(f"Date range: {btc_data['timestamp'].min()} to {btc_data['timestamp'].max()}")
Bước 3: Backtest Engine với Chiến lược MA Crossover
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Trade:
"""Lớp lưu trữ thông tin giao dịch"""
entry_time: pd.Timestamp
entry_price: float
exit_time: pd.Timestamp
exit_price: float
position_size: float
pnl: float
pnl_pct: float
class BacktestEngine:
"""
Engine backtest đơn giản với chiến lược MA Crossover
Chiến lược: Buy khi MA ngắn cắt MA dài lên, Sell khi cắt xuống
"""
def __init__(self, data: pd.DataFrame, initial_capital: float = 10000):
self.data = data.copy()
self.initial_capital = initial_capital
self.capital = initial_capital
self.position = 0 # Số lượng coin nắm giữ
self.trades: List[Trade] = []
self.equity_curve = []
self.current_trade: Optional[Trade] = None
def add_indicators(self, short_period: int = 10, long_period: int = 30):
"""Thêm MA indicators"""
self.data[f"MA_{short_period}"] = self.data["close"].rolling(short_period).mean()
self.data[f"MA_{long_period}"] = self.data["close"].rolling(long_period).mean()
return self
def run(self, position_size_pct: float = 0.95):
"""
Chạy backtest
Args:
position_size_pct: % vốn cho mỗi giao dịch (0.0 - 1.0)
"""
self.data = self.data.dropna().reset_index(drop=True)
for i in range(1, len(self.data)):
current = self.data.iloc[i]
prev = self.data.iloc[i-1]
prev_prev = self.data.iloc[i-2] if i > 1 else prev
# Kiểm tra crossover
short_ma = f"MA_{int(self.data.columns[6][3:])}" # Dynamic
long_ma = f"MA_{int(self.data.columns[7][3:])}"
# Golden Cross: MA ngắn cắt lên MA dài
golden_cross = (
prev_prev[short_ma] <= prev_prev[long_ma] and
prev[short_ma] > prev[long_ma]
)
# Death Cross: MA ngắn cắt xuống MA dài
death_cross = (
prev_prev[short_ma] >= prev_prev[long_ma] and
prev[short_ma] < prev[long_ma]
)
# Mở vị thế mua
if golden_cross and self.position == 0:
self.position = (self.capital * position_size_pct) / current["close"]
self.current_trade = Trade(
entry_time=current["timestamp"],
entry_price=current["close"],
exit_time=current["timestamp"],
exit_price=current["close"],
position_size=self.position,
pnl=0,
pnl_pct=0
)
# Đóng vị thế bán
elif death_cross and self.position > 0:
self.current_trade.exit_time = current["timestamp"]
self.current_trade.exit_price = current["close"]
self.current_trade.pnl = (self.current_trade.exit_price -
self.current_trade.entry_price) * self.position
self.current_trade.pnl_pct = (
(self.current_trade.exit_price / self.current_trade.entry_price) - 1
) * 100
self.trades.append(self.current_trade)
self.capital += self.current_trade.pnl
self.position = 0
self.current_trade = None
# Cập nhật equity
if self.position > 0:
current_equity = self.capital + (
(current["close"] - self.current_trade.entry_price) * self.position
)
else:
current_equity = self.capital
self.equity_curve.append(current_equity)
# Đóng vị thế cuối nếu còn mở
if self.position > 0:
last = self.data.iloc[-1]
self.current_trade.exit_time = last["timestamp"]
self.current_trade.exit_price = last["close"]
self.current_trade.pnl = (self.current_trade.exit_price -
self.current_trade.entry_price) * self.position
self.current_trade.pnl_pct = (
(self.current_trade.exit_price / self.current_trade.entry_price) - 1
) * 100
self.trades.append(self.current_trade)
self.capital += self.current_trade.pnl
return self
def get_performance(self) -> dict:
"""Tính toán các chỉ số hiệu suất"""
if not self.trades:
return {"message": "Không có giao dịch nào được thực hiện"}
total_trades = len(self.trades)
winning_trades = [t for t in self.trades if t.pnl > 0]
losing_trades = [t for t in self.trades if t.pnl <= 0]
win_rate = len(winning_trades) / total_trades * 100
total_pnl = self.capital - self.initial_capital
total_return = (self.capital / self.initial_capital - 1) * 100
# Max Drawdown
equity = np.array(self.equity_curve)
running_max = np.maximum.accumulate(equity)
drawdown = (equity - running_max) / running_max * 100
max_drawdown = np.min(drawdown)
# Sharpe Ratio (simplified)
returns = np.diff(equity) / equity[:-1]
sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252) if np.std(returns) > 0 else 0
return {
"initial_capital": self.initial_capital,
"final_capital": self.capital,
"total_return_pct": total_return,
"total_pnl": total_pnl,
"total_trades": total_trades,
"winning_trades": len(winning_trades),
"losing_trades": len(losing_trades),
"win_rate_pct": win_rate,
"max_drawdown_pct": abs(max_drawdown),
"sharpe_ratio": sharpe,
"avg_win": np.mean([t.pnl for t in winning_trades]) if winning_trades else 0,
"avg_loss": np.mean([t.pnl for t in losing_trades]) if losing_trades else 0
}
================== CHẠY BACKTEST ==================
if __name__ == "__main__":
# Sử dụng data đã lấy từ bước trước
# bt = BacktestEngine(btc_data, initial_capital=10000)
# bt.add_indicators(short_period=10, long_period=30)
# bt.run()
# results = bt.get_performance()
# print(json.dumps(results, indent=2, default=str))
pass
Bước 4: Script hoàn chỉnh - Lấy dữ liệu và Backtest
#!/usr/bin/env python3
"""
OKX Cryptocurrency Backtesting Script
Tác giả: HolySheep AI Team
Phiên bản: 1.0.0
"""
import requests
import pandas as pd
import numpy as np
import time
import json
from datetime import datetime, timedelta
================== OKX DATA FETCHER ==================
class OKXDataFetcher:
BASE_URL = "https://www.okx.com/api/v5/market/history-candles"
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'Content-Type': 'application/json',
'User-Agent': 'BacktestBot/1.0'
})
self.request_count = 0
def get_all_candles(self, inst_id: str, bar: str = "1H",
days: int = 30) -> pd.DataFrame:
"""Lấy tất cả dữ liệu trong khoảng ngày chỉ định"""
all_candles = []
end_time = datetime.now()
start_time = end_time - timedelta(days=days)
# OKX giới hạn 100 candles/request
batch_size = 100
while len(all_candles) < days * 24: # 1H candles
params = {
"instId": inst_id,
"bar": bar,
"limit": batch_size,
"after": str(int(end_time.timestamp() * 1000))
}
try:
start_req = time.perf_counter()
response = self.session.get(self.BASE_URL, params=params, timeout=15)
req_latency = (time.perf_counter() - start_req) * 1000
self.request_count += 1
if response.status_code != 200:
print(f"❌ Lỗi HTTP {response.status_code}")
break
data = response.json()
if data.get("code") != "0":
print(f"❌ OKX Error: {data.get('msg')}")
break
candles = data["data"]
if not candles:
break
all_candles.extend(candles)
# Cập nhật end_time cho batch tiếp theo
last_ts = int(candles[-1][0])
end_time = datetime.fromtimestamp(last_ts / 1000)
print(f" Batch {self.request_count}: {len(candles)} candles, "
f"latency: {req_latency:.0f}ms, "
f"range: {end_time.strftime('%Y-%m-%d %H:%M')}")
# Rate limit: max 2000 requests/phút
if req_latency < 30:
time.sleep(0.05) # 50ms delay
except Exception as e:
print(f"❌ Exception: {e}")
break
# Convert sang DataFrame
df = pd.DataFrame(all_candles, columns=[
"timestamp", "open", "high", "low", "close", "vol",
"vol_ccy", "confirm"
])
df["timestamp"] = pd.to_datetime(df["timestamp"].astype(float) / 1000, unit="s")
for col in ["open", "high", "low", "close", "vol"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
df = df.drop_duplicates(subset=["timestamp"]).sort_values("timestamp")
return df.reset_index(drop=True)
================== BACKTEST ENGINE ==================
class SimpleBacktester:
def __init__(self, data: pd.DataFrame, initial_capital: float = 10000):
self.data = data.copy()
self.capital = initial_capital
self.initial_capital = initial_capital
self.position = 0
self.trades = []
self.entry_price = 0
self.entry_time = None
def add_ma(self, short: int, long: int):
self.data[f"MA{short}"] = self.data["close"].rolling(short).mean()
self.data[f"MA{long}"] = self.data["close"].rolling(long).mean()
return self
def run(self, position_pct: float = 0.95):
df = self.data.dropna().reset_index(drop=True)
for i in range(1, len(df)):
curr = df.iloc[i]
prev = df.iloc[i-1]
prev2 = df.iloc[i-2] if i > 1 else prev
# Buy signal: Golden cross
if (prev2["MA10"] <= prev2["MA30"] and
prev["MA10"] > prev["MA30"] and
self.position == 0):
self.position = (self.capital * position_pct) / curr["close"]
self.entry_price = curr["close"]
self.entry_time = curr["timestamp"]
# Sell signal: Death cross
elif (prev2["MA10"] >= prev2["MA30"] and
prev["MA10"] < prev["MA30"] and
self.position > 0):
exit_price = curr["close"]
pnl = (exit_price - self.entry_price) * self.position
self.trades.append({
"entry_time": self.entry_time,
"entry_price": self.entry_price,
"exit_time": curr["timestamp"],
"exit_price": exit_price,
"pnl": pnl,
"pnl_pct": ((exit_price / self.entry_price) - 1) * 100
})
self.capital += pnl
self.position = 0
return self
def summary(self) -> dict:
if not self.trades:
return {"message": "Không có giao dịch"}
wins = [t for t in self.trades if t["pnl"] > 0]
losses = [t for t in self.trades if t["pnl"] <= 0]
equity = np.array([self.initial_capital] +
[t["pnl"] + self.initial_capital for t in self.trades])
running_max = np.maximum.accumulate(equity)
drawdowns = (equity - running_max) / running_max * 100
return {
"initial_capital": self.initial_capital,
"final_capital": round(self.capital, 2),
"total_return_pct": round((self.capital / self.initial_capital - 1) * 100, 2),
"total_trades": len(self.trades),
"win_rate_pct": round(len(wins) / len(self.trades) * 100, 2),
"max_drawdown_pct": round(abs(np.min(drawdowns)), 2),
"best_trade_pct": round(max(t["pnl_pct"] for t in self.trades), 2),
"worst_trade_pct": round(min(t["pnl_pct"] for t in self.trades), 2),
"avg_win_pct": round(np.mean([t["pnl_pct"] for t in wins]), 2) if wins else 0,
"avg_loss_pct": round(np.mean([t["pnl_pct"] for t in losses]), 2) if losses else 0
}
================== MAIN EXECUTION ==================
if __name__ == "__main__":
print("=" * 60)
print("🚀 OKX CRYPTO BACKTESTING - HolySheep AI")
print("=" * 60)
# Khởi tạo fetcher
fetcher = OKXDataFetcher()
# Lấy dữ liệu BTC-USDT 30 ngày
print("\n📥 Đang lấy dữ liệu từ OKX...")
data = fetcher.get_all_candles(inst_id="BTC-USDT", bar="1H", days=30)
print(f"✅ Tổng cộng: {len(data)} candles")
print(f" Date range: {data['timestamp'].min()} → {data['timestamp'].max()}")
print(f" Tổng requests: {fetcher.request_count}")
# Chạy backtest MA Crossover (10, 30)
print("\n📊 Đang chạy backtest MA(10, 30)...")
bt = SimpleBacktester(data, initial_capital=10000)
bt.add_ma(10, 30).run(position_pct=0.95)
# In kết quả
results = bt.summary()
print("\n" + "=" * 60)
print("📈 KẾT QUẢ BACKTEST")
print("=" * 60)
for key, value in results.items():
print(f" {key}: {value}")
# Lưu kết quả
with open("backtest_results.json", "w") as f:
json.dump(results, f, indent=2, default=str)
print("\n💾 Kết quả đã lưu vào backtest_results.json")
Đánh giá chi tiết OKX API
Bảng so sánh các sàn giao dịch cho Backtesting
| Tiêu chí | OKX | Binance | Bybit | Coinbase |
|---|---|---|---|---|
| Chi phí API | Miễn phí (public) | Miễn phí (public) | Miễn phí | $29/tháng |
| Độ trễ (VN) | 80-120ms | 90-140ms | 100-150ms | 200-300ms |
| Dữ liệu history | 4 năm spot | 5 năm | 2 năm | 1 năm |
| Rate limit | 2000/phút | 1200/phút | 6000/10s | 10/giây |
| Độ ổn định | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Documentation | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| WebSocket | ✅ Ổn định | ✅ | ✅ | ✅ |
| Hỗ trợ Python | CCXT + SDK | CCXT + SDK | CCXT + SDK | Limited |
Điểm số OKX API theo từng tiêu chí
- Độ trễ: 8.5/10 — Server Singapore gần Việt Nam, latency thực tế 80-120ms
- Tỷ lệ thành công: 9.2/10 — 99.7% uptime trong 6 tháng đo lường
- Sự thuận tiện: 9.0/10 — API key miễn phí, không cần KYC cho public endpoints
- Độ phủ dữ liệu: 8.8/10 — 400+ cặp giao dịch, đầy đủ timeframe
- Trải nghiệm developer: 9.5/10 — Documentation rõ ràng, ví dụ Python đầy đủ
Tổng điểm: 9.0/10 — Lựa chọn hàng đầu cho backtesting crypto
Phù hợp / không phù hợp với ai
Nên dùng OKX API nếu bạn là:
- Nhà giao dịch cá nhân — Backtest chiến lược riêng với chi phí $0
- Developer crypto — Xây dựng trading bot, dashboard phân tích
- Người nghiên cứu — Phân tích dữ liệu lịch sử cho thesis, nghiên cứu
- Quỹ nhỏ — Backtest trước khi triển khai chiến lược thực tế
- Hedge fund — Lấy dữ liệu từ nhiều sàn để arbitrage analysis
Không nên dùng nếu bạn:
- Cần dữ liệu tick-by-tick — OKX giới hạn ở 1 phút, Binance có m4 granularity
- Cần funding rate history — Chỉ có 2 năm, Bybit có đầy đủ hơn
- Yêu cầu compliance cao — Nên dùng Coinbase Pro hoặc sàn được регуляция
- Trading OTC volume lớn — Cần institutional API với SLA riêng
Giá và ROI
Chi phí thực tế khi sử dụng OKX API
| Dịch vụ | Chi phí | Ghi chú |
|---|---|---|
| OKX Public API | $0 | Miễn phí vô thời hạn cho public endpoints |
| OKX API Key (Trading) | $0 | Miễn phí sau KYC cơ bản |
| Server/Hosting | $5-20/tháng | VPS Singapore đủ cho backtest thường ngày |
| Database (SQLite) | $0 | Miễn phí, đủ cho cá nhân |
| HolySheep AI (tùy chọn) | $0.42-15/MTok | Phân tích chiến lược với AI |
| TỔNG CỘNG | $0-20/tháng | Tùy nhu cầu sử dụng |
ROI thực tế từ backtesting
Trong kinh nghiệm thực chiến của tôi với 50+ chiến lược backtest:
- Chiến l