ในฐานะวิศวกร quantitative trading มากว่า 8 ปี ผมเคยเจอปัญหาเดียวกันกับทุกคน คือการได้มาซึ่งข้อมูล option chain คุณภาพสูงสำหรับ backtesting ที่ reliable และ cost-effective ในบทความนี้ ผมจะแชร์ architecture ที่พิสูจน์แล้วใน production สำหรับดึงข้อมูล Deribit options historical data ผ่าน Tardis API และใช้คำนวณ implied volatility สำหรับ backtest ที่แม่นยำ

ทำไมต้อง Deribit + Tardis API

Deribit เป็น exchange ที่ได้รับความนิยมสูงสุดสำหรับ BTC/ETH options โดยมี open interest รวมกว่า $10B แต่ปัญหาคือ API ของ Deribit เองไม่ได้ออกแบบมาสำหรับ historical data retrieval โดยเฉพาะ ทำให้การ backtest ย้อนหลังทำได้ยากและไม่ consistent

Tardis API แก้ปัญหานี้โดย providing normalized historical market data จาก exchanges หลายสิบแห่ง รวมถึง Deribit โดยเฉพาะ พร้อม features ที่ quant researcher ต้องการ เช่น:

การตั้งค่า Environment และ Dependencies

pip install tardis-client pandas numpy scipy aiohttp asyncio \
    python-dotenv asyncpg motor prometheus-client \
    pyarrow fastparquet cachetools backoff \
    python-rapidjson orjson msgpack \
    httpx tenacity

สร้าง .env file

cat > .env << 'EOF' TARDIS_API_KEY=your_tardis_api_key_here TARDIS_API_SECRET=your_tardis_secret_here HOLYSHEEP_API_KEY=sk-your-holysheep-key DATABASE_URL=postgresql://user:pass@localhost:5432/options_db REDIS_URL=redis://localhost:6379 EOF

ใช้ Poetry สำหรับ production project

poetry init --name deribit-options-backtest poetry add tardis-client pandas numpy scipy asyncpg aiohttp \ pyarrow fastparquet httpx tenacity backoff orjson

Core Architecture: Async Data Fetcher

import asyncio
import aiohttp
import orjson
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Optional, AsyncIterator
from pathlib import Path
import pyarrow as pa
import pyarrow.parquet as pq
from tenacity import retry, stop_after_attempt, wait_exponential
import backoff
from collections import defaultdict
import numpy as np

@dataclass
class OptionContract:
    """Deribit option contract data structure"""
    instrument_name: str
    timestamp: datetime
    mark_price: float
    underlying_price: float
    strike: float
    expiry: datetime
    option_type: str  # 'call' or 'put'
    iv_bid: float
    iv_ask: float
    iv_mark: float
    delta: float
    gamma: float
    theta: float
    vega: float
    open_interest: float
    volume: float

