DEV Community

ayushman4you-ctrl
ayushman4you-ctrl

Posted on

Build a Black-Scholes Option Greeks, ATR Position Sizer & FOIR Risk Engine in Python (Zero Bloat)

Most retail trading bots and financial scripts fail in live production not because the entry signal is bad, but because risk sizing, option volatility math, and credit leverage limits are hardcoded or ignored.

Recently, we upgraded PyQuant Library (v2.0 Enterprise) with a live browser-based Interactive Quant Terminal alongside our 126+ standalone Python scripts.

Here is the exact vectorized Python code behind the 3 core risk and valuation engines running inside our terminal—plus how you can test them live in your browser or drop them straight into your execution pipeline.


1. Dynamic ATR Volatility Position Sizing & Stop-Loss Engine

Fixed-lot trading destroys accounts during high-volatility regimes. Institutional systems size every trade dynamically so that a 2x ATR(14) stop-loss hit never exceeds a fixed percentage (e.g., 1.0%) of total account equity.

Core Formula:

  • $\text{Max Risk Capital} = \text{Account Capital} \times \frac{\text{Risk \%}}{100}$
  • $\text{Stop Distance} = 2 \times \text{ATR}_{14}$
  • $\text{Optimal Quantity} = \lfloor \frac{\text{Max Risk Capital}}{\text{Stop Distance}} \rfloor$

python
import pandas as pd
import numpy as np

def calculate_atr_position_size(
    df: pd.DataFrame, 
    capital: float = 500000.0, 
    risk_pct: float = 1.0, 
    atr_period: int = 14, 
    atr_multiplier: float = 2.0
) -> dict:
    df = df.copy()
    tr1 = df['High'] - df['Low']
    tr2 = (df['High'] - df['Close'].shift(1)).abs()
    tr3 = (df['Low'] - df['Close'].shift(1)).abs()

    df['TR'] = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
    df['ATR'] = df['TR'].ewm(alpha=1/atr_period, adjust=False).mean()

    latest_close = float(df['Close'].iloc[-1])
    latest_atr = float(df['ATR'].iloc[-1])

    max_risk_amount = capital * (risk_pct / 100.0)
    stop_distance = latest_atr * atr_multiplier
    long_stop_level = latest_close - stop_distance
    optimal_qty = int(np.floor(max_risk_amount / stop_distance)) if stop_distance > 0 else 0

    return {
        "Entry_Price": round(latest_close, 2),
        "ATR_14": round(latest_atr, 2),
        "Max_Allowed_Risk": round(max_risk_amount, 2),
        "Long_Stop_Loss_2xATR": round(long_stop_level, 2),
        "Optimal_Position_Qty": optimal_qty
    }
2. Black-Scholes Option Pricer, Delta & ATM Straddle Calculator
When pricing weekly index options or delta-hedging a multi-leg straddle, relying on heavy C-wrapped libraries causes deployment headaches. Using scipy.stats.norm, you can compute theoretical Call/Put prices, Deltas, and combined ATM Straddle premiums in sub-millisecond time.
import numpy as np
from scipy.stats import norm

def black_scholes_straddle_analyzer(
    spot: float, 
    strike: float, 
    days_to_expiry: float, 
    iv_pct: float, 
    risk_free_pct: float = 6.8
) -> dict:
    T = max(days_to_expiry / 365.0, 0.0001)
    sigma = iv_pct / 100.0
    r = risk_free_pct / 100.0

    d1 = (np.log(spot / strike) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)

    call_price = spot * norm.cdf(d1) - strike * np.exp(-r * T) * norm.cdf(d2)
    put_price = strike * np.exp(-r * T) * norm.cdf(-d2) - spot * norm.cdf(-d1)

    call_delta = norm.cdf(d1)
    put_delta = call_delta - 1.0
    gamma = norm.pdf(d1) / (spot * sigma * np.sqrt(T))

    return {
        "Theoretical_Call": round(float(call_price), 2),
        "Theoretical_Put": round(float(put_price), 2),
        "ATM_Straddle_Combined": round(float(call_price + put_price), 2),
        "Call_Delta": round(float(call_delta), 4),
        "Put_Delta": round(float(put_delta), 4),
        "Gamma": round(float(gamma), 6)
    }

