ผมเคยเจอสถานการณ์ที่ทำให้โปรเจกต์ Options Trading Bot หยุดชะงักทั้งหมด นั่นคือ ConnectionError: HTTPSConnectionPool(host='history.deribit.com', port=443): Max retries exceeded พร้อมกับ 403 Forbidden - Historical data requires active subscription ซ้ำแล้วซ้ำเล่า หลังจากลองทำตามเอกสารอย่างเป็นทางการแล้วพบว่า Deribit เองไม่ได้เปิดให้เข้าถึง historical data โดยตรงสำหรับบัญชีฟรี

บทความนี้จะสอนวิธีดึงข้อมูล BTC Options จาก Deribit ผ่าน Tardis API, คำนวณ Implied Volatility ด้วย Newton-Raphson method, และสร้าง backtesting framework เพื่อทดสอบกลยุทธ์ Options อย่างเป็นระบบ พร้อมตัวอย่างโค้ดที่รันได้จริงภายใน 10 นาที

ทำไมต้องดึงข้อมูล BTC Options จาก Deribit

Deribit คือตลาด Futures และ Options ที่ใหญ่ที่สุดสำหรับ BTC โดยมี Volume กว่า $2.5 พันล้านต่อวัน ข้อมูลที่นี่มีความสำคัญต่อ:

Tardis API: วิธีดาวน์โหลด Deribit Historical Data ในรูปแบบ CSV

Tardis Machine เป็นบริการที่รวบรวม Historical Data จาก Deribit, Binance, OKX และ Exchange อื่นๆ โดยมี API ที่เสถียรกว่าการดึงข้อมูลโดยตรงจาก Deribit มาก

การติดตั้งและตั้งค่าเบื้องต้น

# ติดตั้ง dependencies ที่จำเป็น
pip install tardis-machine pandas numpy scipy requests

สร้างไฟล์ config สำหรับเก็บ API credentials

cat > config.py << 'EOF' import os

Tardis API Configuration

TARDIS_API_KEY = "your_tardis_api_key_here" # สมัครที่ https://tardis.dev TARDIS_BASE_URL = "https://api.tardis.ml/v1"

Deribit WebSocket Endpoint

DERIBIT_WS_URL = "wss://history.deribit.com/ws/api/v2"

Local cache settings

CACHE_DIR = "./data_cache" os.makedirs(CACHE_DIR, exist_ok=True) EOF echo "Setup complete!"

ดาวน์โหลด BTC Options Trade Data ในรูปแบบ CSV

import requests
import pandas as pd
from datetime import datetime, timedelta
import time
import os

class DeribitOptionsDataFetcher:
    """ดึงข้อมูล BTC Options จาก Tardis API"""
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.tardis.ml/v1"
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
    
    def fetch_options_trades(
        self,
        start_date: str,  # format: "2024-01-01"
        end_date: str,    # format: "2024-01-31"
        instrument_prefix: str = "BTC",
        save_csv: bool = True
    ) -> pd.DataFrame:
        """
        ดึงข้อมูล Options Trades สำหรับ BTC
        
        Args:
            start_date: วันที่เริ่มต้น (YYYY-MM-DD)
            end_date: วันที่สิ้นสุด (YYYY-MM-DD)
            instrument_prefix: "BTC" หรือ "ETH"
            save_csv: บันทึกเป็น CSV หรือไม่
        
        Returns:
            DataFrame ที่มี columns: timestamp, instrument_name, 
            direction, amount, price, option_type, strike, expiry
        """
        print(f"Fetching BTC Options data from {start_date} to {end_date}")
        
        # Convert dates to milliseconds 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)
        
        # API endpoint สำหรับ historical trades
        url = f"{self.base_url}/historical/deribit/trades"
        
        params = {
            "exchange": "deribit",
            "symbol": f"{instrument_prefix}-*",  # ดึงทุก Options
            "from": start_ts,
            "to": end_ts,
            "format": "json",
            "limit": 100000  # Max records per request
        }
        
        all_trades = []
        offset = 0
        
        while True:
            params["offset"] = offset
            response = requests.get(
                url, 
                headers=self.headers, 
                params=params,
                timeout=60
            )
            
            if response.status_code == 401:
                raise Exception("❌ 401 Unauthorized: ตรวจสอบ Tardis API Key ของคุณ")
            elif response.status_code == 429:
                print("⏳ Rate limited, รอ 60 วินาที...")
                time.sleep(60)
                continue
            
            data = response.json()
            
            if not data.get("trades"):
                break
                
            all_trades.extend(data["trades"])
            print(f"  ✓ ดึงได้ {len(all_trades)} records...")
            
            if len(data["trades"]) < params["limit"]:
                break
                
            offset += params["limit"]
            time.sleep(0.5)  # หลีกเลี่ยง rate limit
        
        # แปลงเป็น DataFrame และ parse instrument_name
        df = pd.DataFrame(all_trades)
        
        if df.empty:
            print("⚠️ ไม่พบข้อมูลในช่วงวันที่ที่ระบุ")
            return df
        
        # Parse option details จาก instrument_name
        # Format: BTC-20241227-95000-C (Call) หรือ BTC-20241227-95000-P (Put)
        df["expiry"] = df["instrument_name"].str.extract(r"BTC-(\d{8})")
        df["strike"] = df["instrument_name"].str.extract(r"-(\d+)-(C|P)$").astype(float)
        df["option_type"] = df["instrument_name"].str.extract(r"-(\d+)-(C|P)$").iloc[:, -1]
        
        # Convert timestamp
        df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
        df["date"] = df["timestamp"].dt.date
        
        if save_csv:
            filename = f"{CACHE_DIR}/btc_options_{start_date}_{end_date}.csv"
            df.to_csv(filename, index=False)
            print(f"💾 บันทึก {len(df)} records ไปยัง {filename}")
        
        return df

