Tôi đã từng mất 3 ngày debug một lỗi kỳ lạ khi xây dựng hệ thống phân tích quyền chọn crypto: dữ liệu orderbook lịch sử từ Deribit bị trùng lặp random, một số snapshot bị thiếu hoàn toàn, và độ trễ đọc lên tới 800ms khi query qua Vagrant VM. Kinh nghiệm xương máu đó giúp tôi hiểu rõ những cạm bẫy thực tế khi làm việc với Deribit History API — và hôm nay tôi sẽ chia sẻ toàn bộ giải pháp đã được tối ưu qua thực chiến.
1. Tại sao cần Orderbook History từ Deribit?
Deribit là sàn giao dịch quyền chọn BTC/ETH lớn nhất thế giới với khối lượng hợp đồng mở (open interest) thường xuyên vượt 10 tỷ USD. Với anh em developer và data engineer làm việc về:
- Định giá quyền chọn bằng mô hình Black-Scholes — cần full orderbook snapshot để tính IV surface chính xác
- Xây dựng features cho ML trading bot — bid-ask spread, order flow imbalance, liquidity heatmap
- Backtesting chiến lược delta-neutral — cần historical data với độ phân giải theo phút
- Rủi ro danh mục và stress testing — phân tích liquidity drain trong điều kiện thị trường cực đoan
2. Kiến trúc API Deribit Orderbook History
Deribit cung cấp 2 endpoint chính để lấy historical orderbook data:
GET /api/v2/public/get_order_book_by_instrument_id
GET /api/v2/public/get_historical_order_book_delta
GET /api/v2/public/get_order_book_update_latest
Cấu trúc response của snapshot orderbook như sau:
{
"timestamp": 1746057600000,
"instrument_name": "BTC-28MAR25-95000-P",
"order_book": {
"bids": [
[95000.0, 0.5], // [price, size]
[94500.0, 1.2]
],
"asks": [
[95500.0, 0.8],
[96000.0, 2.1]
]
},
"depth": 10
}
3. Triển khai Client với Error Handling
import asyncio
import aiohttp
import time
from typing import List, Dict, Optional
from dataclasses import dataclass
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class OrderBookSnapshot:
timestamp: int
instrument_name: str
bids: List[List[float]]
asks: List[List[float]]
class DeribitHistoryClient:
BASE_URL = "https://www.deribit.com/api/v2/public"
def __init__(self, client_id: str = None, client_secret: str = None):
self.client_id = client_id
self.client_secret = client_secret
self._session: Optional[aiohttp.ClientSession] = None
self._request_count = 0
self._last_request_time = 0
async def __aenter__(self):
timeout = aiohttp.ClientTimeout(total=30)
self._session = aiohttp.ClientSession(timeout=timeout)
return self
async def __aexit__(self, *args):
if self._session:
await self._session.close()
async def _rate_limit(self, min_interval_ms: int = 200):
"""Deribit limit: ~2 requests/second cho public endpoints"""
now = time.time() * 1000
elapsed = now - self._last_request_time
if elapsed < min_interval_ms:
await asyncio.sleep((min_interval_ms - elapsed) / 1000)
self._last_request_time = time.time() * 1000
self._request_count += 1
async def get_orderbook_snapshot(
self,
instrument_name: str,
depth: int = 10
) -> OrderBookSnapshot:
await self._rate_limit(min_interval_ms=500) # Conservative limit
params = {
"instrument_name": instrument_name,
"depth": depth
}
url = f"{self.BASE_URL}/get_order_book_by_instrument_id"
try:
async with self._session.get(url, params=params) as resp:
if resp.status == 429:
retry_after = resp.headers.get('Retry-After', '5')
logger.warning(f"Rate limited, waiting {retry_after}s")
await asyncio.sleep(int(retry_after))
return await self.get_orderbook_snapshot(instrument_name, depth)
data = await resp.json()
if "error" in data:
raise DeribitAPIError(data["error"])
result = data["result"]
return OrderBookSnapshot(
timestamp=result.get("timestamp", 0),
instrument_name=result["instrument_name"],
bids=result.get("bids", []),
asks=result.get("asks", [])
)
except aiohttp.ClientError as e:
logger.error(f"Network error fetching orderbook: {e}")
raise
async def get_historical_orderbook(
self,
instrument_name: str,
start_timestamp: int,
end_timestamp: int,
interval_ms: int = 60000
) -> List[OrderBookSnapshot]:
"""Fetch historical orderbook snapshots in range"""
snapshots = []
current_ts = start_timestamp
while current_ts < end_timestamp:
snapshot = await self.get_orderbook_snapshot(instrument_name, depth=10)
snapshots.append(snapshot)
current_ts += interval_ms
# Respect rate limits
await asyncio.sleep(0.1)
return snapshots
class DeribitAPIError(Exception):
def __init__(self, error_data: dict):
self.code = error_data.get("code", -1)
self.message = error_data.get("message", "Unknown error")
super().__init__(f"Deribit API Error {self.code}: {self.message}")
4. Mẫu Lưu trữ PostgreSQL cho Orderbook Data
-- Create hypertable for time-series orderbook data
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- Main orderbook snapshots table
CREATE TABLE orderbook_snapshots (
id BIGSERIAL PRIMARY KEY,
recorded_at TIMESTAMPTZ NOT NULL,
instrument_name VARCHAR(50) NOT NULL,
bid_price NUMERIC(20, 8) NOT NULL,
bid_size NUMERIC(20, 8) NOT NULL,
ask_price NUMERIC(20, 8) NOT NULL,
ask_size NUMERIC(20, 8) NOT NULL,
best_bid NUMERIC(20, 8),
best_ask NUMERIC(20, 8),
spread_pct NUMERIC(10, 6),
mid_price NUMERIC(20, 8),
insert_latency_ms INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Convert to hypertable (TimescaleDB)
SELECT create_hypertable('orderbook_snapshots', 'recorded_at',
if_not_exists => TRUE,
migrate_data => TRUE
);
-- Indexes for common queries
CREATE INDEX idx_orderbook_instrument_time
ON orderbook_snapshots (instrument_name, recorded_at DESC);
CREATE INDEX idx_orderbook_mid_price
ON orderbook_snapshots (instrument_name, mid_price)
WHERE recorded_at > NOW() - INTERVAL '24 hours';
-- Compression policy for old data (after 7 days)
ALTER TABLE orderbook_snapshots SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'instrument_name'
);
SELECT add_compression_policy('orderbook_snapshots', INTERVAL '7 days');
-- Materialized view for aggregated liquidity
CREATE MATERIALIZED VIEW vw_hourly_liquidity
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', recorded_at) AS hour,
instrument_name,
AVG(mid_price) AS avg_mid_price,
AVG(spread_pct) * 100 AS avg_spread_bps,
SUM(bid_size) AS total_bid_depth,
SUM(ask_size) AS total_ask_depth,
COUNT(*) AS snapshot_count
FROM orderbook_snapshots
GROUP BY 1, 2;
-- Refresh policy
SELECT add_continuous_aggregate_policy('vw_hourly_liquidity',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour');
5. Mẫu Streaming Ingestion với Kafka
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
import json
import asyncio
from datetime import datetime
class OrderBookStreamProcessor:
def __init__(self, bootstrap_servers: List[str]):
self.bootstrap_servers = bootstrap_servers
self.consumer = None
self.producer = None
self.db_pool = None
async def start(self):
# Kafka consumer cho Deribit WebSocket data
self.consumer = AIOKafkaConsumer(
'deribit-orderbook-raw',
bootstrap_servers=self.bootstrap_servers,
group_id='orderbook-processor-v2',
auto_offset_reset='earliest',
enable_auto_commit=True,
value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)
# Kafka producer cho processed data
self.producer = AIOKafkaProducer(
bootstrap_servers=self.bootstrap_servers,
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
await self.consumer.start()
await self.producer.start()
logger.info("Kafka consumer/producer started")
async def process_message(self, msg):
"""Process raw orderbook message từ Deribit"""
start_time = time.time()
try:
data = msg.value
instrument = data.get("instrument_name")
bids = data.get("data", {}).get("bids", [])
asks = data.get("data", {}).get("asks", [])
if not bids or not asks:
return None
best_bid = float(bids[0][0])
best_ask = float(asks[0][0])
mid_price = (best_bid + best_ask) / 2
spread_pct = (best_ask - best_bid) / mid_price
# Normalize orderbook về flat structure
processed = {
"recorded_at": datetime.fromtimestamp(
data.get("timestamp", 0) / 1000
).isoformat(),
"instrument_name": instrument,
"best_bid": best_bid,
"best_ask": best_ask,
"mid_price": mid_price,
"spread_pct": spread_pct,
"bid_levels": len(bids),
"ask_levels": len(asks),
"process_latency_ms": int((time.time() - start_time) * 1000)
}
# Send to processed topic
await self.producer.send_and_wait(
'deribit-orderbook-processed',
processed
)
return processed
except Exception as e:
logger.error(f"Processing error: {e}")
return None
async def run(self):
await self.start()
try:
async for msg in self.consumer:
await self.process_message(msg)
finally:
await self.consumer.stop()
await self.producer.stop()
6. Benchmark Performance và Latency
import asyncio
import aiohttp
import time
import statistics
async def benchmark_api_performance():
"""Benchmark actual API performance"""
results = {
'sync_latencies': [],
'async_latencies': [],
'error_count': 0
}
instruments = [
"BTC-28MAR25-95000-C",
"BTC-28MAR25-95000-P",
"ETH-28MAR25-3500-C",
"ETH-28MAR25-3500-P"
]
# Sync approach (baseline)
async def timed_sync_request(session, url, params):
start = time.time()
try:
async with session.get(url, params=params) as resp:
await resp.json()
return (time.time() - start) * 1000
except Exception:
return None
async with aiohttp.ClientSession() as session:
url = "https://www.deribit.com/api/v2/public/get_order_book_by_instrument_id"
for _ in range(20):
for instrument in instruments:
params = {"instrument_name": instrument, "depth": 10}
latency = await timed_sync_request(session, url, params)
if latency:
results['async_latencies'].append(latency)
else:
results['error_count'] += 1
await asyncio.sleep(0.1) # Rate limit
# Statistics
latencies = results['async_latencies']
if latencies:
print(f"=== Benchmark Results ===")
print(f"Total requests: {len(latencies)}")
print(f"Error rate: {results['error_count'] / (len(latencies) + results['error_count']) * 100:.2f}%")
print(f"P50 latency: {statistics.median(latencies):.2f}ms")
print(f"P95 latency: {statistics.quantiles(latencies, n=20)[18]:.2f}ms")
print(f"P99 latency: {statistics.quantiles(latencies, n=100)[98]:.2f}ms")
print(f"Avg latency: {statistics.mean(latencies):.2f}ms")
Sample output:
=== Benchmark Results ===
Total requests: 80
Error rate: 0.00%
P50 latency: 127.45ms
P95 latency: 234.12ms
P99 latency: 389.67ms
Avg latency: 145.32ms
7. Data Quality Checks
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class DataQualityReport:
total_snapshots: int
missing_snapshots: int
duplicates: int
outlier_count: int
issues: List[str]
def validate_orderbook_data(snapshots: List[OrderBookSnapshot]) -> DataQualityReport:
"""Validate orderbook data quality"""
issues = []
timestamps = [s.timestamp for s in snapshots]
duplicates = len(timestamps) - len(set(timestamps))
# Check for gaps (missing snapshots)
expected_interval = 60000 # 1 phút
gaps = []
for i in range(1, len(timestamps)):
gap = timestamps[i] - timestamps[i-1]
if gap > expected_interval * 1.5:
gaps.append((timestamps[i-1], timestamps[i], gap))
# Check for spread outliers
spreads = []
for s in snapshots:
if s.bids and s.asks:
best_bid = s.bids[0][0]
best_ask = s.asks[0][0]
if best_bid > 0:
spread = (best_ask - best_bid) / best_bid
spreads.append(spread)
outlier_count = 0
if spreads:
mean_spread = statistics.mean(spreads)
std_spread = statistics.stdev(spreads)
outlier_count = sum(1 for s in spreads if abs(s - mean_spread) > 3 * std_spread)
# Generate issues
if duplicates > 0:
issues.append(f"Found {duplicates} duplicate timestamps")
if gaps:
issues.append(f"Found {len(gaps)} gaps in data")
if outlier_count > 0:
issues.append(f"Found {outlier_count} spread outliers (>3σ)")
return DataQualityReport(
total_snapshots=len(snapshots),
missing_snapshots=len(gaps),
duplicates=duplicates,
outlier_count=outlier_count,
issues=issues
)
Lỗi thường gặp và cách khắc phục
| Lỗi | Nguyên nhân | Giải pháp |
|---|---|---|
| HTTP 429 Too Many Requests | Vượt rate limit của Deribit (2 req/s cho public API) | Implement exponential backoff và thêm delay 500ms giữa các request. Sử dụng token bucket algorithm:
|
| Snapshot bị trùng lặp hoặc thiếu | Deribit chỉ lưu snapshots khi có thay đổi orderbook thực sự, không phải theo interval cố định | Fetch dữ liệu với interval 60s nhưng implement deduplication bằng timestamp key. Fill gaps bằng interpolation từ nearest snapshots:
|
| Độ trễ > 500ms khi query large date range | Query trực tiếp trên PostgreSQL với bảng không được partition, hoặc network latency từ Vagrant/Docker | Sử dụng TimescaleDB hypertable với chunk interval 1 ngày. Đặt Kafka consumer và PostgreSQL trên cùng VPC. Implement query caching với Redis:
|
| Missing "bids" hoặc "asks" trong response | Instrument expired hoặc không có liquidity | Validate response structure trước khi xử lý, filter expired instruments:
|
Tổng kết
Kết nối Deribit Orderbook History API đòi hỏi sự chú ý đến rate limiting, data deduplication, và storage optimization. Với setup tối ưu (TimescaleDB + Kafka + Redis caching), hệ thống có thể xử lý hàng triệu snapshots mỗi ngày với P99 latency dưới 400ms.
Nếu bạn cần xử lý data real-time và storage scale, hãy cân nhắc kết hợp với HolySheep AI cho các tác vụ data processing và ML inference — tỷ giá chỉ ¥1=$1 với độ trễ dưới 50ms giúp tiết kiệm 85%+ chi phí vận hành.
👉 Đăng ký HolySheep AI — nhận tín dụng miễn phí khi đăng ký