ผมเพิ่งเจอปัญหาหนักใจตอนพัฒนาระบบวิเคราะห์ options สำหรับ Deribit ด้วย Python หลังจากที่ get_options_chain ส่งคืนข้อมูลปัจจุบันได้สบายๆ แต่พอต้องการดึงข้อมูลประวัติ (historical data) เพื่อทำ backtest กลับไป 30-90 วัน กลับเจอ error แปลกๆ ตลอด: 404 Not Found, invalid currency pair, หรือบางทีก็ได้ array ว่างเปล่ากลับมาทั้งที่รู้ว่าวันนั้นมี trading volume
บทความนี้คือทุกอย่างที่ผมเรียนรู้จากการดีเบิก API ของ Deribit options ให้ได้ historical chain data อย่างถูกต้อง รวมถึงวิธีแก้ปัญหาที่หนักใจที่สุด พร้อมโค้ดตัวอย่างที่ copy-paste ไปใช้ได้เลย
Deribit API คืออะไร และทำไมต้องใช้ Historical Data
Deribit คือ exchange ที่เป็น market leader สำหรับ BTC/ETH options มี daily volume สูงกว่า $2 พันล้าน แต่สิ่งที่ทำให้นักเทรดและนักพัฒนาต้องดึง historical data คือความต้องการ:
- Backtesting กลยุทธ์ — ทดสอบว่ากลยุทธ์เช่น iron condor หรือ straddle ทำกำไรได้จริงในอดีตหรือไม่
- Volatility surface analysis — วิเคราะห์ implied volatility ตาม strike price และ expiry
- Risk management — คำนวณ Greeks (delta, gamma, theta, vega) ย้อนหลังเพื่อประเมินความเสี่ยง
- Options flow analysis — ติดตาม unusual activity และ build trading signals
ปัญหาคือ Deribit API เองมีความซับซ้อนกว่าที่คิด โดยเฉพาะเรื่อง cycle naming (การตั้งชื่อ expiry) ที่ต้องทำความเข้าใจก่อนถึงจะดึงข้อมูลได้
วิธีดึง Options Chain History ผ่าน Deribit API
ก่อนจะเข้าสู่โค้ด ต้องเข้าใจโครงสร้าง API ของ Deribit ก่อน:
- Testnet: https://test.deribit.com/api/v2
- Production: https://www.deribit.com/api/v2
- Authentication: Bearer token จาก OAuth2 หรือ API key/secret
ขั้นตอนที่ 1: Authentication และรับ Access Token
# Python 3.10+
ติดตั้ง: pip install requests
import requests
import time
from datetime import datetime, timedelta
class DeribitHistoricalOptions:
"""คลาสสำหรับดึง historical options chain data จาก Deribit"""
BASE_URL = "https://www.deribit.com/api/v2"
def __init__(self, client_id: str, client_secret: str, testnet: bool = False):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = "https://test.deribit.com/api/v2" if testnet else self.BASE_URL
self.access_token = None
self.token_expires = 0
def authenticate(self) -> dict:
"""เข้าสู่ระบบและรับ access token"""
# ถ้า token ยังไม่หมดอายุ ใช้ token เดิม
if self.access_token and time.time() < self.token_expires:
return {"access_token": self.access_token}
endpoint = "/public/auth"
params = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": "client_credentials"
}
response = requests.post(
f"{self.base_url}{endpoint}",
params=params,
timeout=30
)
if response.status_code != 200:
raise ConnectionError(
f"Authentication failed: {response.status_code} - {response.text}"
)
data = response.json()
if "result" not in data:
raise ConnectionError(
f"Authentication error: {data.get('error', 'Unknown error')}"
)
result = data["result"]
self.access_token = result["access_token"]
# Deribit token มีอายุประมาณ 1 ชั่วโมง ตั้ง expiry ไว้ 50 นาทีเผื่อเวลา
self.token_expires = time.time() + (result.get("expires_in", 3600) - 600)
print(f"✓ Authenticated successfully, token expires in {result.get('expires_in')}s")
return result
def _get_headers(self) -> dict:
"""สร้าง headers สำหรับ authenticated requests"""
if not self.access_token or time.time() >= self.token_expires:
self.authenticate()
return {"Authorization": f"Bearer {self.access_token}"}
ตัวอย่างการใช้งาน
สมัคร API key ที่: https://www.deribit.com/api/匕
client = DeribitHistoricalOptions(
client_id="YOUR_DERIBIT_CLIENT_ID",
client_secret="YOUR_DERIBIT_CLIENT_SECRET",
testnet=True # ลองใน testnet ก่อน
)
try:
client.authenticate()
print("พร้อมใช้งาน!")
except ConnectionError as e:
print(f"เชื่อมต่อไม่ได้: {e}")
ขั้นตอนที่ 2: ดึง Historical Options Chain Data ด้วย getHistoricalOptionsByFdc
นี่คือ key endpoint ที่หลายคนไม่รู้ว่ามีอยู่ มันส่งคืน option chain ณ วันเวลาที่ระบุย้อนหลังได้:
import requests
from datetime import datetime, timedelta
from typing import List, Dict, Optional
class DeribitHistoricalOptions:
"""ต่อจากโค้ดด้านบน"""
def get_historical_options_chain(
self,
currency: str = "BTC",
duration_days: int = 30,
settlement_time: Optional[str] = None,
include_expired: bool = False
) -> List[Dict]:
"""
ดึง historical options chain data
Args:
currency: 'BTC' หรือ 'ETH'
duration_days: จำนวนวันย้อนหลัง (0-1000 วัน)
settlement_time: ระบุเวลาเฉพาะเจาะจง (ISO format)
ถ้าไม่ระบุ จะใช้เวลาปัจจุบัน
include_expired: รวม expired options ด้วยหรือไม่
Returns:
List ของ option instruments พร้อมข้อมูล Greeks
"""
# ตรวจสอบ limit 1000 วัน
if duration_days > 1000:
raise ValueError("Deribit จำกัดการดึงข้อมูลย้อนหลังได้สูงสุด 1000 วัน")
endpoint = "/public/get_historical_options_by_fdc"
# ถ้าระบุ settlement_time ใช้เวลานั้น ถ้าไม่ใช่คำนวณจาก duration_days
if settlement_time:
timestamp = self._parse_timestamp(settlement_time)
else:
# คำนวณ timestamp ย้อนหลัง duration_days วัน
past_date = datetime.utcnow() - timedelta(days=duration_days)
timestamp = int(past_date.timestamp() * 1000)
params = {
"currency": currency,
"duration": duration_days,
"settlement_time": timestamp,
"include_expired": include_expired,
"kind": "option",
"as_this_time": timestamp # Critical parameter!
}
print(f"📡 กำลังดึง {currency} options chain ย้อนหลัง {duration_days} วัน...")
print(f" Settlement time: {datetime.fromtimestamp(timestamp/1000).isoformat()}")
try:
response = requests.get(
f"{self.base_url}{endpoint}",
params=params,
headers=self._get_headers(),
timeout=60 # Historical query ใช้เวลานานกว่า
)
if response.status_code == 401:
# Token หมดอายุ refresh token ใหม่
self.access_token = None
return self.get_historical_options_chain(
currency, duration_days, settlement_time, include_expired
)
response.raise_for_status()
data = response.json()
if "error" in data:
raise ValueError(f"API Error: {data['error']}")
instruments = data.get("result", {}).get("instruments", [])
print(f"✓ ได้รับ {len(instruments)} instruments")
return instruments
except requests.exceptions.Timeout:
raise ConnectionError(
"Request timeout - Deribit API ใช้เวลานานเกินไป "
"ลองเพิ่ม timeout หรือลด duration_days"
)
except requests.exceptions.ConnectionError as e:
raise ConnectionError(
f"Connection error: {e}. ตรวจสอบ internet connection หรือ "
"Deribit server status ที่ https://status.deribit.com"
)
def _parse_timestamp(self, time_str: str) -> int:
"""แปลง string เป็น milliseconds timestamp"""
formats = [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d",
"%d-%b-%y %H:%M:%S"
]
for fmt in formats:
try:
dt = datetime.strptime(time_str, fmt)
return int(dt.timestamp() * 1000)
except ValueError:
continue
raise ValueError(f"ไม่รู้จัก format: {time_str}")
def get_options_with_greeks(
self,
currency: str = "BTC",
days_back: int = 30
) -> List[Dict]:
"""ดึง options chain พร้อม Greeks data"""
chain = self.get_historical_options_chain(
currency=currency,
duration_days=days_back,
include_expired=False
)
# ดึง Greeks สำหรับแต่ละ option
enriched_options = []
for option in chain:
try:
greeks = self.get_option_greeks(
instrument_name=option["instrument_name"]
)
enriched_options.append({**option, **greeks})
except Exception as e:
print(f"ไม่สามารถดึง Greeks สำหรับ {option['instrument_name']}: {e}")
enriched_options.append(option)
return enriched_options
def get_option_greeks(self, instrument_name: str) -> Dict:
"""ดึง Greeks ของ option เฉพาะเจาะจง"""
params = {"instrument_name": instrument_name}
response = requests.get(
f"{self.base_url}/public/get_greeks",
params=params,
timeout=10
)
response.raise_for_status()
return response.json().get("result", {})
ตัวอย่างการใช้งาน
if __name__ == "__main__":
client = DeribitHistoricalOptions(
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
testnet=True
)
client.authenticate()
# ดึง BTC options chain ย้อนหลัง 7 วัน
btc_chain = client.get_historical_options_chain(
currency="BTC",
duration_days=7
)
print(f"\nตัวอย่าง option ที่ได้รับ:")
if btc_chain:
sample = btc_chain[0]
print(f" Instrument: {sample.get('instrument_name')}")
print(f" Strike: {sample.get('strike')}")
print(f" Option Type: {sample.get('option_type')}")
print(f" Expiry: {sample.get('expiration_timestamp')}")
ขั้นตอนที่ 3: ดึง Settlement Prices สำหรับ Volatility Analysis
from datetime import datetime, timedelta
import pandas as pd
class DeribitSettlementData:
"""ดึงข้อมูล settlement prices และ volatility"""
BASE_URL = "https://www.deribit.com/api/v2"
def get_settlement_history(
self,
currency: str = "BTC",
start_timestamp: int = None,
end_timestamp: int = None,
settlement_type: str = "settlement"
) -> pd.DataFrame:
"""
ดึงประวัติ settlement prices
settlement_type: 'settlement' (final), 'mark', หรือ 'index'
"""
# ถ้าไม่ระบุเวลา ใช้ 30 วันย้อนหลัง
if end_timestamp is None:
end_timestamp = int(datetime.utcnow().timestamp() * 1000)
if start_timestamp is None:
start_timestamp = int(
(datetime.utcnow() - timedelta(days=30)).timestamp() * 1000
)
endpoint = "/public/get_settlements"
params = {
"currency": currency,
"start_timestamp": start_timestamp,
"end_timestamp": end_timestamp,
"type": settlement_type
}
response = requests.get(
f"{self.BASE_URL}{endpoint}",
params=params,
timeout=30
)
response.raise_for_status()
data = response.json()
settlements = data.get("result", {}).get("settlements", [])
# แปลงเป็น DataFrame
df = pd.DataFrame(settlements)
if not df.empty:
# แปลง timestamp เป็น datetime
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
df["date"] = df["timestamp"].dt.date
# เลือก columns ที่สำคัญ
important_cols = [
"timestamp", "date", "instrument_name",
"settlement_price", "mark_price", "index_price"
]
df = df[[c for c in important_cols if c in df.columns]]
return df
def calculate_implied_volatility(
self,
df: pd.DataFrame,
spot_price: float,
strike: float,
time_to_expiry: float,
option_type: str = "call"
) -> pd.DataFrame:
"""
คำนวณ implied volatility จาก mark price
ใช้ Black-Scholes model
"""
from scipy.stats import norm
import numpy as np
def black_scholes_iv(row, S, K, T, option_type):
"""คำนวณ IV จาก option price"""
try:
r = 0.01 # risk-free rate (annualized)
C = row["mark_price"] if "mark_price" in row else row["settlement_price"]
if C <= 0:
return None
# Newton-Raphson iteration
sigma = 0.3 # initial guess
for _ in range(100):
d1 = (np.log(S/K) + (r + sigma**2/2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
if option_type == "call":
price = S * norm.cdf(d1) - K * np.exp(-r*T) * norm.cdf(d2)
else:
price = K * np.exp(-r*T) * norm.cdf(-d2) - S * norm.cdf(-d1)
vega = S * norm.pdf(d1) * np.sqrt(T)
if vega == 0:
break
diff = C - price
if abs(diff) < 1e-6:
break
sigma += diff / vega
if sigma <= 0 or sigma > 5: # reasonable bounds
return None
return sigma
except Exception:
return None
df["implied_volatility"] = df.apply(
lambda row: black_scholes_iv(
row, spot_price, strike, time_to_expiry, option_type
),
axis=1
)
return df
ตัวอย่างการใช้งาน
if __name__ == "__main__":
fetcher = DeribitSettlementData()
# ดึง settlement history 90 วัน
now = datetime.utcnow()
start = now - timedelta(days=90)
df = fetcher.get_settlement_history(
currency="BTC",
start_timestamp=int(start.timestamp() * 1000),
end_timestamp=int(now.timestamp() * 1000)
)
print(f"ได้รับ {len(df)} settlement records")
print(df.head())
# คำนวณ IV สำหรับ options ที่ strike 80,000
# (ต้องกำหนด spot และ time to expiry ที่เหมาะสม)
# df_iv = fetcher.calculate_implied_volatility(df, spot_price=85000, strike=80000, time_to_expiry=30/365)
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. Error 401 Unauthorized — Token หมดอายุ
อาการ: ได้รับ response {"error": {"code": -32603, "message": "invalid_token", "data": null}, ...} หรือ {"error": {"code": 13009, "message": "Unauthorized", ...}}
สาเหตุ: Deribit access token มีอายุ 1 ชั่วโมง หลังจากนั้นต้อง refresh token ใหม่ แต่หลายคนใช้ token ที่ hardcoded ไว้
# ❌ วิธีที่ผิด - hardcoded token
headers = {"Authorization": "Bearer eyJh...hardcoded"}
✓ วิธีที่ถูก - implement token refresh
class DeribitAPI:
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.access_token = None
self.token_expires_at = 0
def get_valid_token(self) -> str:
"""ตรวจสอบว่า token ยัง valid หรือไม่"""
current_time = time.time()
# refresh token ถ้าหมดอายุภายใน 5 นาที
if not self.access_token or current_time >= (self.token_expires_at - 300):
self._refresh_token()
return self.access_token
def _refresh_token(self):
"""รีเฟรช access token"""
auth_url = "https://www.deribit.com/api/v2/public/auth"
params = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": "client_credentials"
}
response = requests.post(auth_url, params=params)
data = response.json()
if "error" in data:
raise ConnectionError(f"Auth failed: {data['error']['message']}")
result = data["result"]
self.access_token = result["access_token"]
self.token_expires_at = current_time + result["expires_in"]
print(f"Token refreshed, expires in {result['expires_in']} seconds")
def make_request(self, endpoint, params=None):
"""ส่ง request พร้อม auto-refresh token"""
url = f"https://www.deribit.com/api/v2{endpoint}"
headers = {"Authorization": f"Bearer {self.get_valid_token()}"}
response = requests.get(url, params=params, headers=headers)
# ถ้าได้ 401 ลองรีเฟรช token แล้วส่งใหม่
if response.status_code == 401:
self._refresh_token()
headers["Authorization"] = f"Bearer {self.access_token}"
response = requests.get(url, params=params, headers=headers)
return response
ใช้งาน
api = DeribitAPI("YOUR_ID", "YOUR_SECRET")
ส่ง request ได้เลย ไม่ต้องกังวลเรื่อง token expiry
result = api.make_request("/public/get_instruments", {"currency": "BTC"})
2. Error 404 Not Found — ผิด Cycle Name หรือ Instrument หมดอายุแล้ว
อาการ: เรียก get_order_book หรือ get_trade_volumes แล้วได้ {"error": {"code": -32603, "message": "instrument not found"}}
สาเหตุ: Deribit ใช้ cycle name format ที่เฉพาะเจาะจง เช่น BTC-28MAR25, BTC-27JUN25 ถ้าดึง expired options ต้องระบุชื่อให้ตรง
# Deribit cycle naming convention
BTC-YYMMDD เช่น BTC-28MAR25, BTC-27JUN25, BTC-26SEP25
def get_active_cycles(currency="BTC"):
"""ดึงรายชื่อ cycle ที่ active ทั้งหมด"""
url = "https://www.deribit.com/api/v2/public/get_instruments"
params = {
"currency": currency,
"kind": "option",
"expired": False # ดึงเฉพาะที่ยังไม่หมดอายุ
}
response = requests.get(url, params=params)
instruments = response.json()["result"]
# ดึง cycle names ที่ไม่ซ้ำกัน
cycles = set()
for inst in instruments:
# format: BTC-28MAR25-80000-C (call) หรือ BTC-28MAR25-80000-P (put)
cycle_name = inst["instrument_name"].rsplit("-", 2)[0]
cycles.add(cycle_name)
return sorted(list(cycles))
def get_instruments_by_cycle(currency="BTC", cycle="BTC-28MAR25"):
"""ดึง instruments ทั้งหมดใน cycle เฉพาะ"""
url = "https://www.deribit.com/api/v2/public/get_instruments"
params = {
"currency": currency,
"kind": "option",
"expired": False
}
response = requests.get(url, params=params)
all_instruments = response.json()["result"]
# กรองเฉพาะ cycle ที่ต้องการ
filtered = [
inst for inst in all_instruments
if inst["instrument_name"].startswith(cycle)
]
return filtered
✓ ดึง order book ด้วยชื่อที่ถูกต้อง
cycles = get_active_cycles("BTC")
print(f"Active BTC cycles: {cycles}")
if cycles:
# ดึง instruments ใน cycle แรก
instruments = get_instruments_by_cycle("BTC", cycles[0])
print(f"มี {len(instruments)} instruments ใน {cycles[0]}")
# ดึง order book ของ instrument แรก
if instruments:
inst_name = instruments[0]["instrument_name"]
print(f"ดึง order book สำหรับ: {inst_name}")
url = "https://www.deribit.com/api/v2/public/get_order_book"
response = requests.get(url, params={"instrument_name": inst_name})
if "error" in response.json():
print(f"Error: {response.json()['error']['message']}")
else:
print(f"✓ Success: {response.json()['result']}")
3. Empty Array กลับมา — Settlement Time อยู่นอกช่วง Data
อาการ: เรียก get_historical_options_by_fdc ได้ response {"result": {"instruments": []}} แต่รู้ว่าวันนั้นมี trading
สาเหตุ: Deribit เก็บ historical data ได้เฉพาะวันที่มี expiry settlement (วันศุกร์สุดท้ายของเดือน) หรือ settlement time ที่ระบุไม่ตรงกับ settlement window จริง
from datetime import datetime, timedelta
import calendar
class DeribitSettlementCalendar:
"""Deribit settlement calendar - options expire วันศุกร์สุดท้ายของเดือน"""
# settlement_time ของ Deribit เป็น 08:00 UTC
SETTLEMENT_HOUR = 8
@staticmethod
def get_weekly_expiry_dates(year, month):
"""หาวันหมดอายุรายสัปดาห์ (สำหรับ BTC weekly options)"""
# หาวันศุกร์สุดท้ายของเดือน
last_day = calendar.monthrange(year, month)[1]
last_date = datetime(year, month, last_day)
# หาวันศุกร์
days_to_friday = (4 - last_date.weekday()) % 7
last_friday = last_date - timedelta(days=days_to_friday)
# สำหรับ weekly options - ทุกวันศุกร์
fridays = []
first_friday = last_friday
# หาวันศุกร์ทั้งหมดในเดือน
current = first_friday
while current.month == month:
fridays.append(current)
current -= timedelta(days=7)
return sorted(fridays)
@staticmethod
def get_monthly_expiry_dates(year):
"""หาวันหมดอายุรายเดือน (สำหรับ BTC/ETH monthly options)"""
expirations = []
for month in range(1, 13):
last_day = calendar.monthrange(year, month)[1]
last_date = datetime(year, month, last_day)
# หาวันศุกร์สุดท้าย
days_to_friday = (4 - last_date.weekday()) % 7
expiry = last_date - timedelta(days=days_to_friday)
# ตั้งเวลา 08:00 UTC
expiry = expiry.replace(hour=8, minute=0, second=0)
expirations.append(expiry)
return expirations
@staticmethod
def get_closest_settlement_date(target_date):
"""หา settlement date ที่ใกล้ที่สุดกับ target date"""
# Deribit มี 2 ประเภท settlement:
# 1. Weekly - �
แหล่งข้อมูลที่เกี่ยวข้อง
บทความที่เกี่ยวข้อง