@dataclass
class DeribitOptionsFetcher:
    """
    Production-grade Deribit options data fetcher using Tardis API
    Supports batch downloads, caching, and incremental updates
    """
    api_key: str
    api_secret: str
    base_url: str = "https://api.tardis.dev/v1"
    max_concurrent_requests: int = 10
    rate_limit_per_second: int = 5
    cache_dir: Path = field(default_factory=lambda: Path("./data_cache"))
    
    def __post_init__(self):
        self.cache_dir.mkdir(parents=True, exist_ok=True)
        self._semaphore = asyncio.Semaphore(self.max_concurrent_requests)
        self._rate_limiter = asyncio.Semaphore(self.rate_limit_per_second)
        self._session: Optional[aiohttp.ClientSession] = None
        self._request_times: List[float] = []
        
    async def __aenter__(self):
        connector = aiohttp.TCPConnector(
            limit=100,
            limit_per_host=20,
            ttl_dns_cache=300,
            enable_cleanup_closed=True
        )
        self._session = aiohttp.ClientSession(
            connector=connector,
            json_serialize=lambda x: orjson.dumps(x).decode(),
            timeout=aiohttp.ClientTimeout(total=60, connect=10)
        )
        return self
    
    async def __aexit__(self, *args):
        if self._session:
            await self._session.close()

    @backoff.on_exception(
        backoff.expo,
        (aiohttp.ClientError, asyncio.TimeoutError),
        max_tries=5,
        max_time=120
    )
    async def _rate_limited_request(self, url: str, params: Dict) -> Dict:
        """Rate-limited request với exponential backoff"""
        async with self._rate_limiter:
            async with self._semaphore:
                now = asyncio.get_event_loop().time()
                self._request_times = [
                    t for t in self._request_times 
                    if now - t < 1.0
                ]
                if len(self._request_times) >= self.rate_limit_per_second:
                    sleep_time = 1.0 - (now - self._request_times[0])
                    if sleep_time > 0:
                        await asyncio.sleep(sleep_time)
                
                headers = {
                    "Authorization": f"Bearer {self.api_key}",
                    "X-API-Key": self.api_secret,
                    "Content-Type": "application/json"
                }
                
                async with self._session.get(url, params=params, headers=headers) as resp:
                    if resp.status == 429:
                        retry_after = int(resp.headers.get("Retry-After", 60))
                        await asyncio.sleep(retry_after)
                        raise aiohttp.ClientError("Rate limited")
                    resp.raise_for_status()
                    return await resp.json()

    async def fetch_options_chain(
        self,
        symbol: str,
        start_date: datetime,
        end_date: datetime,
        expiry_filter: Optional[List[str]] = None
    ) -> AsyncIterator[OptionContract]:
        """
        Fetch options chain data for specified date range
        Supports BTC and ETH options on Deribit
        """
        date_ranges = self._split_date_range(start_date, end_date, days_per_chunk=7)
        
        for chunk_start, chunk_end in date_ranges:
            cache_file = self.cache_dir / f"{symbol}_{chunk_start.date()}_{chunk_end.date()}.parquet"
            
            if cache_file.exists():
                yield from self._read_cache(cache_file)
                continue
            
            params = {
                "exchange": "deribit",
                "symbol": symbol,
                "from": chunk_start.isoformat(),
                "to": chunk_end.isoformat(),
                "format": "objects",
                "has_content": True,
                "limit": 10000,
                "meta": json.dumps({
                    "types": ["trade", "quote"],
                    "channels": ["book", "trades"]
                })
            }
            
            try:
                data = await self._rate_limited_request(
                    f"{self.base_url}/historical/derivatives",
                    params
                )
                
                contracts = []
                for record in data.get("data", []):
                    if record.get("instrument_type") != "option":
                        continue
                    
                    contract = self._parse_option_record(record, symbol)
                    if contract and (not expiry_filter or contract.instrument_name in expiry_filter):
                        contracts.append(contract)
                
                if contracts:
                    await self._write_cache(cache_file, contracts)
                    yield from contracts
                    
            except Exception as e:
                print(f"Error fetching {symbol} {chunk_start.date()}: {e}")
                continue

    def _parse_option_record(self, record: Dict, symbol: str) -> Optional[OptionContract]:
        """Parse raw Tardis record to OptionContract"""
        try:
            timestamp = datetime.fromisoformat(
                record.get("timestamp", record.get("local_timestamp", "").replace("Z", "+00:00"))
            )
            
            if record.get("type") == "trade":
                price = record.get("price", 0)
                side = record.get("side", "buy")
            else:
                best_bid = record.get("best_bid_price", 0)
                best_ask = record.get("best_ask_price", 0)
                price = (best_bid + best_ask) / 2 if best_bid and best_ask else 0
            
            underlying = self._extract_underlying(record.get("instrument_name", ""))
            expiry = self._extract_expiry(record.get("instrument_name", ""))
            strike = self._extract_strike(record.get("instrument_name", ""))
            option_type = "call" if "C" in record.get("instrument_name", "") else "put"
            
            return OptionContract(
                instrument_name=record.get("instrument_name", ""),
                timestamp=timestamp,
                mark_price=price,
                underlying_price=underlying,
                strike=strike,
                expiry=expiry,
                option_type=option_type,
                iv_bid=record.get("iv_bid", 0),
                iv_ask=record.get("iv_ask", 0),
                iv_mark=record.get("iv_mark", price),
                delta=record.get("delta", 0),
                gamma=record.get("gamma", 0),
                theta=record.get("theta", 0),
                vega=record.get("vega", 0),
                open_interest=record.get("open_interest", 0),
                volume=record.get("volume", 0)
            )
        except Exception:
            return None

    @staticmethod
    def _split_date_range(start: datetime, end: datetime, days_per_chunk: int) -> List[tuple]:
        """Split date range into chunks"""
        chunks = []
        current = start
        while current < end:
            chunk_end = min(current + timedelta(days=days_per_chunk), end)
            chunks.append((current, chunk_end))
            current = chunk_end
        return chunks
    
    async def _write_cache(self, path: Path, contracts: List[OptionContract]):
        """Write contracts to Parquet cache"""
        table = pa.Table.from_pylist([
            {
                "instrument_name": c.instrument_name,
                "timestamp": c.timestamp,
                "mark_price": c.mark_price,
                "underlying_price": c.underlying_price,
                "strike": c.strike,
                "expiry": c.expiry,
                "option_type": c.option_type,
                "iv_bid": c.iv_bid,
                "iv_ask": c.iv_ask,
                "iv_mark": c.iv_mark,
                "delta": c.delta,
                "gamma": c.gamma,
                "theta": c.theta,
                "vega": c.vega,
                "open_interest": c.open_interest,
                "volume": c.volume
            } for c in contracts
        ])
        pq.write_table(table, path, compression="snappy")
    
    def _read_cache(self, path: Path) -> List[OptionContract]:
        """Read contracts from Parquet cache"""
        table = pq.read_table(path)
        return [
            OptionContract(
                instrument_name=row["instrument_name"],
                timestamp=row["timestamp"].to_pydatetime(),
                mark_price=row["mark_price"],
                underlying_price=row["underlying_price"],
                strike=row["strike"],
                expiry=row["expiry"],
                option_type=row["option_type"],
                iv_bid=row["iv_bid"],
                iv_ask=row["iv_ask"],
                iv_mark=row["iv_mark"],
                delta=row["delta"],
                gamma=row["gamma"],
                theta=row["theta"],
                vega=row["vega"],
                open_interest=row["open_interest"],
                volume=row["volume"]
            )
            for row in table.to_pylist()
        ]

