Spaces:
Runtime error
Runtime error
| from __future__ import annotations | |
| from market_data.schemas import OptionChain, OptionContract | |
| from .payoff import estimate_breakevens | |
| from .schemas import OptionLeg, OptionStrategy | |
| def usable_contracts(contracts: list[OptionContract]) -> list[OptionContract]: | |
| return [ | |
| contract | |
| for contract in contracts | |
| if contract.mid is not None | |
| and contract.mid > 0 | |
| and not {"missing_or_zero_bid_ask", "zero_open_interest"}.intersection(contract.liquidity_warnings) | |
| ] | |
| def nearest_contract(contracts: list[OptionContract], target_strike: float) -> OptionContract | None: | |
| valid = usable_contracts(contracts) | |
| if not valid: | |
| return None | |
| return min(valid, key=lambda contract: abs(contract.strike - target_strike)) | |
| def contract_to_leg(contract: OptionContract, action: str, quantity: int = 1) -> OptionLeg: | |
| return OptionLeg( | |
| action=action, | |
| option_type=contract.option_type, | |
| strike=contract.strike, | |
| expiration=contract.expiration, | |
| quantity=quantity, | |
| premium=contract.mid or contract.last_price or 0.0, | |
| implied_volatility=contract.implied_volatility, | |
| liquidity_warnings=contract.liquidity_warnings, | |
| ) | |
| def make_strategy( | |
| name: str, | |
| volatility_view: str, | |
| directional_view: str, | |
| legs: list[OptionLeg], | |
| rationale: str, | |
| risks: list[str], | |
| score: float, | |
| ) -> OptionStrategy: | |
| net_cash_flow = sum(leg.cash_flow() for leg in legs) | |
| net_debit_or_credit = -net_cash_flow | |
| breakevens = estimate_breakevens(legs) | |
| max_profit: float | str | None = None | |
| max_loss: float | str | None = None | |
| if name in {"long_straddle", "long_strangle"}: | |
| max_loss = round(max(net_debit_or_credit, 0.0), 2) | |
| max_profit = "unlimited" | |
| elif name == "short_straddle": | |
| max_profit = round(abs(min(net_debit_or_credit, 0.0)), 2) | |
| max_loss = "unlimited" | |
| elif name == "iron_condor": | |
| call_strikes = sorted(leg.strike for leg in legs if leg.option_type == "call") | |
| put_strikes = sorted(leg.strike for leg in legs if leg.option_type == "put") | |
| width = max(call_strikes[-1] - call_strikes[0], put_strikes[-1] - put_strikes[0]) | |
| credit = abs(min(net_debit_or_credit, 0.0)) | |
| max_profit = round(credit, 2) | |
| max_loss = round(width * 100 - credit, 2) | |
| elif name == "calendar_spread": | |
| max_loss = round(max(net_debit_or_credit, 0.0), 2) | |
| max_profit = "path_dependent" | |
| return OptionStrategy( | |
| name=name, | |
| volatility_view=volatility_view, | |
| directional_view=directional_view, | |
| legs=legs, | |
| rationale=rationale, | |
| risks=risks, | |
| max_profit=max_profit, | |
| max_loss=max_loss, | |
| breakevens=breakevens, | |
| net_debit_or_credit=round(net_debit_or_credit, 2), | |
| score=score, | |
| ) | |
| def generate_volatility_strategies( | |
| near_chain: OptionChain, | |
| volatility_view: str = "neutral", | |
| directional_view: str = "neutral", | |
| far_chain: OptionChain | None = None, | |
| ) -> list[OptionStrategy]: | |
| if near_chain.underlying_price is None: | |
| return [] | |
| spot = near_chain.underlying_price | |
| atm_call = nearest_contract(near_chain.calls, spot) | |
| atm_put = nearest_contract(near_chain.puts, spot) | |
| otm_call = nearest_contract(near_chain.calls, spot * 1.05) | |
| otm_put = nearest_contract(near_chain.puts, spot * 0.95) | |
| strategies: list[OptionStrategy] = [] | |
| if atm_call and atm_put: | |
| if volatility_view in {"long_vol", "neutral", "vol_expansion"}: | |
| strategies.append( | |
| make_strategy( | |
| name="long_straddle", | |
| volatility_view="long_vol", | |
| directional_view="neutral", | |
| legs=[contract_to_leg(atm_call, "buy"), contract_to_leg(atm_put, "buy")], | |
| rationale="Benefits from a large realized move or IV expansion; risk is premium paid.", | |
| risks=["theta_decay", "iv_crush", "requires_large_move"], | |
| score=0.75, | |
| ) | |
| ) | |
| if volatility_view in {"short_vol", "neutral", "vol_compression"}: | |
| strategies.append( | |
| make_strategy( | |
| name="short_straddle", | |
| volatility_view="short_vol", | |
| directional_view="neutral", | |
| legs=[contract_to_leg(atm_call, "sell"), contract_to_leg(atm_put, "sell")], | |
| rationale="Benefits from realized volatility staying below implied volatility.", | |
| risks=["unlimited_tail_risk", "gap_risk", "margin_requirement"], | |
| score=0.45, | |
| ) | |
| ) | |
| if otm_call and otm_put and volatility_view in {"long_vol", "neutral", "vol_expansion"}: | |
| strategies.append( | |
| make_strategy( | |
| name="long_strangle", | |
| volatility_view="long_vol", | |
| directional_view="neutral", | |
| legs=[contract_to_leg(otm_call, "buy"), contract_to_leg(otm_put, "buy")], | |
| rationale="Lower-cost long volatility expression than a straddle, but needs a larger move.", | |
| risks=["theta_decay", "wide_breakevens", "iv_crush"], | |
| score=0.65, | |
| ) | |
| ) | |
| if far_chain and atm_call and volatility_view in {"long_vol", "neutral", "term_structure"}: | |
| far_call = nearest_contract(far_chain.calls, atm_call.strike) | |
| if far_call: | |
| strategies.append( | |
| make_strategy( | |
| name="calendar_spread", | |
| volatility_view="term_structure", | |
| directional_view="neutral", | |
| legs=[contract_to_leg(atm_call, "sell"), contract_to_leg(far_call, "buy")], | |
| rationale="Expresses a term-structure view and benefits if longer-dated IV holds up.", | |
| risks=["path_dependency", "front_expiry_gamma", "term_structure_shift"], | |
| score=0.60, | |
| ) | |
| ) | |
| if otm_call and otm_put and volatility_view in {"short_vol", "neutral", "vol_compression"}: | |
| long_call = nearest_contract(near_chain.calls, otm_call.strike * 1.03) | |
| long_put = nearest_contract(near_chain.puts, otm_put.strike * 0.97) | |
| if long_call and long_put: | |
| strategies.append( | |
| make_strategy( | |
| name="iron_condor", | |
| volatility_view="short_vol", | |
| directional_view="neutral", | |
| legs=[ | |
| contract_to_leg(otm_put, "sell"), | |
| contract_to_leg(long_put, "buy"), | |
| contract_to_leg(otm_call, "sell"), | |
| contract_to_leg(long_call, "buy"), | |
| ], | |
| rationale="Defined-risk short volatility strategy for range-bound markets.", | |
| risks=["short_gamma", "tail_loss_to_width", "assignment_risk"], | |
| score=0.70, | |
| ) | |
| ) | |
| return sorted(strategies, key=lambda strategy: strategy.score, reverse=True) | |