Python Risk Management Series (Part 2): Renting Stocks and Hallucinating Arbitrage
In Part 1, we talked about how to stop paying for insurance. In Part 2, we are going to talk about how to stop paying for stocks.
The trouble with buying stocks is that they are capital inefficient. If you want to buy 100 shares of Dell, you have to find roughly $13,500. If you are a “rational economic actor”—a phrase economists use to describe people who don’t exist—you would prefer to get the exact same exposure to Dell for, say, $4,000, and keep the other $9,000 in T-Bills yielding 4.5%.
The financial industry has a solution for this. It is called the Stock Replacement Strategy, or if you are on Reddit, the Poor Man’s Covered Call (PMCC).
The Strategy: The “Poor Man’s” Hedge Fund
Technically known as a Long Call Diagonal Debit Spread, the PMCC has two moving parts:
- The Anchor (Long Leg): You buy a Deep-In-The-Money LEAPS Call (e.g., 0.80 Delta, 1+ Year out). This acts as your “stock.”
- The Income (Short Leg): You sell a Near-Term Out-Of-The-Money Call (e.g., 0.30 Delta, 30 Days out) against it.
“Don’t you sell the call to finance the LEAP?” Yes, but partially. Unlike the “Zero-Cost Collar” in Part 1 where the trade was free, a PMCC is a Debit transaction so it cost you money.
- The LEAPS might cost $10.00.
- You sell the Near-Term call for $1.00.
- Net Cost: $9.00.
You are not getting the position for free. You are getting a “discount” on your entry. The goal is to keep selling that $1.00 call every single month until the LEAPS is paid for.
The Python Focus: The code below focuses strictly on Leg 1 (The LEAPS). Why? Because finding a short call is easy—you just look 30 days out. But finding a LEAPS that is liquid, fairly valued, and offers good leverage? That is where the edge is. This script hunts for the “Anchor” of the trade.
The Code: Hunting for Value (and Bugs)
The Python script below scans the market for these “rental agreements.” It looks for:
- Deep ITM Calls: Delta between 0.75 and 0.85.
- Long Duration: Expiration > 1 year.
- “Fair Value”: It runs a Black-Scholes calculation to see if the option is “cheap” relative to its implied volatility.
We ran the code, and the output (see the screenshot) tells a tale of two markets.