ตัวอย่างการใช้งาน

if __name__ == "__main__": fetcher = DeribitOptionsDataFetcher(api_key="your_tardis_api_key") # ดึงข้อมูล 7 วันล่าสุด end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") df = fetcher.fetch_options_trades( start_date=start_date, end_date=end_date ) print(f"\n📊 สรุปข้อมูล:") print(f" Total Trades: {len(df):,}") print(f" Unique Options: {df['instrument_name'].nunique()}") print(f" Date Range: {df['date'].min()} to {df['date'].max()}")

การคำนวณ Implied Volatility ด้วย Newton-Raphson Method

Implied Volatility (IV) คือค่าความผันผวนที่ "ซ่อนอยู่" ในราคา Options ตาม Black-Scholes Model การคำนวณ IV แม่นยำเป็นหัวใจของการทำ Quantitative Analysis

import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq, newton
from typing import Tuple, Optional

class ImpliedVolatilityCalculator:
    """คำนวณ Implied Volatility สำหรับ BTC Options"""
    
    @staticmethod
    def black_scholes_call(
        S: float,  # Spot price
        K: float,  # Strike price
        T: float,  # Time to expiry (years)
        r: float,  # Risk-free rate
        sigma: float  # Volatility
    ) -> float:
        """คำนวณราคา Call Option ตาม Black-Scholes"""
        if T <= 0 or sigma <= 0:
            return max(S - K, 0)
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    
    @staticmethod
    def black_scholes_put(
        S: float, K: float, T: float, r: float, sigma: float
    ) -> float:
        """คำนวณราคา Put Option ตาม Black-Scholes"""
        if T <= 0 or sigma <= 0:
            return max(K - S, 0)
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        
        return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
    
    @staticmethod
    def vega(
        S: float, K: float, T: float, r: float, sigma: float
    ) -> float:
        """คำนวณ Vega - ความไวต่อการเปลี่ยนแปลงของ IV"""
        if T <= 0:
            return 0
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
        return S * np.sqrt(T) * norm.pdf(d1)
    
    def calculate_iv_newton_raphson(
        self,
        option_price: float,
        S: float,
        K: float,
        T: float,
        r: float,
        is_call: bool = True,
        max_iterations: int = 100,
        tolerance: float = 1e-6
    ) -> Optional[float]:
        """
        คำนวณ IV ด้วย Newton-Raphson Method
        
        วิธีนี้ converge เร็วกว่า Brent's method แต่อาจไม่เสถียรถ้า initial guess ไม่ดี
        
        Args:
            option_price: ราคา Options จริง
            S: Spot price ของ BTC
            K: Strike price
            T: Time to expiry (years)
            r: Risk-free rate (ใช้ 0.05 สำหรับ BTC)
            is_call: True = Call, False = Put
        
        Returns:
            Implied Volatility หรือ None ถ้าคำนวณไม่ได้
        """
        # Initial guess: ATM option ใช้ 0.8 (80% annualized)
        sigma = 0.8
        
        # กำหนดขอบเขต IV ที่เป็นไปได้
        sigma_min, sigma_max = 0.01, 5.0
        
        for i in range(max_iterations):
            if is_call:
                price_calc = self.black_scholes_call(S, K, T, r, sigma)
            else:
                price_calc = self.black_scholes_put(S, K, T, r, sigma)
            
            vega = self.vega(S, K, T, r, sigma)
            
            # ถ้า Vega ใกล้ 0 แสดงว่าหาค่าไม่ได้
            if abs(vega) < 1e-10:
                return None
            
            # คำนวณ error
            diff = option_price - price_calc
            
            if abs(diff) < tolerance:
                return sigma
            
            # Update sigma ด้วย Newton-Raphson
            sigma = sigma + diff / vega
            
            # ตรวจสอบขอบเขต
            if sigma < sigma_min or sigma > sigma_max:
                return None
        
        return None
    
    def calculate_iv_brent(
        self,
        option_price: float,
        S: float,
        K: float,
        T: float,
        r: float,
        is_call: bool = True
    ) -> Optional[float]:
        """
        คำนวณ IV ด้วย Brent's Method (เสถียรกว่า)
        
        วิธีนี้ guarantee convergence แต่ช้ากว่า
        """
        def objective(sigma):
            if is_call:
                return self.black_scholes_call(S, K, T, r, sigma) - option_price
            else:
                return self.black_scholes_put(S, K, T, r, sigma) - option_price
        
        # หาช่วงที่ function เปลี่ยนเครื่องหมาย
        sigma_min, sigma_max = 0.01, 5.0
        
        try:
            # ตรวจสอบว่า root อยู่ในช่วงที่กำหนด
            f_min = objective(sigma_min)
            f_max = objective(sigma_max)
            
            if f_min * f_max > 0:
                # Root ไม่อยู่ในช่วง ลองขยายช่วง
                for multiplier in [2, 5, 10]:
                    sigma_max = multiplier
                    f_max = objective(sigma_max)
                    if f_min * f_max <= 0:
                        break
            
            iv = brentq(objective, sigma_min, sigma_max, xtol=1e-6)
            return iv
            
        except ValueError:
            return None

