186 lines
5.5 KiB
Python
186 lines
5.5 KiB
Python
"""
|
|
FinanceIQ v5.1 — Options Analytics Engine
|
|
Black-Scholes pricing, Greeks, implied volatility, and payoff diagrams.
|
|
"""
|
|
import math
|
|
from scipy.stats import norm
|
|
|
|
|
|
def d1(S, K, T, r, sigma):
|
|
"""Calculate d1 for Black-Scholes."""
|
|
if T <= 0 or sigma <= 0:
|
|
return 0.0
|
|
return (math.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * math.sqrt(T))
|
|
|
|
|
|
def d2(S, K, T, r, sigma):
|
|
"""Calculate d2 for Black-Scholes."""
|
|
return d1(S, K, T, r, sigma) - sigma * math.sqrt(T) if T > 0 and sigma > 0 else 0.0
|
|
|
|
|
|
def bs_call_price(S, K, T, r, sigma):
|
|
"""Black-Scholes call option price."""
|
|
if T <= 0:
|
|
return max(S - K, 0)
|
|
_d1 = d1(S, K, T, r, sigma)
|
|
_d2 = d2(S, K, T, r, sigma)
|
|
return S * norm.cdf(_d1) - K * math.exp(-r * T) * norm.cdf(_d2)
|
|
|
|
|
|
def bs_put_price(S, K, T, r, sigma):
|
|
"""Black-Scholes put option price."""
|
|
if T <= 0:
|
|
return max(K - S, 0)
|
|
_d1 = d1(S, K, T, r, sigma)
|
|
_d2 = d2(S, K, T, r, sigma)
|
|
return K * math.exp(-r * T) * norm.cdf(-_d2) - S * norm.cdf(-_d1)
|
|
|
|
|
|
def calculate_greeks(S, K, T, r, sigma, option_type="call"):
|
|
"""
|
|
Calculate all Greeks for an option.
|
|
S: underlying price, K: strike, T: time to expiry (years),
|
|
r: risk-free rate, sigma: implied volatility
|
|
"""
|
|
if T <= 0 or sigma <= 0:
|
|
intrinsic = max(S - K, 0) if option_type == "call" else max(K - S, 0)
|
|
return {
|
|
"delta": 1.0 if (option_type == "call" and S > K) else (-1.0 if option_type == "put" and K > S else 0.0),
|
|
"gamma": 0.0,
|
|
"theta": 0.0,
|
|
"vega": 0.0,
|
|
"rho": 0.0,
|
|
"price": intrinsic
|
|
}
|
|
|
|
_d1 = d1(S, K, T, r, sigma)
|
|
_d2 = d2(S, K, T, r, sigma)
|
|
sqrt_T = math.sqrt(T)
|
|
|
|
# Delta
|
|
delta = norm.cdf(_d1) if option_type == "call" else norm.cdf(_d1) - 1
|
|
|
|
# Gamma (same for calls & puts)
|
|
gamma = norm.pdf(_d1) / (S * sigma * sqrt_T)
|
|
|
|
# Theta (per day)
|
|
theta_common = -(S * norm.pdf(_d1) * sigma) / (2 * sqrt_T)
|
|
if option_type == "call":
|
|
theta = (theta_common - r * K * math.exp(-r * T) * norm.cdf(_d2)) / 365
|
|
else:
|
|
theta = (theta_common + r * K * math.exp(-r * T) * norm.cdf(-_d2)) / 365
|
|
|
|
# Vega (per 1% move in IV)
|
|
vega = S * sqrt_T * norm.pdf(_d1) / 100
|
|
|
|
# Rho (per 1% move in rate)
|
|
if option_type == "call":
|
|
rho = K * T * math.exp(-r * T) * norm.cdf(_d2) / 100
|
|
else:
|
|
rho = -K * T * math.exp(-r * T) * norm.cdf(-_d2) / 100
|
|
|
|
# Price
|
|
price = bs_call_price(S, K, T, r, sigma) if option_type == "call" else bs_put_price(S, K, T, r, sigma)
|
|
|
|
return {
|
|
"delta": round(delta, 4),
|
|
"gamma": round(gamma, 6),
|
|
"theta": round(theta, 4),
|
|
"vega": round(vega, 4),
|
|
"rho": round(rho, 4),
|
|
"price": round(price, 2)
|
|
}
|
|
|
|
|
|
def implied_volatility(market_price, S, K, T, r, option_type="call", tol=1e-6, max_iter=100):
|
|
"""
|
|
Calculate implied volatility using Newton-Raphson method.
|
|
"""
|
|
if T <= 0 or market_price <= 0:
|
|
return 0.0
|
|
|
|
sigma = 0.3 # initial guess
|
|
for _ in range(max_iter):
|
|
if option_type == "call":
|
|
price = bs_call_price(S, K, T, r, sigma)
|
|
else:
|
|
price = bs_put_price(S, K, T, r, sigma)
|
|
|
|
diff = price - market_price
|
|
if abs(diff) < tol:
|
|
return round(sigma, 6)
|
|
|
|
# Vega for Newton-Raphson step
|
|
_d1 = d1(S, K, T, r, sigma)
|
|
vega = S * math.sqrt(T) * norm.pdf(_d1)
|
|
if vega < 1e-12:
|
|
break
|
|
sigma -= diff / vega
|
|
sigma = max(sigma, 0.001) # floor
|
|
|
|
return round(sigma, 6)
|
|
|
|
|
|
def payoff_diagram(S, K, premium, option_type="call", is_long=True, num_points=50):
|
|
"""
|
|
Generate payoff diagram data points.
|
|
Returns list of {price, payoff, profit} dicts.
|
|
"""
|
|
low = K * 0.7
|
|
high = K * 1.3
|
|
step = (high - low) / num_points
|
|
points = []
|
|
for i in range(num_points + 1):
|
|
price = low + i * step
|
|
if option_type == "call":
|
|
payoff = max(price - K, 0)
|
|
else:
|
|
payoff = max(K - price, 0)
|
|
|
|
if is_long:
|
|
profit = payoff - premium
|
|
else:
|
|
profit = premium - payoff
|
|
|
|
points.append({
|
|
"price": round(price, 2),
|
|
"payoff": round(payoff, 2),
|
|
"profit": round(profit, 2)
|
|
})
|
|
return points
|
|
|
|
|
|
def enrich_chain_with_greeks(chain_data, current_price, expiry_date_str, risk_free_rate=0.05):
|
|
"""
|
|
Add Greeks to each option in a chain.
|
|
chain_data: list of dicts from yfinance option_chain
|
|
expiry_date_str: 'YYYY-MM-DD'
|
|
"""
|
|
from datetime import datetime
|
|
try:
|
|
expiry = datetime.strptime(expiry_date_str, "%Y-%m-%d")
|
|
today = datetime.now()
|
|
T = max((expiry - today).days / 365.0, 0.001)
|
|
except:
|
|
T = 0.1 # fallback
|
|
|
|
for opt in chain_data:
|
|
strike = opt.get("strike", 0)
|
|
last_price = opt.get("lastPrice", 0)
|
|
iv = opt.get("impliedVolatility", 0.3)
|
|
opt_type = "call" if opt.get("_type", "call") == "call" else "put"
|
|
|
|
if strike > 0 and current_price > 0 and iv > 0:
|
|
greeks = calculate_greeks(current_price, strike, T, risk_free_rate, iv, opt_type)
|
|
opt["delta"] = greeks["delta"]
|
|
opt["gamma"] = greeks["gamma"]
|
|
opt["theta"] = greeks["theta"]
|
|
opt["vega"] = greeks["vega"]
|
|
opt["rho"] = greeks["rho"]
|
|
opt["bs_price"] = greeks["price"]
|
|
else:
|
|
opt["delta"] = opt["gamma"] = opt["theta"] = opt["vega"] = opt["rho"] = 0
|
|
opt["bs_price"] = 0
|
|
|
|
return chain_data
|