# Example: Spot=24500, Strike=24500, 7 DTE, 15% IV
# print(black_scholes_straddle_analyzer(24500, 24500, 7, 15.0))
3. Implied Volatility (IV) Numerical Solver via Newton-Raphson
Market feeds give you the live option LTP, not the exact Implied Volatility. Using Vega as the analytical derivative of the Black-Scholes formula, Newton-Raphson converges on the exact market IV in 4 to 6 iterations.
import numpy as np
from scipy.stats import norm

def solve_implied_volatility(
    market_price: float, S: float, K: float, T: float, r: float, option_type: str = 'call'
) -> float:
    sigma = 0.25  # Initial 25% IV guess
    for _ in range(100):
        d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)

        if option_type == 'call':
            price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
        else:
            price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)

        diff = market_price - price
        if abs(diff) < 1e-6:
            return round(float(sigma * 100), 2)

        vega = S * norm.pdf(d1) * np.sqrt(T)
        if vega < 1e-8:
            break
        sigma = sigma + diff / vega

    return round(float(sigma * 100), 2)
4. Retail & Commercial Credit Underwriting: Amortization & FOIR Gatekeeper
In banking and fintech lending, Fixed Obligation to Income Ratio (FOIR) determines whether a borrower can service a new facility without breaching the 55% regulatory/credit norm.
def underwrite_loan_foir(
    principal: float, 
    annual_rate_pct: float, 
    tenure_months: int, 
    net_monthly_income: float, 
    existing_emis: float = 0.0,
    max_foir_limit: float = 55.0
) -> dict:
    r = (annual_rate_pct / 100.0) / 12.0
    if r > 0:
        proposed_emi = (principal * r * ((1 + r) ** tenure_months)) / (((1 + r) ** tenure_months) - 1)
    else:
        proposed_emi = principal / tenure_months

    total_interest = (proposed_emi * tenure_months) - principal
    total_monthly_obligations = existing_emis + proposed_emi
    foir_pct = (total_monthly_obligations / net_monthly_income) * 100.0

    return {
        "Monthly_EMI": round(proposed_emi, 2),
        "Total_Interest_Outflow": round(max(0.0, total_interest), 2),
        "Computed_FOIR_Pct": round(foir_pct, 2),
        "Credit_Decision": "ELIGIBLE" if foir_pct <= max_foir_limit else "REJECTED_HIGH_FOIR"
    }
🧮 Test These Models Live in Your Browser (Or Grab All 126+ Scripts)
You don't need to spin up a local environment just to run quick position-sizing or option-pricing checks during market hours:

🌐 Try the Live Interactive Quant Terminal & Browse 126+ Free Scripts: https://pyquant-library.netlify.app

⭐ Star the Open Index on GitHub: awesome-python-quant-scripts

📦 Download the Complete 126-Script Engineer ZIP Bundle ($19): Get Instant Access on Gumroad
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
eqvero profile image
Eqvero •

For the Newton-Raphson IV solver, one practical edge case in live options feeds is deep out-of-the-money strikes where Vega drops near zero.

When norm.pdf(d1) collapses, dividing diff by a tiny Vega causes the sigma update step to overshoot wildly, often pushing sigma into negative territory. In the next iteration, a non-positive sigma breaks d1 and throws a math error or NaN.

In production pipelines, two small guards make a noticeable difference:

  1. Clamping the updated volatility to a realistic bound (for example, sigma = max(0.001, min(sigma + diff / vega, 5.0))).
  2. Falling back to Brent's method or binary search if Newton-Raphson fails to converge within 15 iterations.

That prevents stale quotes or wide bid-ask spreads on far wings from blowing up the pricing worker.