ตัวอย่างการใช้งาน

if __name__ == "__main__": calc = ImpliedVolatilityCalculator() # ตั้งค่า parameters S = 67500 # BTC Spot price K = 68000 # Strike price (slightly ITM) T = 30 / 365 # 30 days to expiry r = 0.05 # Risk-free rate # ราคา Options จริงจากตลาด (ประมาณ) market_call_price = 2800 # คำนวณ IV iv_newton = calc.calculate_iv_newton_raphson( option_price=market_call_price, S=S, K=K, T=T, r=r, is_call=True ) iv_brent = calc.calculate_iv_brent( option_price=market_call_price, S=S, K=K, T=T, r=r, is_call=True ) print(f"📊 Implied Volatility Calculation Results:") print(f" Spot Price: ${S:,.0f}") print(f" Strike Price: ${K:,.0f}") print(f" Days to Expiry: {T*365:.0f} days") print(f" Market Option Price: ${market_call_price:,.2f}") print(f" IV (Newton-Raphson): {iv_newton*100:.2f}%") print(f" IV (Brent's Method): {iv_brent*100:.2f}%") # Verify: คำนวณราคากลับจาก IV theoretical_price = calc.black_scholes_call(S, K, T, r, iv_newton) print(f" Theoretical Price: ${theoretical_price:,.2f}") print(f" Price Error: ${abs(market_call_price - theoretical_price):.2f}")

สร้าง Options Backtesting Framework ด้วย Python

หลังจากได้ข้อมูล IV มาแล้ว ต่อไปคือการสร้าง Backtesting Framework เพื่อทดสอบกลยุทธ์ Options ต่างๆ

import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Dict, Optional
from datetime import datetime, timedelta
from collections import defaultdict

@dataclass
class OptionContract:
    """โครงสร้างข้อมูล Option Contract"""
    expiry: str
    strike: float
    option_type: str  # 'C' หรือ 'P'
    iv: float
    delta: float
    premium: float
    expiry_date: datetime

