Overall Statistics
Total Trades
630
Average Win
0%
Average Loss
0.60%
Compounding Annual Return
27.786%
Drawdown
20.100%
Expectancy
-1
Net Profit
82.143%
Sharpe Ratio
1.213
Probabilistic Sharpe Ratio
58.806%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0.201
Beta
-0.012
Annual Standard Deviation
0.165
Annual Variance
0.027
Information Ratio
0.604
Tracking Error
0.228
Treynor Ratio
-16.812
Total Fees
$1807.00
Estimated Strategy Capacity
$620000.00
Lowest Capacity Asset
SPXW Y98J76OI23E6|SPX 31
Portfolio Turnover
0.46%
from __future__ import annotations

import option
import spread

from AlgorithmImports import *


class FocusedApricotScorpion(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2021, 1, 1)
        self.SetCash(300000)
        self.SetSecurityInitializer(self.SecurityInitializer)
        
        self.spx = self.AddIndex("SPX").Symbol
        self.spxw = self.AddIndexOption(self.spx, "SPXW").Symbol

        self.Schedule.On(
            self.DateRules.EveryDay(self.spx),
            self.TimeRules.AfterMarketOpen(self.spx, 30),
            self._after_open_of_market,
        )

    def SecurityInitializer(self, security):
        security.SetMarketPrice(self.GetLastKnownPrice(security))

        if security.Type in [SecurityType.Equity, SecurityType.Index]:
            periods = 5
            security.VolatilityModel = StandardDeviationOfReturnsVolatilityModel(periods)
            trade_bars = self.History[TradeBar](security.Symbol, periods, Resolution.Daily)
            for trade_bar in trade_bars:
                security.VolatilityModel.Update(security, trade_bar)
                
        # if security.Type is SecurityType.Option:
        #     security.PriceModel = OptionPriceModels.BjerksundStensland()

    def OnData(self, data: Slice):
        if not self.Portfolio.Invested:
            return

    def _after_open_of_market(self):
        option_symbols = self.OptionChainProvider.GetOptionContractList(self.spxw, self.Time)
        option_chains = option.sort_listed_options(option_symbols)
        today = self.Time.date()
        
        # Check if today is an option expiration day
        if today not in option_chains:
            return

        # Get the straddle price
        chain = option_chains[today]
        spot_price = self.Securities[self.spx].Close
        volatility = self.Securities[self.spx].VolatilityModel.Volatility
        atm_options = option.get_atm_options(spot_price, chain)
        leg_1 = self.AddOptionContract(atm_options[0].symbol)
        leg_2 = self.AddOptionContract(atm_options[1].symbol)
        straddle = spread.get_straddle_price(spot_price, volatility, leg_1, leg_2)

        # Filter trades based on factors
        cond_0 = straddle.percent_price > 1
        cond_1 = straddle.percent_price < 1

        cond_2 = straddle.relative_percent_price > 4
        cond_3 = straddle.relative_percent_price < 5
        # if cond_1 and cond_2:
        #     return
        if cond_3:
            return

        # Send straddle order
        trade_risk = 10_000
        try:
            quantity = round(trade_risk / straddle.straddle_premium)
            legs = []
            legs.append(Leg.Create(leg_1.Symbol, 1))
            legs.append(Leg.Create(leg_2.Symbol, 1))
            self.ComboMarketOrder(legs, quantity, tag=str(straddle.relative_percent_price))
        except ZeroDivisionError:
            print("Missing straddle price.")



from __future__ import annotations
from collections import defaultdict
import datetime as dt

from AlgorithmImports import *


def sort_listed_options(symbols: list) -> dict:
    """Group all active options for symbol by expiration date."""
    chains = defaultdict(list)
    for s in symbols:
        option = OptionSymbol(s).compute()
        chains[option.expiration].append(option)
    return chains

def get_atm_options(spot_price: float, options: list) -> list:
    sorted_options = sorted(options, key=lambda x: x.distance_from_atm(spot_price))
    return sorted_options[:2]


class OptionSymbol:
    def __init__(self, symbol):
        self.symbol = symbol

    def compute(self) -> self:
        self.expiration = self._expiration()
        self.strike = self._strike()
        self.put_call = self._put_call()
        return self

    def distance_from_atm(self, price) -> float:
        return abs(self.strike - price)

    def _expiration(self) -> dt.date:
        return dt.datetime.strptime(self.symbol.Value[6:12], "%y%m%d").date()
    
    def _strike(self) -> float:
        return float(self.symbol.Value[-8:]) / 1000

    def _put_call(self) -> str:
        return self.symbol.Value[12:13]
from __future__ import annotations
from dataclasses import dataclass

from AlgorithmImports import *


def get_straddle_price(spot_price: float, volatility: float, leg_1, leg_2) -> StraddlePrice:
    straddle_price = _mid_price(leg_1) + _mid_price(leg_2)
    return StraddlePrice(straddle_price, spot_price, volatility)

def _mid_price(contract) -> float:
    return (contract.AskPrice + contract.BidPrice) / 2


@dataclass
class StraddlePrice:
    """Keep record of straddle price and percent price."""
    straddle_price: float
    stock_price: float
    volatility: float

    @property
    def straddle_premium(self) -> float:
        return self.straddle_price * 100

    @property
    def percent_price(self) -> float:
        """Straddle price as percentage of stock price."""
        return round(self._percent_price() * 100, 3)

    @property
    def relative_percent_price(self) -> float:
        """Straddle percent price divided by historical volatility."""
        return round(self._percent_price() / self.volatility * 100, 3)

    def _percent_price(self) -> float:
        return self.straddle_price / self.stock_price