"""
Options Trading Strategies Module
Implements common options strategies with profit/loss calculations,
risk analysis, and breakeven points.
Supported Strategies:
- Single-leg: Covered Call, Protective Put, Cash-Secured Put
- Vertical Spreads: Bull Call/Put, Bear Call/Put
- Horizontal: Calendar Spread
- Advanced: Iron Condor, Butterfly, Straddle, Strangle
"""
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from enum import Enum
import numpy as np
from datetime import date, datetime
from ..models.ticker_data import OptionContract
from ..utils.logger import setup_logger
from ..analysis.greeks_calculator import calculate_advanced_greeks
logger = setup_logger(__name__)
[docs]
class OptionType(Enum):
"""Option type"""
CALL = "call"
PUT = "put"
[docs]
class PositionType(Enum):
"""Position type (long or short)"""
LONG = "long"
SHORT = "short"
[docs]
class StrategyType(Enum):
"""Options strategy types"""
# Single-leg strategies
COVERED_CALL = "covered_call"
PROTECTIVE_PUT = "protective_put"
CASH_SECURED_PUT = "cash_secured_put"
LONG_CALL = "long_call"
LONG_PUT = "long_put"
# Vertical spreads
BULL_CALL_SPREAD = "bull_call_spread"
BULL_PUT_SPREAD = "bull_put_spread"
BEAR_CALL_SPREAD = "bear_call_spread"
BEAR_PUT_SPREAD = "bear_put_spread"
# Horizontal spreads
CALENDAR_SPREAD = "calendar_spread"
# Advanced strategies
IRON_CONDOR = "iron_condor"
BUTTERFLY = "butterfly"
STRADDLE = "straddle"
STRANGLE = "strangle"
[docs]
@dataclass
class OptionLeg:
"""Single option leg in a strategy"""
option_type: OptionType
position_type: PositionType # long or short
strike: float
premium: float # Price paid/received
quantity: int = 1
expiration: Optional[date] = None
# For advanced Greeks calculation
implied_volatility: Optional[float] = None # As decimal (e.g., 0.35 for 35%)
risk_free_rate: float = 0.05 # Default 5%
[docs]
def pnl_at_price(self, stock_price: float) -> float:
"""Calculate P&L for this leg at a given stock price"""
if self.option_type == OptionType.CALL:
intrinsic_value = max(0, stock_price - self.strike)
else: # PUT
intrinsic_value = max(0, self.strike - stock_price)
if self.position_type == PositionType.LONG:
# Long: paid premium, receive intrinsic value
pnl = (intrinsic_value - self.premium) * 100 * self.quantity
else: # SHORT
# Short: received premium, pay intrinsic value
pnl = (self.premium - intrinsic_value) * 100 * self.quantity
return pnl
[docs]
def calculate_advanced_greeks(self, stock_price: float) -> Dict[str, float]:
"""
Calculate advanced Greeks using existing calculator
Requires implied_volatility and expiration to be set.
Returns dictionary with delta, gamma, vega, theta, vanna, charm, vomma.
"""
if not self.implied_volatility or not self.expiration:
logger.debug("Cannot calculate advanced Greeks without IV and expiration")
return {}
days_to_expiry = (self.expiration - date.today()).days
if days_to_expiry <= 0:
logger.debug("Option expired, cannot calculate Greeks")
return {}
# Calculate Greeks using existing calculator
greeks = calculate_advanced_greeks(
stock_price=stock_price,
strike_price=self.strike,
days_to_expiry=days_to_expiry,
risk_free_rate=self.risk_free_rate,
implied_volatility=self.implied_volatility,
option_type=self.option_type.value
)
# Adjust for position type (short reverses signs)
multiplier = -1 if self.position_type == PositionType.SHORT else 1
# Adjust for quantity
adjusted_greeks = {
k: v * multiplier * self.quantity
for k, v in greeks.items()
}
return adjusted_greeks
[docs]
@dataclass
class OptionsStrategy:
"""
Complete options strategy with multiple legs
Attributes:
name: Strategy name
strategy_type: Type of strategy
legs: List of option legs
stock_position: Shares of underlying stock (positive=long, negative=short)
current_stock_price: Current price of underlying
target_date: Target date for P&L calculation (default: expiration)
"""
name: str
strategy_type: StrategyType
legs: List[OptionLeg]
current_stock_price: float
stock_position: int = 0 # Number of shares
target_date: Optional[date] = None
metadata: Dict = field(default_factory=dict)
[docs]
def net_premium(self) -> float:
"""Calculate net premium (positive = credit, negative = debit)"""
premium = 0
for leg in self.legs:
if leg.position_type == PositionType.LONG:
premium -= leg.premium * 100 * leg.quantity
else: # SHORT
premium += leg.premium * 100 * leg.quantity
return premium
[docs]
def max_profit(self) -> float:
"""Calculate maximum profit potential"""
if self.strategy_type == StrategyType.COVERED_CALL:
# Max profit: stock gains + premium received
strike = self.legs[0].strike
stock_gain = (strike - self.current_stock_price) * self.stock_position
return stock_gain + self.net_premium()
elif self.strategy_type in [StrategyType.BULL_CALL_SPREAD, StrategyType.BEAR_PUT_SPREAD]:
# Max profit: spread width - net debit
strikes = sorted([leg.strike for leg in self.legs])
spread_width = (strikes[1] - strikes[0]) * 100
return spread_width - abs(self.net_premium())
elif self.strategy_type in [StrategyType.BEAR_CALL_SPREAD, StrategyType.BULL_PUT_SPREAD]:
# Max profit: net credit received
return self.net_premium()
elif self.strategy_type == StrategyType.IRON_CONDOR:
# Max profit: net credit received
return self.net_premium()
else:
# For undefined strategies, calculate at extreme prices
max_pnl = float('-inf')
for price in np.linspace(0, self.current_stock_price * 3, 1000):
pnl = self.pnl_at_price(price)
max_pnl = max(max_pnl, pnl)
return max_pnl if max_pnl > float('-inf') else None
[docs]
def max_loss(self) -> float:
"""Calculate maximum loss potential"""
if self.strategy_type == StrategyType.COVERED_CALL:
# Max loss: stock goes to zero minus premium received
stock_loss = -self.current_stock_price * self.stock_position
return stock_loss + self.net_premium()
elif self.strategy_type == StrategyType.PROTECTIVE_PUT:
# Max loss: strike - current price - premium paid
strike = self.legs[0].strike
return (strike - self.current_stock_price) * self.stock_position + self.net_premium()
elif self.strategy_type in [StrategyType.BULL_CALL_SPREAD, StrategyType.BEAR_PUT_SPREAD]:
# Max loss: net debit paid
return self.net_premium()
elif self.strategy_type in [StrategyType.BEAR_CALL_SPREAD, StrategyType.BULL_PUT_SPREAD]:
# Max loss: spread width - net credit
strikes = sorted([leg.strike for leg in self.legs])
spread_width = (strikes[1] - strikes[0]) * 100
return self.net_premium() - spread_width
elif self.strategy_type == StrategyType.IRON_CONDOR:
# Max loss: width of widest spread - net credit
strikes = sorted([leg.strike for leg in self.legs])
call_spread = (strikes[3] - strikes[2]) * 100
put_spread = (strikes[1] - strikes[0]) * 100
max_spread = max(call_spread, put_spread)
return self.net_premium() - max_spread
else:
# For undefined strategies, calculate at extreme prices
min_pnl = float('inf')
for price in np.linspace(0, self.current_stock_price * 3, 1000):
pnl = self.pnl_at_price(price)
min_pnl = min(min_pnl, pnl)
return min_pnl if min_pnl < float('inf') else None
[docs]
def breakeven_points(self) -> List[float]:
"""Calculate breakeven stock prices"""
breakevens = []
# Sample prices and find where P&L crosses zero
prices = np.linspace(0, self.current_stock_price * 3, 1000)
pnls = [self.pnl_at_price(p) for p in prices]
for i in range(len(pnls) - 1):
# Check for zero crossing
if (pnls[i] <= 0 and pnls[i+1] >= 0) or (pnls[i] >= 0 and pnls[i+1] <= 0):
# Linear interpolation to find exact breakeven
t = -pnls[i] / (pnls[i+1] - pnls[i])
breakeven = prices[i] + t * (prices[i+1] - prices[i])
breakevens.append(round(breakeven, 2))
return breakevens
[docs]
def pnl_at_price(self, stock_price: float) -> float:
"""Calculate total P&L at a given stock price"""
total_pnl = 0
# Add stock position P&L
if self.stock_position != 0:
stock_pnl = (stock_price - self.current_stock_price) * self.stock_position
total_pnl += stock_pnl
# Add options P&L
for leg in self.legs:
total_pnl += leg.pnl_at_price(stock_price)
return total_pnl
[docs]
def payoff_diagram(self, price_range: Optional[Tuple[float, float]] = None,
num_points: int = 100) -> Tuple[np.ndarray, np.ndarray]:
"""
Generate data for payoff diagram
Returns:
(prices, pnls): Arrays for plotting
"""
if price_range is None:
# Default range: ±50% from current price
min_price = self.current_stock_price * 0.5
max_price = self.current_stock_price * 1.5
else:
min_price, max_price = price_range
prices = np.linspace(min_price, max_price, num_points)
pnls = np.array([self.pnl_at_price(p) for p in prices])
return prices, pnls
[docs]
def risk_metrics(self) -> Dict:
"""Calculate risk metrics for the strategy"""
max_p = self.max_profit()
max_l = self.max_loss()
breakevens = self.breakeven_points()
net_prem = self.net_premium()
# Calculate risk/reward ratio
if max_l and max_l < 0:
risk_reward = abs(max_p / max_l) if max_p else 0
else:
risk_reward = None
# Calculate probability of profit (simplified)
# This is a rough estimate based on breakeven points
if len(breakevens) == 1:
# Single breakeven (e.g., long call/put)
if breakevens[0] > self.current_stock_price:
pop = 50 * (1 - (breakevens[0] - self.current_stock_price) / self.current_stock_price)
else:
pop = 50 * (1 + (self.current_stock_price - breakevens[0]) / self.current_stock_price)
pop = max(0, min(100, pop))
elif len(breakevens) == 2:
# Two breakevens (e.g., iron condor)
range_width = breakevens[1] - breakevens[0]
current_in_range = breakevens[0] <= self.current_stock_price <= breakevens[1]
if current_in_range:
pop = 70 # Rough estimate
else:
pop = 30
else:
pop = None
return {
"max_profit": max_p,
"max_loss": max_l,
"net_premium": net_prem,
"breakeven_points": breakevens,
"risk_reward_ratio": risk_reward,
"probability_of_profit": pop,
"current_pnl": self.pnl_at_price(self.current_stock_price),
"debit_credit": "Credit" if net_prem > 0 else "Debit",
}
[docs]
def advanced_greeks(self) -> Dict[str, float]:
"""
Calculate aggregate advanced Greeks for entire strategy
Returns dictionary with all Greeks summed across legs:
- delta, gamma, theta, vega (first-order)
- vanna, charm, vomma (second-order/advanced)
"""
totals = {
'delta': 0.0,
'gamma': 0.0,
'theta': 0.0,
'vega': 0.0,
'vanna': 0.0,
'charm': 0.0,
'vomma': 0.0
}
# Aggregate Greeks from all legs
for leg in self.legs:
leg_greeks = leg.calculate_advanced_greeks(self.current_stock_price)
for greek in totals:
if greek in leg_greeks:
totals[greek] += leg_greeks[greek]
# Add stock position delta (100 shares = 1.0 delta)
if self.stock_position != 0:
totals['delta'] += self.stock_position / 100
return totals
[docs]
def to_dict(self) -> Dict:
"""Convert strategy to dictionary for serialization"""
data = {
"name": self.name,
"strategy_type": self.strategy_type.value,
"current_stock_price": self.current_stock_price,
"stock_position": self.stock_position,
"legs": [
{
"option_type": leg.option_type.value,
"position_type": leg.position_type.value,
"strike": leg.strike,
"premium": leg.premium,
"quantity": leg.quantity,
"expiration": leg.expiration.isoformat() if leg.expiration else None,
"implied_volatility": leg.implied_volatility,
"risk_free_rate": leg.risk_free_rate,
}
for leg in self.legs
],
"risk_metrics": self.risk_metrics(),
"metadata": self.metadata,
}
# Add advanced Greeks if available
if any(leg.implied_volatility for leg in self.legs):
data["advanced_greeks"] = self.advanced_greeks()
return data
[docs]
class StrategyBuilder:
"""Builder for common options strategies"""
[docs]
@staticmethod
def covered_call(stock_price: float, shares: int, call_strike: float,
call_premium: float, expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Covered Call: Own stock + Sell call
Strategy: Generate income on existing stock position
Max Profit: (Strike - Stock Price) + Premium
Max Loss: Stock price - Premium
Breakeven: Stock Price - Premium
Args:
implied_volatility: IV as decimal (e.g., 0.35 for 35%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
leg = OptionLeg(
option_type=OptionType.CALL,
position_type=PositionType.SHORT,
strike=call_strike,
premium=call_premium,
quantity=shares // 100,
expiration=expiration,
implied_volatility=implied_volatility,
risk_free_rate=risk_free_rate
)
return OptionsStrategy(
name=f"Covered Call on {ticker}",
strategy_type=StrategyType.COVERED_CALL,
legs=[leg],
stock_position=shares,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"strategy_description": "Generate income by selling calls against stock position"
}
)
[docs]
@staticmethod
def protective_put(stock_price: float, shares: int, put_strike: float,
put_premium: float, expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Protective Put: Own stock + Buy put
Strategy: Protect downside on stock position
Max Profit: Unlimited (stock can rise infinitely)
Max Loss: (Stock Price - Strike) + Premium
Breakeven: Stock Price + Premium
Args:
implied_volatility: IV as decimal (e.g., 0.25 for 25%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
leg = OptionLeg(
option_type=OptionType.PUT,
position_type=PositionType.LONG,
strike=put_strike,
premium=put_premium,
quantity=shares // 100,
expiration=expiration,
implied_volatility=implied_volatility,
risk_free_rate=risk_free_rate
)
return OptionsStrategy(
name=f"Protective Put on {ticker}",
strategy_type=StrategyType.PROTECTIVE_PUT,
legs=[leg],
stock_position=shares,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"strategy_description": "Protect stock position with downside insurance"
}
)
[docs]
@staticmethod
def cash_secured_put(stock_price: float, put_strike: float, put_premium: float,
quantity: int, expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Cash-Secured Put: Sell put (with cash to buy stock if assigned)
Strategy: Generate income or acquire stock at lower price
Max Profit: Premium received
Max Loss: Strike - Premium (if stock goes to zero)
Breakeven: Strike - Premium
Args:
implied_volatility: IV as decimal (e.g., 0.30 for 30%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
leg = OptionLeg(
option_type=OptionType.PUT,
position_type=PositionType.SHORT,
strike=put_strike,
premium=put_premium,
quantity=quantity,
expiration=expiration,
implied_volatility=implied_volatility,
risk_free_rate=risk_free_rate
)
return OptionsStrategy(
name=f"Cash-Secured Put on {ticker}",
strategy_type=StrategyType.CASH_SECURED_PUT,
legs=[leg],
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"cash_required": put_strike * 100 * quantity,
"strategy_description": "Generate income or acquire stock at discount"
}
)
[docs]
@staticmethod
def bull_call_spread(stock_price: float, long_strike: float, short_strike: float,
long_premium: float, short_premium: float, quantity: int,
expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Bull Call Spread: Buy lower strike call + Sell higher strike call
Strategy: Moderately bullish with limited risk
Max Profit: (Short Strike - Long Strike) - Net Debit
Max Loss: Net Debit
Breakeven: Long Strike + Net Debit
Args:
implied_volatility: IV as decimal (e.g., 0.35 for 35%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
legs = [
OptionLeg(OptionType.CALL, PositionType.LONG, long_strike, long_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.CALL, PositionType.SHORT, short_strike, short_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"Bull Call Spread on {ticker}",
strategy_type=StrategyType.BULL_CALL_SPREAD,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"spread_width": short_strike - long_strike,
"strategy_description": "Bullish strategy with capped profit and loss"
}
)
[docs]
@staticmethod
def iron_condor(stock_price: float,
put_long_strike: float, put_short_strike: float,
call_short_strike: float, call_long_strike: float,
put_long_premium: float, put_short_premium: float,
call_short_premium: float, call_long_premium: float,
quantity: int, expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Iron Condor: Sell put spread + Sell call spread
Strategy: Profit from low volatility (stock stays in range)
Max Profit: Net Credit
Max Loss: Width of widest spread - Net Credit
Breakevens: Two points (at put spread and call spread)
Args:
implied_volatility: IV as decimal (e.g., 0.20 for 20%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
legs = [
OptionLeg(OptionType.PUT, PositionType.LONG, put_long_strike, put_long_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.PUT, PositionType.SHORT, put_short_strike, put_short_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.CALL, PositionType.SHORT, call_short_strike, call_short_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.CALL, PositionType.LONG, call_long_strike, call_long_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"Iron Condor on {ticker}",
strategy_type=StrategyType.IRON_CONDOR,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"profit_range": (put_short_strike, call_short_strike),
"strategy_description": "Profit from range-bound stock movement"
}
)
[docs]
@staticmethod
def long_call(stock_price: float, strike: float, premium: float,
quantity: int, expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Long Call: Buy call option
Strategy: Bullish - profit from stock rise with limited risk
Max Profit: Unlimited (stock can rise infinitely)
Max Loss: Premium paid
Breakeven: Strike + Premium
Args:
implied_volatility: IV as decimal (e.g., 0.35 for 35%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
leg = OptionLeg(
option_type=OptionType.CALL,
position_type=PositionType.LONG,
strike=strike,
premium=premium,
quantity=quantity,
expiration=expiration,
implied_volatility=implied_volatility,
risk_free_rate=risk_free_rate
)
return OptionsStrategy(
name=f"Long Call on {ticker}",
strategy_type=StrategyType.LONG_CALL,
legs=[leg],
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"strategy_description": "Bullish speculation with limited risk"
}
)
[docs]
@staticmethod
def long_put(stock_price: float, strike: float, premium: float,
quantity: int, expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Long Put: Buy put option
Strategy: Bearish - profit from stock decline with limited risk
Max Profit: Strike - Premium (if stock goes to zero)
Max Loss: Premium paid
Breakeven: Strike - Premium
Args:
implied_volatility: IV as decimal (e.g., 0.35 for 35%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
leg = OptionLeg(
option_type=OptionType.PUT,
position_type=PositionType.LONG,
strike=strike,
premium=premium,
quantity=quantity,
expiration=expiration,
implied_volatility=implied_volatility,
risk_free_rate=risk_free_rate
)
return OptionsStrategy(
name=f"Long Put on {ticker}",
strategy_type=StrategyType.LONG_PUT,
legs=[leg],
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"strategy_description": "Bearish speculation with limited risk"
}
)
[docs]
@staticmethod
def bull_put_spread(stock_price: float, long_strike: float, short_strike: float,
long_premium: float, short_premium: float, quantity: int,
expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Bull Put Spread: Buy lower strike put + Sell higher strike put
Strategy: Moderately bullish with limited risk (credit spread)
Max Profit: Net Credit
Max Loss: (Short Strike - Long Strike) - Net Credit
Breakeven: Short Strike - Net Credit
Args:
implied_volatility: IV as decimal (e.g., 0.25 for 25%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
legs = [
OptionLeg(OptionType.PUT, PositionType.LONG, long_strike, long_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.PUT, PositionType.SHORT, short_strike, short_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"Bull Put Spread on {ticker}",
strategy_type=StrategyType.BULL_PUT_SPREAD,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"spread_width": short_strike - long_strike,
"strategy_description": "Bullish credit spread with capped profit and loss"
}
)
[docs]
@staticmethod
def bear_call_spread(stock_price: float, long_strike: float, short_strike: float,
long_premium: float, short_premium: float, quantity: int,
expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Bear Call Spread: Sell lower strike call + Buy higher strike call
Strategy: Moderately bearish with limited risk (credit spread)
Max Profit: Net Credit
Max Loss: (Long Strike - Short Strike) - Net Credit
Breakeven: Short Strike + Net Credit
Args:
implied_volatility: IV as decimal (e.g., 0.25 for 25%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
legs = [
OptionLeg(OptionType.CALL, PositionType.SHORT, short_strike, short_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.CALL, PositionType.LONG, long_strike, long_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"Bear Call Spread on {ticker}",
strategy_type=StrategyType.BEAR_CALL_SPREAD,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"spread_width": long_strike - short_strike,
"strategy_description": "Bearish credit spread with capped profit and loss"
}
)
[docs]
@staticmethod
def bear_put_spread(stock_price: float, long_strike: float, short_strike: float,
long_premium: float, short_premium: float, quantity: int,
expiration: date, ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Bear Put Spread: Buy higher strike put + Sell lower strike put
Strategy: Moderately bearish with limited risk (debit spread)
Max Profit: (Long Strike - Short Strike) - Net Debit
Max Loss: Net Debit
Breakeven: Long Strike - Net Debit
Args:
implied_volatility: IV as decimal (e.g., 0.30 for 30%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
legs = [
OptionLeg(OptionType.PUT, PositionType.LONG, long_strike, long_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.PUT, PositionType.SHORT, short_strike, short_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"Bear Put Spread on {ticker}",
strategy_type=StrategyType.BEAR_PUT_SPREAD,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"spread_width": long_strike - short_strike,
"strategy_description": "Bearish debit spread with capped profit and loss"
}
)
[docs]
@staticmethod
def butterfly(stock_price: float, lower_strike: float, middle_strike: float, upper_strike: float,
lower_premium: float, middle_premium: float, upper_premium: float,
quantity: int, expiration: date, option_type: str = "call",
ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Butterfly Spread: Buy 1 low strike + Sell 2 middle strikes + Buy 1 high strike
Strategy: Profit from low volatility (stock stays near middle strike)
Max Profit: (Middle Strike - Lower Strike) - Net Debit
Max Loss: Net Debit
Breakevens: Two points around middle strike
Args:
implied_volatility: IV as decimal (e.g., 0.25 for 25%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
opt_type = OptionType.CALL if option_type.lower() == "call" else OptionType.PUT
legs = [
OptionLeg(opt_type, PositionType.LONG, lower_strike, lower_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(opt_type, PositionType.SHORT, middle_strike, middle_premium, quantity * 2, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(opt_type, PositionType.LONG, upper_strike, upper_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"{option_type.capitalize()} Butterfly on {ticker}",
strategy_type=StrategyType.BUTTERFLY,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"implied_volatility": implied_volatility,
"target_price": middle_strike,
"strategy_description": "Profit from low volatility around middle strike"
}
)
[docs]
@staticmethod
def straddle(stock_price: float, strike: float, call_premium: float, put_premium: float,
quantity: int, expiration: date, position: str = "long",
ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Straddle: Buy/Sell both call and put at same strike
Long Straddle: Profit from high volatility (big move in either direction)
Short Straddle: Profit from low volatility (stock stays near strike)
Long Straddle:
Max Profit: Unlimited
Max Loss: Total premium paid
Breakevens: Strike ± Total Premium
Short Straddle:
Max Profit: Total premium received
Max Loss: Unlimited
Breakevens: Strike ± Total Premium
Args:
implied_volatility: IV as decimal (e.g., 0.30 for 30%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
pos_type = PositionType.LONG if position.lower() == "long" else PositionType.SHORT
legs = [
OptionLeg(OptionType.CALL, pos_type, strike, call_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.PUT, pos_type, strike, put_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"{position.capitalize()} Straddle on {ticker}",
strategy_type=StrategyType.STRADDLE,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"position": position,
"implied_volatility": implied_volatility,
"strategy_description": f"{position.capitalize()} volatility play"
}
)
[docs]
@staticmethod
def strangle(stock_price: float, put_strike: float, call_strike: float,
put_premium: float, call_premium: float, quantity: int,
expiration: date, position: str = "long",
ticker: str = "UNKNOWN",
implied_volatility: Optional[float] = None,
risk_free_rate: float = 0.05) -> OptionsStrategy:
"""
Strangle: Buy/Sell call and put at different strikes
Long Strangle: Profit from high volatility (cheaper than straddle)
Short Strangle: Profit from low volatility (higher risk than short straddle)
Long Strangle:
Max Profit: Unlimited
Max Loss: Total premium paid
Breakevens: Put Strike - Premium, Call Strike + Premium
Short Strangle:
Max Profit: Total premium received
Max Loss: Unlimited
Breakevens: Put Strike - Premium, Call Strike + Premium
Args:
implied_volatility: IV as decimal (e.g., 0.30 for 30%) - optional for Greeks
risk_free_rate: Risk-free rate as decimal (default: 0.05)
"""
pos_type = PositionType.LONG if position.lower() == "long" else PositionType.SHORT
legs = [
OptionLeg(OptionType.PUT, pos_type, put_strike, put_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate),
OptionLeg(OptionType.CALL, pos_type, call_strike, call_premium, quantity, expiration,
implied_volatility=implied_volatility, risk_free_rate=risk_free_rate)
]
return OptionsStrategy(
name=f"{position.capitalize()} Strangle on {ticker}",
strategy_type=StrategyType.STRANGLE,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"position": position,
"implied_volatility": implied_volatility,
"strike_range": (put_strike, call_strike),
"strategy_description": f"{position.capitalize()} volatility play with OTM options"
}
)
[docs]
@staticmethod
def calendar_spread(stock_price: float, strike: float,
near_premium: float, far_premium: float,
near_expiration: date, far_expiration: date,
quantity: int, option_type: str = "call",
ticker: str = "UNKNOWN") -> OptionsStrategy:
"""
Calendar Spread (Time Spread): Sell near-term + Buy far-term at same strike
Strategy: Profit from time decay difference and volatility
Max Profit: Varies (depends on volatility changes)
Max Loss: Net Debit
Best Case: Stock stays near strike at near expiration
"""
opt_type = OptionType.CALL if option_type.lower() == "call" else OptionType.PUT
legs = [
OptionLeg(opt_type, PositionType.SHORT, strike, near_premium, quantity, near_expiration),
OptionLeg(opt_type, PositionType.LONG, strike, far_premium, quantity, far_expiration)
]
return OptionsStrategy(
name=f"{option_type.capitalize()} Calendar Spread on {ticker}",
strategy_type=StrategyType.CALENDAR_SPREAD,
legs=legs,
stock_position=0,
current_stock_price=stock_price,
metadata={
"ticker": ticker,
"target_price": strike,
"time_spread_days": (far_expiration - near_expiration).days,
"strategy_description": "Profit from time decay and volatility changes"
}
)