@dataclass
class Trade:
    """โครงสร้างข้อมูล Trade"""
    entry_date: datetime
    exit_date: Optional[datetime]
    contract: OptionContract
    quantity: int
    entry_price: float
    exit_price: Optional[float]
    pnl: Optional[float]
    strategy_name: str

class OptionsBacktester:
    """Framework สำหรับ Backtest กลยุทธ์ Options"""
    
    def __init__(
        self, 
        initial_capital: float = 100000,
        risk_free_rate: float = 0.05
    ):
        self.initial_capital = initial_capital
        self.current_capital = initial_capital
        self.risk_free_rate = risk_free_rate
        self.trades: List[Trade] = []
        self.equity_curve: List[Dict] = []
        
        # Stats
        self.total_trades = 0
        self.winning_trades = 0
        self.losing_trades = 0
    
    def calculate_greeks(self, S: float, K: float, T: float, 
                        sigma: float, r: float, is_call: bool) -> Dict:
        """คำนวณ Greeks ทั้งหมด"""
        from scipy.stats import norm
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        
        if is_call:
            delta = norm.cdf(d1)
            theta = (-S * norm.pdf(d1) * sigma / (2 * np.sqrt(T)) 
                    - r * K * np.exp(-r * T) * norm.cdf(d2))
        else:
            delta = -norm.cdf(-d1)
            theta = (-S * norm.pdf(d1) * sigma / (2 * np.sqrt(T)) 
                    + r * K * np.exp(-r * T) * norm.cdf(-d2))
        
        gamma = norm.pdf(d1) / (S * sigma * np.sqrt(T))
        vega = S * np.sqrt(T) * norm.pdf(d1) / 100  # Per 1% change
        
        return {
            "delta": delta,
            "gamma": gamma,
            "theta": theta / 365,  # Daily theta
            "vega": vega  # Per 1% IV change
        }
    
    def execute_strategy_straddle(
        self,
        df: pd.DataFrame,
        entry_date: datetime,
        days_to_expiry: int = 30,
        risk_per_trade: float = 0.02  # 2% ของ Capital
    ) -> Optional[Trade]:
        """
        กลยุทธ์ Straddle: ซื้อ Call + Put ที่ Strike เดียวกัน
        
        กำไรเมื่อ: ราคาเคลื่อนไหวมากทั้งขึ้นและลง
        ขาดทุนเมื่อ: ราคาเคลื่อนไหวน้อย
        """
        # หา ATM strike
        current_row = df[df['timestamp'] <= entry_date].iloc[-1]
        S = current_row.get('underlying_price', 67000)  # Spot price
        
        # หา ATM strike
        K = round(S / 1000) * 1000  # Round to nearest 1000
        
        # คำนวณ T
        expiry_dt = entry_date + timedelta(days=days_to_expiry)
        T = days_to_expiry / 365
        
        # คำนวณ IV จากข้อมูล
        call_iv = current_row.get('call_iv', 0.75)
        put_iv = current_row.get('put_iv', 0.80)
        
        # คำนวณ premium
        calc = ImpliedVolatilityCalculator()
        call_premium = calc.black_scholes_call(S, K, T, self.risk_free_rate, call_iv)
        put_premium = calc.black_scholes_put(S, K, T, self.risk_free_rate, put_iv)
        
        # Position size ตาม risk
        total_premium = call_premium + put_premium
        position_value = self.current_capital * risk_per_trade
        quantity = max(1, int(position_value / total_premium))
        
        # สร้าง contract
        contract = OptionContract(
            expiry=expiry_dt.strftime("%Y%m%d"),
            strike=K,
            option_type="STRADDLE",
            iv=(call_iv + put_iv) / 2,
            delta=0,
            premium=total_premium,
            expiry_date=expiry_dt
        )
        
        # Record trade
        trade = Trade(
            entry_date=entry_date,
            exit_date=None,
            contract=contract,
            quantity=quantity,
            entry_price=total_premium,
            exit_price=None,
            pnl=None,
            strategy_name="Straddle"
        )
        
        self.trades.append(trade)
        return trade
    
    def close_trade(
        self, 
        trade: Trade, 
        exit_date: datetime,
        exit_price: float
    ):
        """ปิด position และคำนวณ P&L"""
        trade.exit_date = exit_date
        trade.exit_price = exit_price
        trade.pnl = (exit_price - trade.entry_price) * trade.quantity
        
        self.current_capital += trade.pnl
        self.total_trades += 1
        
        if trade.pnl > 0:
            self.winning_trades += 1
        else:
            self.losing_trades += 1
        
        # Record equity curve
        self.equity_curve.append({
            "date": exit_date,
            "capital": self.current_capital,
            "pnl": trade.pnl
        })
    
    def run_backtest(
        self,
        df: pd.DataFrame,
        strategy: str = "straddle",
        entry_interval_days: int = 7,
        holding_days: int = 30
    ) -> Dict:
        """รัน Backtest ทั้งหมด"""
        
        print(f"🚀 Starting Backtest: {strategy}")
        print(f"   Initial Capital: ${self.initial_capital:,.2f}")
        print(f"   Entry Interval: {entry_interval_days} days")
        print(f"   Holding Period: {holding_days} days")
        
        # Generate entry dates
        start_date = df['timestamp'].min()
        end_date = df['timestamp'].max()
        
        entry_dates = []
        current = start_date
        while current < end_date:
            entry_dates.append(current)
            current += timedelta(days=entry_interval_days)
        
        active_trades = {}
        
        for entry_date in entry_dates:
            # Execute new trades
            if strategy == "straddle":
                trade = self.execute_strategy_straddle(
                    df, entry_date, days_to_expiry=holding_days
                )
                if trade:
                    active_trades[trade] = trade.contract.expiry_date
            
            # Check for expiries
            trades_to_close = []
            for trade, expiry in active_trades.items():
                if entry_date >= expiry:
                    trades_to_close.append(trade)
            
            # Close expired trades
            for trade in trades_to_close:
                # สมมติ expiry price = spot price
                current_row = df[df['timestamp'] <= trade.contract.expiry_date].iloc[-1]
                S_exit = current_row.get('underlying_price', 67000)
                
                # Calculate exit price (intrinsic value only at expiry)
                if trade.contract.option_type == "STRADDLE":
                    call_intrinsic = max(S_exit - trade.contract.strike, 0)
                    put_intrinsic = max(trade.contract.strike - S_exit, 0)
                    exit_price = call_intrinsic + put_intrinsic
                else:
                    exit_price = 0
                
                self.close_trade(trade, trade.contract.expiry_date, exit_price)
                del active_trades[trade]
        
        # Close remaining trades at end
        final_date = df['timestamp'].max()
        for trade in list(active_trades.keys()):
            self.close_trade(trade, final_date, trade.entry_price * 0.5)  # Assume 50% loss
        
        return self.generate_report()
    
    def generate_report(self) -> Dict:
        """สร้าง Backtest Report"""
        
        if not self.equity_curve:
            return {"error": "No trades executed"}
        
        equity_df = pd.DataFrame(self.equity_curve)
        
        total_return = (self.current_capital - self.initial_capital) / self.initial_capital
        win_rate = self.winning_trades / self.total_trades if self.total_trades > 0 else 0
        
        # Calculate max drawdown
        equity_df['peak'] = equity_df['capital'].cummax()
        equity_df['drawdown'] = (equity_df['capital'] - equity_df['peak']) / equity_df['peak']
        max_drawdown = equity_df['drawdown'].min()
        
        # Sharpe Ratio
        returns = equity_df['capital'].pct_change().dropna()
        sharpe_ratio = returns.mean() / returns.std() * np.sqrt(252) if returns.std() > 0 else 0
        
        report = {
            "total_trades": self.total_trades,
            "winning_trades": self.winning_trades,
            "losing_trades": self.losing_trades,
            "win_rate": f"{win_rate:.2%}",
            "initial_capital": f"${self.initial_capital:,.2f}",
            "final_capital": f"${self.current_capital:,.2f}",
            "total_return": f"{total_return:.2%}",
            "max_drawdown": f"{max_drawdown:.2%}",
            "sharpe_ratio": f"{sharpe_ratio:.2f}",
            "avg_pnl": f"${equity_df['pnl'].mean():,.2f}",
        }
        
        print("\n" + "="*50)
        print("📊 BACKTEST RESULTS")
        print("="*50)
        for key, value in report.items():
            print(f"   {key}: {value}")
        print("="*50)
        
        return report

ตัวอย่างการใช้งาน

if __name__ == "__main__": #