1. The Value Play: Crypto Miners (CIFR)
Look at the top rows. The scanner found CIFR (Cipher Mining).
- Strike: $22.00
- Expiry: Dec 2027 (years away)
- Diff%: -15.2%
The Trade: The model thinks the “Fair Value” of this option is $9.85, but the market is selling it for $8.35. The code is telling you that this leverage is on sale. You can control this volatile crypto-mining stock for 15% less than the theoretical model suggests you should pay. The Lev (Leverage) column is roughly 1.72, meaning for every $1 you put in, you get $1.72 of exposure to the stock.
2. The Hallucination: DELL
Now, look at the very last row. This is why we write Python scripts instead of just trusting them.
- Ticker: DELL
- Mark: $45.82
- Diff%: 56,881.1%
The computer has flagged this as the greatest trading opportunity in the history of capitalism. It claims the Fair Value is $0.08, but the price is $45.82.
What happened? The “Mark” is correct—Dell stock is trading around $135, so a $90 strike call should be worth about $45 (Intrinsic Value). But the “Fair Value” calculation hallucinated. Why? Look at the columns Bid and Ask. They are None.
The data feed for this specific option was illiquid or broken at the moment of the snapshot. Because the script couldn’t find a bid/ask, it likely fed garbage into the Black-Scholes model, which spit out a Fair Value of 8 cents.
The Catch: There Is No Free Lunch
It is important to pause here. If “renting” stocks is cheaper than buying them, why buy them at all?
There are specific risks to the PMCC that the Python script cannot code away:
- The “Capped Upside” (The FOMO Risk): If you implement the full PMCC and sell a short call against your LEAPS, and the stock “moons” (goes up 50% in a week), you lose. Your LEAPS gains value, but your Short Call loses value just as fast. You are capped. You watch the rocket ship take off while you are tethered to the ground.
- The “Gap” (Locked Loss): This is the nightmare scenario. If the stock crashes, your LEAPS loses value. If the stock rips, you are capped. But if the stock stays flat? You still have to beat the “debit” you paid. Unlike owning stock, where “flat” means you just lost time, with a PMCC, “flat” means your LEAPS is slowly rotting away due to time decay (Theta), while your short call might not be decaying fast enough to cover it.
- The Dividend Problem: When you own Dell stock, and Michael Dell decides to pay a dividend, a check shows up in your account. When you own a Dell LEAPS, you get nothing. The market prices this in, but you lose that compounding cash flow.
- Early Assignment: If your short call goes Deep ITM, the counterparty might exercise it early. You don’t own the shares. You have to scramble to exercise your LEAPS (burning any remaining time value) to cover the position. It is a logistical headache that simple stock owners never face.
The Takeaway
This is the most important lesson in algorithmic trading: Data is hard, and leverage is sharp. The script successfully found a legit deep-value play in CIFR (buying cheap volatility). But it also found a “ghost” in DELL.
If you blindly sorted by “Diff%” and auto-traded, you would be in trouble. But as a tool to filter 5,000 stocks down to 10 candidates? It’s perfect. It highlights the cheap leverage, and it highlights the broken data. It is your job to know the difference.
IMPORTANT: Disclaimer
Nothing in this article constitutes professional and/or financial advice, nor does any information on this site constitute a comprehensive or complete statement of the matters discussed or the law relating thereto. PythonForFinanceHub is not a fiduciary by virtue of any person’s use of or access to the Site or Content. You alone assume the sole responsibility of evaluating the merits and risks associated with the use of any information or other Content on the Site before making any decisions based on such information or other Content.
Put simply: This is code, not a crystal ball. Options trading involves significant risk and is not suitable for every investor. You can lose 100% of your capital. The code provided is for educational purposes only and may contain bugs (as seen above). Do your own research or hire a professional who actually knows what they are doing.
from polygon import RESTClient
from datetime import date, timedelta, datetime
import pandas as pd
import numpy as np
import math
import yfinance as yf
from google.colab import userdata
# --- CONFIGURATION ---
API_KEY = userdata.get("massive") # your Polygon key in Colab secrets
# Now supports multiple tickers
TICKERS = ["DELL", "CIFR", "EOSE"] # <-- edit list here
TARGET_DELTA = 0.80
DELTA_BUFFER = 0.05 # keep calls in [0.75, 0.85]
MAX_SPREAD_PCT = 0.10 # max (ask-bid)/mid
MIN_T = 1.0 # LEAPS = >= 1 year
RISK_FREE_RATE = 0.045 # 4.5%
MIN_THEO = 0.05 # avoid divide-by-near-zero in Diff%
def black_scholes_call(S, K, T, r, sigma, q=0.0):
"""
European call price with continuous dividend yield q.
S spot, K strike, T years, r risk-free, sigma annualized IV, q dividend yield.
"""
if T <= 0 or sigma <= 0 or S <= 0 or K <= 0:
return np.nan
d1 = (np.log(S / K) + (r - q + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
def N(x):
return 0.5 * (1 + math.erf(x / np.sqrt(2)))
return (S * np.exp(-q * T) * N(d1)) - (K * np.exp(-r * T) * N(d2))
def get_underlying_inputs(ticker):
"""
Get spot and dividend yield from yfinance.
Dividend yield is optional; defaults to 0 if missing.
"""
print(f"Fetching underlying data for {ticker} from yfinance...")
stock = yf.Ticker(ticker)
hist_1d = stock.history(period="1d")
if hist_1d.empty:
raise ValueError("yfinance returned no spot data.")
spot = float(hist_1d["Close"].iloc[-1])
try:
info = stock.info or {}
except Exception:
info = {}
div_yield = info.get("dividendYield", 0.0) or 0.0
return spot, float(div_yield)
def get_mark_price(contract):
"""
Robust mark (mid) price.
Uses bid/ask if available; otherwise last trade; otherwise day close.
Returns (mark, bid, ask).
"""
bid = getattr(contract.last_quote, "bid", None)
ask = getattr(contract.last_quote, "ask", None)
bid = float(bid) if bid not in (None, 0) else None
ask = float(ask) if ask not in (None, 0) else None
if bid is not None and ask is not None and ask >= bid:
mark = 0.5 * (bid + ask)
return mark, bid, ask
last = getattr(contract.last_trade, "price", None)
last = float(last) if last not in (None, 0) else None
if last is not None:
return last, bid, ask
close = getattr(contract.day, "close", None)
close = float(close) if close not in (None, 0) else None
if close is not None:
return close, bid, ask
return None, bid, ask
def find_value_leaps_iv_one_ticker(client, ticker):
"""
Runs your LEAPS cheapness scan for a single ticker.
Returns a DataFrame (possibly empty).
"""
# 1) Underlying inputs
spot, div_yield = get_underlying_inputs(ticker)
print(f"Spot Price: ${spot:.2f}")
print(f"Dividend Yield (yfinance): {div_yield*100:.2f}%")
print(f"Risk Free Rate: {RISK_FREE_RATE*100:.2f}%")
# 2) Pull LEAPS calls chain snapshot from Polygon
min_expiration = date.today() + timedelta(days=int(MIN_T * 365))
print("Fetching Options Chain from Polygon...")
try:
chain_iter = client.list_snapshot_options_chain(
ticker,
params={
"expiration_date.gte": min_expiration.isoformat(),
"contract_type": "call",
},
)
except Exception as e:
print(f"Polygon Error for {ticker}: {e}")
return pd.DataFrame()
candidates = []
print("Filtering and calculating IV-based Fair Value...")
for c in chain_iter:
if not c.greeks or not c.day:
continue
delta = getattr(c.greeks, "delta", None)
if delta is None:
continue
if not ((TARGET_DELTA - DELTA_BUFFER) <= delta <= (TARGET_DELTA + DELTA_BUFFER)):
continue
mark, bid, ask = get_mark_price(c)
if mark is None or mark <= 0:
continue
# Spread filter if we have bid+ask
if bid is not None and ask is not None and ask > 0:
spread_pct = (ask - bid) / mark
if spread_pct > MAX_SPREAD_PCT:
continue
else:
spread_pct = np.nan
strike = float(c.details.strike_price)
expiry_str = c.details.expiration_date
expiry = datetime.strptime(expiry_str, "%Y-%m-%d").date()
T = (expiry - date.today()).days / 365.0
if T <= 0:
continue
iv = getattr(c, "implied_volatility", None)
if iv is None or iv <= 0:
continue
iv = float(iv)
theo = black_scholes_call(spot, strike, T, RISK_FREE_RATE, iv, q=div_yield)
if theo is None or np.isnan(theo) or theo < MIN_THEO:
continue
diff_pct = (mark - theo) / theo
leverage = (delta * spot) / mark
candidates.append({
"Ticker": ticker,
"Expiry": expiry_str,
"Strike": strike,
"Delta": round(delta, 2),
"Bid": bid,
"Ask": ask,
"Mark": round(mark, 2),
"Spread%": round(spread_pct * 100, 1) if not np.isnan(spread_pct) else None,
"IV%": round(iv * 100, 1),
"Fair Val (IV BS)": round(theo, 2),
"Diff%": round(diff_pct * 100, 1),
"Lev": round(leverage, 2),
})
if not candidates:
return pd.DataFrame()
return pd.DataFrame(candidates).sort_values("Diff%")
def find_value_leaps_iv_multi(tickers):
"""
Multi-ticker wrapper.
Prints per-ticker tables and also returns a combined DataFrame.
"""
client = RESTClient(API_KEY)
all_dfs = []
for t in tickers:
print(f"\n==============================")
print(f" RUNNING LEAPS SCAN FOR {t}")
print(f"==============================")
df_t = find_value_leaps_iv_one_ticker(client, t)
if df_t.empty:
print(f"No candidates found for {t}.")
continue
print(f"\n--- LEAPS IV-BASED ANALYSIS FOR {t} ---")
print("(Negative Diff% = below IV-based BS fair value; Positive = above)")
print(df_t.to_string(index=False))
all_dfs.append(df_t)
if not all_dfs:
print("\nNo candidates found for any ticker.")
return pd.DataFrame()
combined = pd.concat(all_dfs, ignore_index=True).sort_values(["Ticker", "Diff%"])
return combined
if __name__ == "__main__":
combined_df = find_value_leaps_iv_multi(TICKERS)
# Optional: view combined cheapest across all names
if not combined_df.empty:
print("\n==========================================")
print(" CHEAPEST LEAPS ACROSS ALL TICKERS (TOP 25)")
print("==========================================")
print(combined_df.sort_values("Diff%").head(25).to_string(index=False))