Implied Volatility Calculator

import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq, minimize_scalar
from typing import Tuple, Optional
from dataclasses import dataclass
import pandas as pd

@dataclass
class ImpliedVolResult:
    """Result container for IV calculation"""
    iv: float
    method: str
    success: bool
    error_message: Optional[str] = None
    iterations: int = 0

class ImpliedVolatilityCalculator:
    """
    Production IV calculator với multiple methods:
    - Newton-Raphson (fast, requires Greeks)
    - Bisection (robust, slow)
    - Brent (recommended for production)
    """
    
    def __init__(self, tolerance: float = 1e-8, max_iterations: int = 100):
        self.tolerance = tolerance
        self.max_iterations = max_iterations
    
    def black_scholes_price(
        self, S: float, K: float, T: float, r: float, 
        sigma: float, option_type: str = "call"
    ) -> float:
        """
        Black-Scholes option pricing model
        S: Spot price
        K: Strike price
        T: Time to expiry (years)
        r: Risk-free rate
        sigma: Volatility
        """
        if T <= 0 or sigma <= 0:
            return max(0, S - K if option_type == "call" else K - S)
        
        d1 = (np.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        
        if option_type.lower() == "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)
        
        return max(0, price)
    
    def vega(self, S: float, K: float, T: float, r: float, sigma: float) -> float:
        """First derivative of option price w.r.t. volatility"""
        if T <= 0:
            return 0
        d1 = (np.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
        return S * norm.pdf(d1) * np.sqrt(T) / 100  # Per vol point
    
    def implied_vol_newton_raphson(
        self, market_price: float, S: float, K: float, 
        T: float, r: float, option_type: str = "call",
        initial_sigma: float = 0.5
    ) -> ImpliedVolResult:
        """
        Newton-Raphson method - fastest when Greeks available
        Converges quadratically near root
        """
        sigma = initial_sigma
        
        for i in range(self.max_iterations):
            price = self.black_scholes_price(S, K, T, r, sigma, option_type)
            v = self.vega(S, K, T, r, sigma)
            
            diff = market_price - price
            
            if abs(diff) < self.tolerance:
                return ImpliedVolResult(
                    iv=sigma, method="newton_raphson", 
                    success=True, iterations=i+1
                )
            
            if abs(v) < 1e-10:
                break
            
            sigma += diff / v
            
            if sigma <= 0 or sigma > 5:
                return ImpliedVolResult(
                    iv=np.nan, method="newton_raphson",
                    success=False, 
                    error_message=f"Sigma out of bounds: {sigma}",
                    iterations=i+1
                )
        
        return ImpliedVolResult(
            iv=sigma, method="newton_raphson",
            success=False, error_message="Max iterations reached",
            iterations=self.max_iterations
        )
    
    def implied_vol_bisection(
        self, market_price: float, S: float, K: float,
        T: float, r: float, option_type: str = "call"
    ) -> ImpliedVolResult:
        """
        Bisection method - most robust, guaranteed convergence
        Slower than Newton but reliable
        """
        sigma_low, sigma_high = 0.001, 5.0
        
        price_low = self.black_scholes_price(S, K, T, r, sigma_low, option_type)
        price_high = self.black_scholes_price(S, K, T, r, sigma_high, option_type)
        
        if (price_low - market_price) * (price_high - market_price) > 0:
            return ImpliedVolResult(
                iv=np.nan, method="bisection",
                success=False,
                error_message="No solution in volatility range"
            )
        
        for i in range(self.max_iterations):
            sigma_mid = (sigma_low + sigma_high) / 2
            price_mid = self.black_scholes_price(S, K, T, r, sigma_mid, option_type)
            
            if abs(price_mid - market_price) < self.tolerance:
                return ImpliedVolResult(
                    iv=sigma_mid, method="bisection",
                    success=True, iterations=i+1
                )
            
            if (price_low - market_price) * (price_mid - market_price) < 0:
                sigma_high = sigma_mid
            else:
                sigma_low = sigma_mid
        
        return ImpliedVolResult(
            iv=sigma_mid, method="bisection",
            success=True, iterations=self.max_iterations
        )
    
    def implied_vol_brent(
        self, market_price: float, S: float, K: float,
        T: float, r: float, option_type: str = "call"
    ) -> ImpliedVolResult:
        """
        Brent's method - best of both worlds
        Combines bisection reliability with secant speed
        """
        def objective(sigma):
            price = self.black_scholes_price(S, K, T, r, sigma, option_type)
            return price - market_price
        
        try:
            sigma = brentq(
                objective, 0.001, 5.0, 
                xtol=self.tolerance, 
                maxiter=self.max_iterations
            )
            return ImpliedVolResult(
                iv=sigma, method="brent",
                success=True
            )
        except ValueError as e:
            return ImpliedVolResult(
                iv=np.nan, method="brent",
                success=False,
                error_message=str(e)
            )
    
    def calculate_batch_iv(
        self, 
        options_df: pd.DataFrame,
        risk_free_rate: float = 0.05
    ) -> pd.DataFrame:
        """
        Batch calculate IV for entire options chain
        Optimized for production use with vectorization hints
        """
        df = options_df.copy()
        df["implied_vol"] = np.nan
        df["iv_method"] = ""
        df["iv_success"] = False
        
        for idx, row in df.iterrows():
            if row["mark_price"] <= 0 or row["underlying_price"] <= 0:
                continue
            
            T = (row["expiry"] - row["timestamp"]).total_seconds() / (365.25 * 86400)
            
            if T <= 0:
                continue
            
            market_price = row["mark_price"]
            S = row["underlying_price"]
            K = row["strike"]
            option_type = row["option_type"]
            
            result = self.implied_vol_brent(
                market_price, S, K, T, risk_free_rate, option_type
            )
            
            df.at[idx, "implied_vol"] = result.iv if result.success else np.nan
            df.at[idx, "iv_method"] = result.method
            df.at[idx, "iv_success"] = result.success
        
        return df[df["iv_success"]]

def calculate_volatility_surface(options_df: pd.DataFrame) -> pd.DataFrame:
    """
    Calculate full volatility surface (IV vs Strike vs Expiry)
    Essential for model validation and trading signal generation
    """
    df = options_df.copy()
    
    df["moneyness"] = df["underlying_price"] / df["strike"]
    df["time_to_expiry"] = (df["expiry"] - df["timestamp"]).dt.total_seconds() / (365.25 * 86400)
    
    surface = df.groupby(["expiry", "strike"]).agg({
        "implied_vol": ["mean", "std", "count"],
        "mark_price": "mean",
        "open_interest": "sum"
    }).reset_index()
    
    surface.columns = ["expiry", "strike", "iv_mean", "iv_std", "n_contracts", "avg_price", "total_oi"]
    
    return surface[surface["n_contracts"] >= 5]

Backtesting Engine with HolySheep AI

import httpx
import orjson
import asyncio
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
import pandas as pd
import numpy as np

@dataclass
class BacktestResult:
    """Backtest results container"""
    strategy_name: str
    start_date: datetime
    end_date: datetime
    total_trades: int
    winning_trades: int
    losing_trades: int
    win_rate: float
    total_pnl: float
    max_drawdown: float
    sharpe_ratio: float
    sortino_ratio: float
    avg_trade_pnl: float
    max_consecutive_losses: int
    profit_factor: float

class OptionsBacktester:
    """
    Production options backtesting engine
    Integrates with HolySheep AI for signal generation
    """
    
    def __init__(
        self, 
        holysheep_api_key: str,
        initial_capital: float = 100_000,
        max_position_size: float = 0.1,
        commission_rate: float = 0.0004
    ):
        self.initial_capital = initial_capital
        self.max_position_size = max_position_size
        self.commission_rate = commission_rate
        
        self.base_url = "https://api.holysheep.ai/v1"
        self.headers = {
            "Authorization": f"Bearer {holysheep_api_key}",
            "Content-Type": "application/json"
        }
    
    async def generate_trading_signal(
        self, 
        iv_data: Dict[str, Any],
        market_context: Dict[str, Any]
    ) -> Optional[Dict]:
        """
        Use HolySheep AI to analyze IV data and generate trading signals
        Supports GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2
        """
        prompt = f"""
        Analyze the following BTC options implied volatility data and generate a trading signal.
        
        Current Market Context:
        - BTC Price: ${market_context.get('btc_price', 0):,.2f}
        - 30-day IV: {market_context.get('iv_30d', 0)*100:.2f}%
        - 90-day IV: {market_context.get('iv_90d', 0)*100:.2f}%
        - IV Rank: {market_context.get('iv_rank', 0)*100:.2f}%
        - Risk-free rate: {market_context.get('risk_free_rate', 0.05)*100:.2f}%
        
        Options Chain Summary:
        - Near-term ATM IV: {iv_data.get('near_atm_iv', 0)*100:.2f}%
        - Near-term OTM Call IV: {iv_data.get('near_otm_call_iv', 0)*100:.2f}%
        - Near-term OTM Put IV: {iv_data.get('near_otm_put_iv', 0)*100:.2f}%
        - Put-Call Ratio: {iv_data.get('put_call_ratio', 0):.2f}
        
        Volatility Skew:
        - 25-delta put IV vs ATM: {(iv_data.get('iv_25d_put', 0) - iv_data.get('near_atm_iv', 0))*100:.2f} vol points
        - 25-delta call IV vs ATM: {(iv_data.get('iv_25d_call', 0) - iv_data.get('near_atm_iv', 0))*100:.2f} vol points
        
        Return a JSON response with:
        {{
            "signal": "bullish" | "bearish" | "neutral",
            "confidence": 0.0-1.0,
            "recommended_strategy": "straddle" | "strangle" | "iron_condor" | "vertical_spread" | "butterfly",
            "strike_selection": {{"type": "ATM" | "OTM" | "ITM", "delta_target": 0.0-1.0}},
            "rationale": "brief explanation",
            "risk_level": "low" | "medium" | "high",
            "expected_move_pct": -50 to 50
        }}
        """
        
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{self.base_url}/chat/completions",
                headers=self.headers,
                json={
                    "model": "gpt-4.1",
                    "messages": [
                        {"role": "system", "content": "You are an expert options trader. Return ONLY valid JSON."},
                        {"role": "user", "content": prompt}
                    ],
                    "temperature": 0.3,
                    "response_format": {"type": "json_object"}
                }
            )
            
            if response.status_code == 200:
                result = response.json()
                return orjson.loads(result["choices"][0]["message"]["content"])
            else:
                print(f"API Error: {response.status_code} - {response.text}")
                return None
    
    async def run_backtest(
        self,
        options_fetcher: 'DeribitOptionsFetcher',
        start_date: datetime,
        end_date: datetime,
        symbols: List[str] = ["BTC", "ETH"]
    ) -> BacktestResult:
        """
        Run complete backtest over historical data
        """
        capital = self.initial_capital
        capital_history = [capital]
        trades = []
        consecutive_losses = 0
        max_consecutive_losses = 0
        total_wins = 0
        total_losses = 0
        
        all_iv_data = []
        
        async for option in options_fetcher.fetch_options_chain(
            symbol="BTC", start_date=start_date, end_date=end_date
        ):
            all_iv_data.append({
                "timestamp": option.timestamp,
                "strike": option.strike,
                "iv": option.iv_mark,
                "price": option.mark_price,
                "option_type": option.option_type,
                "underlying": option.underlying_price
            })
        
        if not all_iv_data:
            return BacktestResult(
                strategy_name="IV Mean Reversion",
                start_date=start_date,
                end_date=end_date,
                total_trades=0,
                winning_trades=0,
                losing_trades=0,
                win_rate=0.0,
                total_pnl=0.0,
                max_drawdown=0.0,
                sharpe_ratio=0.0,
                sortino_ratio=0.0,
                avg_trade_pnl=0.0,
                max_consecutive_losses=0,
                profit_factor=0.0
            )
        
        df = pd.DataFrame(all_iv_data)
        df["expiry_group"] = df.groupby("timestamp")["strike"].transform("count")
        
        iv_stats = df.groupby("timestamp").agg({
            "iv": ["mean", "std", "min", "max"]
        }).reset_index()
        iv_stats.columns = ["timestamp", "iv_mean", "iv_std", "iv_min", "iv_max"]
        iv_stats["iv_range"] = iv_stats["iv_max"] - iv_stats["iv_min"]
        
        for _, row in iv_stats.iterrows():
            if iv_stats["iv"].std() < 0.05:
                continue
            
            iv_data = {
                "near_atm_iv": row["iv_mean"],
                "near_otm_call_iv": row["iv_max"],
                "near_otm_put_iv": row["iv_min"],
                "put_call_ratio": 1.0
            }
            
            market_context = {
                "btc_price": row.get("underlying", 50000),
                "iv_30d": row["iv_mean"],
                "iv_90d": row["iv_mean"] * 1.1,
                "iv_rank": 0.5,
                "risk_free_rate": 0.05
            }
            
            signal = await self.generate_trading_signal(iv_data, market_context)
            
            if signal and signal.get("confidence", 0) > 0.7:
                trade_pnl = self._simulate_trade(signal, row, capital)
                
                capital += trade_pnl
                capital_history.append(capital)
                trades.append(trade_pnl)
                
                if trade_pnl > 0:
                    total_wins += 1
                    consecutive_losses = 0
                else:
                    total_losses += 1
                    consecutive_losses += 1
                    max_consecutive_losses = max(max_consecutive_losses, consecutive_losses)
        
        returns = np.diff(capital_history) / capital_history[:-1]
        sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252) if np.std(returns) > 0 else 0
        
        downside_returns = returns[returns < 0]
        sortino = np.mean(returns) / np.std(downside_returns) * np.sqrt(252) if len(downside_returns) > 0 and np.std(downside_returns) > 0 else 0
        
        cumulative = np.maximum.accumulate(capital_history)
        drawdowns = (cumulative - capital_history) / cumulative
        max_dd = np.max(drawdowns) if len(drawdowns) > 0 else 0
        
        winning_pnl = sum(t for t in trades if t > 0)
        losing_pnl = abs(sum(t for t in trades if t < 0))
        profit_factor = winning_pnl / losing_pnl if losing_pnl > 0 else float('inf')
        
        return BacktestResult(
            strategy_name="IV Mean Reversion with AI Signals",
            start_date=start_date,
            end_date=end_date,
            total_trades=len(trades),
            winning_trades=total_wins,