ผมเพิ่งเจอปัญหาหนักใจตอนพัฒนาระบบวิเคราะห์ 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 คือความต้องการ:

ปัญหาคือ Deribit API เองมีความซับซ้อนกว่าที่คิด โดยเฉพาะเรื่อง cycle naming (การตั้งชื่อ expiry) ที่ต้องทำความเข้าใจก่อนถึงจะดึงข้อมูลได้

วิธีดึง Options Chain History ผ่าน Deribit API

ก่อนจะเข้าสู่โค้ด ต้องเข้าใจโครงสร้าง API ของ Deribit ก่อน:

ขั้นตอนที่ 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 - �