Team Ai
Apppublic

mathidot111/First_agent_template

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
option_backtest.py234 linesDownload Raw Back to backtest
1from __future__ import annotations2 3from dataclasses import asdict, dataclass4from datetime import timedelta5from pathlib import Path6 7import pandas as pd8 9from .vol_backtest import max_drawdown10 11 12REQUIRED_QUOTE_COLUMNS = {13    "date",14    "underlying_symbol",15    "underlying_price",16    "contract_symbol",17    "option_type",18    "expiration",19    "strike",20    "bid",21    "ask",22}23 24 25@dataclass26class OptionBacktestTrade:27    entry_date: str28    exit_date: str29    contract_symbol: str30    option_type: str31    strike: float32    expiration: str33    quantity: int34    entry_price: float35    exit_price: float36    fees: float37    pnl: float38 39    def to_dict(self) -> dict:40        return asdict(self)41 42 43def validate_quote_frame(quotes: pd.DataFrame) -> None:44    missing = REQUIRED_QUOTE_COLUMNS - set(quotes.columns)45    if missing:46        raise ValueError(f"Historical option quotes missing required columns: {sorted(missing)}")47 48 49def prepare_quotes(quotes: pd.DataFrame) -> pd.DataFrame:50    validate_quote_frame(quotes)51    frame = quotes.copy()52    frame["date"] = pd.to_datetime(frame["date"]).dt.normalize()53    frame["expiration"] = pd.to_datetime(frame["expiration"]).dt.normalize()54    frame["option_type"] = frame["option_type"].str.lower()55    quoted_mid = (frame["bid"] + frame["ask"]) / 256    if "mid" not in frame.columns:57        frame["mid"] = quoted_mid58    else:59        frame["mid"] = frame["mid"].where(frame["mid"].notna(), quoted_mid)60    frame["dte"] = (frame["expiration"] - frame["date"]).dt.days61    frame = frame[(frame["bid"] >= 0) & (frame["ask"] > 0) & (frame["dte"] >= 0)]62    return frame.sort_values(["date", "expiration", "strike", "option_type"]).reset_index(drop=True)63 64 65def load_option_quotes_csv(path: str | Path) -> pd.DataFrame:66    return prepare_quotes(pd.read_csv(path))67 68 69def available_exit_date(70    quotes: pd.DataFrame,71    entry_date: pd.Timestamp,72    target_exit_date: pd.Timestamp,73    contract_symbol: str,74) -> pd.Timestamp | None:75    contract_quotes = quotes[76        (quotes["contract_symbol"] == contract_symbol)77        & (quotes["date"] >= target_exit_date)78    ]79    if contract_quotes.empty:80        contract_quotes = quotes[quotes["contract_symbol"] == contract_symbol]81        contract_quotes = contract_quotes[82            (contract_quotes["date"] > entry_date)83            & (contract_quotes["date"] < target_exit_date)84        ]85        if contract_quotes.empty:86            return None87        return contract_quotes["date"].max()88    if contract_quotes.empty:89        return None90    return contract_quotes["date"].min()91 92 93def quote_price(row: pd.Series, side: str, price_field: str) -> float:94    if price_field == "mid":95        return float(row["mid"])96    if price_field != "trade":97        raise ValueError("price_field must be 'trade' or 'mid'.")98    if side == "buy":99        return float(row["ask"])100    return float(row["bid"])101 102 103def select_expiration_slice(day_quotes: pd.DataFrame, target_dte: int) -> pd.DataFrame:104    candidates = day_quotes[day_quotes["dte"] > 0]105    if candidates.empty:106        return candidates107    expiration = candidates.assign(dte_error=(candidates["dte"] - target_dte).abs()).sort_values("dte_error").iloc[0]["expiration"]108    return candidates[candidates["expiration"] == expiration]109 110 111def select_atm_contract(expiration_slice: pd.DataFrame, option_type: str) -> pd.Series | None:112    contracts = expiration_slice[expiration_slice["option_type"] == option_type]113    if contracts.empty:114        return None115    spot = float(expiration_slice["underlying_price"].iloc[0])116    return contracts.assign(strike_error=(contracts["strike"] - spot).abs()).sort_values("strike_error").iloc[0]117 118 119def backtest_long_straddle_from_quotes(120    quotes: pd.DataFrame,121    symbol: str,122    target_dte: int = 30,123    holding_days: int = 5,124    entry_every_days: int = 5,125    contract_multiplier: int = 100,126    fee_per_contract: float = 0.65,127    price_field: str = "trade",128) -> dict:129    frame = prepare_quotes(quotes)130    frame = frame[frame["underlying_symbol"].str.upper() == symbol.upper()]131    if frame.empty:132        raise ValueError(f"No historical option quotes found for {symbol}.")133 134    trades: list[OptionBacktestTrade] = []135    trade_groups = []136    equity = [0.0]137    dates = sorted(frame["date"].unique())138    next_entry_date = dates[0]139 140    for entry_date in dates:141        entry_date = pd.Timestamp(entry_date)142        if entry_date < next_entry_date:143            continue144 145        day_quotes = frame[frame["date"] == entry_date]146        expiration_slice = select_expiration_slice(day_quotes, target_dte)147        if expiration_slice.empty:148            continue149 150        call = select_atm_contract(expiration_slice, "call")151        put = select_atm_contract(expiration_slice, "put")152        if call is None or put is None:153            continue154 155        target_exit_date = entry_date + timedelta(days=holding_days)156        pending_group_trades = []157        group_pnl = 0.0158        for leg in [call, put]:159            exit_date = available_exit_date(frame, entry_date, target_exit_date, str(leg["contract_symbol"]))160            if exit_date is None:161                continue162            exit_quote = frame[163                (frame["date"] == exit_date)164                & (frame["contract_symbol"] == leg["contract_symbol"])165            ].iloc[0]166 167            entry_price = quote_price(leg, "buy", price_field)168            exit_price = quote_price(exit_quote, "sell", price_field)169            fees = fee_per_contract * 2170            pnl = (exit_price - entry_price) * contract_multiplier - fees171            trade = OptionBacktestTrade(172                entry_date=str(entry_date.date()),173                exit_date=str(pd.Timestamp(exit_date).date()),174                contract_symbol=str(leg["contract_symbol"]),175                option_type=str(leg["option_type"]),176                strike=float(leg["strike"]),177                expiration=str(pd.Timestamp(leg["expiration"]).date()),178                quantity=1,179                entry_price=round(entry_price, 4),180                exit_price=round(exit_price, 4),181                fees=round(fees, 2),182                pnl=round(pnl, 2),183            )184            pending_group_trades.append(trade)185            group_pnl += pnl186 187        if len(pending_group_trades) == 2:188            trades.extend(pending_group_trades)189            equity.append(equity[-1] + group_pnl)190            trade_groups.append(191                {192                    "entry_date": str(entry_date.date()),193                    "exit_date": pending_group_trades[0].exit_date,194                    "strategy": "long_straddle",195                    "pnl": round(group_pnl, 2),196                    "legs": [trade.to_dict() for trade in pending_group_trades],197                }198            )199            next_entry_date = entry_date + timedelta(days=entry_every_days)200 201    equity_series = pd.Series(equity)202    group_pnls = [group["pnl"] for group in trade_groups]203    wins = [pnl for pnl in group_pnls if pnl > 0]204    losses = [pnl for pnl in group_pnls if pnl <= 0]205 206    return {207        "strategy": "long_straddle",208        "symbol": symbol.upper(),209        "target_dte": target_dte,210        "holding_days": holding_days,211        "entry_every_days": entry_every_days,212        "contract_multiplier": contract_multiplier,213        "fee_per_contract": fee_per_contract,214        "price_field": price_field,215        "trade_count": len(trade_groups),216        "leg_trade_count": len(trades),217        "total_pnl": round(float(equity_series.iloc[-1]), 2) if not equity_series.empty else 0.0,218        "max_drawdown": round(max_drawdown(equity_series + 100000), 6),219        "win_rate": len(wins) / len(group_pnls) if group_pnls else 0.0,220        "avg_win": round(sum(wins) / len(wins), 2) if wins else 0.0,221        "avg_loss": round(sum(losses) / len(losses), 2) if losses else 0.0,222        "trades": trade_groups[:200],223        "data_requirements": [224            "Historical option quotes with date, expiration, strike, bid, ask, and underlying_price.",225            "For production-grade backtests, include deltas, IV, volume, open interest, and corporate action adjusted symbols.",226        ],227        "limitations": [228            "No early assignment model yet.",229            "No margin model yet.",230            "No intraday fills; entry and exit use the daily quote row.",231            "Results are only as good as the historical option quote data supplied.",232        ],233    }234