Zero collar python
|

Python Risk Management Series (Part 1): The “Zero-Cost” Hedge

The basic premise of owning stocks is that you want them to go up. This is a good premise. It has worked, historically, quite well.

But sometimes stocks go down, and this is generally considered “bad.” If you are a rational economic actor, you might look at your portfolio of tech stocks and think, “I would like to keep the ‘going up’ part, but I would strictly prefer to avoid the ‘going down’ part.”

In financial markets, there is a product for this. It is called a Put option. You pay a premium, and if the market crashes, someone else reimburses you. It is insurance.

The problem with insurance, however, is that you have to pay for it. If you buy 5% Out-of-the-Money Puts on the Nasdaq every month, you are creating a permanent 3-4% drag on your annual returns. You are safe, sure, but you are also lighting a small pile of money on fire every 30 days just to sleep better at night.

So, for Part 1 of our Python Risk Management Series, we are going to do some financial engineering. We are going to build a Zero-Cost Collar.

The Structure: Trading FOMO for JOMO

The logic of the collar is simple: You want the Put (the floor), but you refuse to pay cash for it.

So you look at your portfolio and say, “Well, I probably don’t need infinite upside right now.” You sell a Call option—giving away your potential gains above a certain price—and you use the cash from that sale to buy the Put.

You are swapping your “FOMO” (Fear Of Missing Out) for “JOMO” (Joy Of Missing Out on a crash). If structured correctly, the net cost to your account is $0.00.

The Code: A Little Data Arbitrage

Before we look at the trade, we have to talk about the Python behind it.

A recurring theme in this series will be that financial data is expensive, and we prefer not to spend money if we don’t have to. We need high-fidelity options data (Greeks, bid-ask spreads) to structure these trades, which is why we use Polygon.io. But we don’t want to burn our API limits querying the spot price of QQQ every 10 seconds.

So, our script (see below) performs a little API Arbitrage:

  1. It uses yfinance (which is free) to handle the mundane task of checking the stock price.
  2. It only calls the Polygon API (which is paid/limited) when it needs to do the heavy lifting on the option chain.

It’s a small detail, but efficient markets are built on small efficiencies.

The Case Study: QQQ

We ran our scanner on the Nasdaq 100 (QQQ). The reference price is $608.89. The script hunts for collars expiring in roughly 30 days (December 26).

Zero cost collar

It presents two ways to view the world:

1. The “Growth Mode” (Table 1)

The Strategy: “I’m bullish, but I’m not crazy.”

The Trade:

  • Buy: 550 Put (Protection starts at -9.7%)
  • Sell: 646 Call (Upside capped at +6.1%)
  • Net Cost: $0.39

You are essentially telling the market: “I am willing to lose the first 9.7% of my money. But in exchange, I want to participate in the rally up to $646.”

The cost is 39 cents. On a $600 stock, that is a rounding error. You have effectively constructed a customized insurance policy where you chose a high deductible in exchange for a near-zero premium.

2. The “Fortress Mode” (Table 2)

The Strategy: “I think the market is broken and I want to be paid to wait.”

The Trade:

  • Buy: 575 Put (Protection starts at -5.6%)
  • Sell: 631 Call (Upside capped at +3.6%)
  • Net Credit: $0.35

Here, the dynamic shifts. You are barely giving the stock room to breathe (capped at 3.6% upside). In exchange, you get a much tighter floor (max loss 5.6%).

But the fun part is the Net Credit. The market is literally paying you 35 cents to put this hedge on. You have engineered a trade where, if the stock goes sideways, you make money. If the stock crashes, you are safe. The only way you “lose” (in an opportunity cost sense) is if QQQ rips higher than $631 and you have to watch everyone else get rich while you sit capped at your strike price.

The Takeaway

This script isn’t trying to predict the future. It is simply asking the options market what the price of fear is.

Right now, the market is saying you can have 6% upside and 10% downside risk for essentially zero cost. That is the “price” of the structure.

Trading is usually about having an opinion on direction (up or down). But risk management is about having an opinion on structure. And this code lets you structure your risk so that—at least for the next 30 days—you know exactly what your best and worst days will look like.

Caveats

This is the clean, classroom version of a collar. In the real world, volatility moves, bid–ask spreads are annoying, and in-the-money Calls can get assigned early. A “zero-cost” collar on your screen might be a few basis points expensive once you include slippage and commissions, and a vol spike can change the economics between the day you design the trade and the day you actually get filled. The point of the code is not to promise free insurance; it is to give you a systematic way to scan what the options market is offering you today, with all those imperfections baked into the prices.

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.

#_ZERO COLLAR PROTECTION 
from polygon import RESTClient
import pandas as pd
import yfinance as yf
from datetime import datetime, timedelta
from google.colab import userdata

# --- CONFIGURATION ---
API_KEY = userdata.get('massive')
TICKERS = ["QQQ", "SPY", "VT"]
TARGET_DTES = [30, 37]
DTE_TOLERANCE = 10         # Widened to catch monthly expirations (like VT)
MIN_PROTECTION_PCT = 0.05
MAX_PROTECTION_PCT = 0.12  # Slight bump to allow finding deep protection
MAX_DEBIT_LIMIT = 0.5     # For Table 1: Max you are willing to pay

client = RESTClient(API_KEY)

def get_current_price(ticker):
    try:
        t = yf.Ticker(ticker)
        price = t.fast_info.get('last_price')
        if not price:
            price = t.history(period='1d')['Close'].iloc[-1]
        return price
    except Exception as e:
        print(f"Error fetching price for {ticker}: {e}")
        return None

def get_midpoint(opt):
    quote = getattr(opt, 'last_quote', None)
    if quote:
        bid = quote.bid
        ask = quote.ask
        if bid is not None and ask is not None and ask > 0:
            return (bid + ask) / 2.0
    if opt.day and opt.day.close is not None:
        return opt.day.close
    return 0.0

def get_closest_expiration(ticker, target_days):
    target_date = datetime.now() + timedelta(days=target_days)
    min_date = (target_date - timedelta(days=DTE_TOLERANCE)).strftime('%Y-%m-%d')
    max_date = (target_date + timedelta(days=DTE_TOLERANCE)).strftime('%Y-%m-%d')

    try:
        contracts = list(client.list_options_contracts(
            underlying_ticker=ticker,
            expiration_date_gte=min_date,
            expiration_date_lte=max_date,
            limit=1000
        ))
    except Exception as e:
        print(f"Error listing contracts: {e}")
        return None

    dates = sorted({c.expiration_date for c in contracts})
    if not dates:
        return None
    return min(dates, key=lambda d: abs(datetime.fromisoformat(d) - target_date))

def analyze_collar(ticker):
    current_price = get_current_price(ticker)
    if not current_price:
        return

    print(f"\n{'='*60}")
    print(f"ANALYZING {ticker} (Ref Price: ${current_price:.2f})")
    print(f"{'='*60}")

    for dte in TARGET_DTES:
        expiry = get_closest_expiration(ticker, dte)
        if not expiry:
            print(f"\n> Target: {dte} DTE | No expiry found in range.")
            continue

        print(f"\n> Target: {dte} DTE | Closest Expiry: {expiry}")

        try:
            chain = client.list_snapshot_options_chain(
                underlying_asset=ticker,
                params={"expiration_date": expiry}
            )
            chain = list(chain)
        except Exception as e:
            print(f"    Error fetching chain: {e}")
            continue

        puts, calls = [], []
        for opt in chain:
            price = get_midpoint(opt)
            strike = opt.details.strike_price
            ctype = opt.details.contract_type

            if price <= 0.01: continue

            if ctype == "put":
                puts.append({"strike": strike, "price": price})
            elif ctype == "call":
                calls.append({"strike": strike, "price": price})

        if not puts or not calls:
            print("    No valid options data found.")
            continue

        df_puts = pd.DataFrame(puts)
        df_calls = pd.DataFrame(calls)

        # Base Filter for Puts (Roughly 5% to 12% OTM)
        lower_strike = current_price * (1 - MAX_PROTECTION_PCT)
        upper_strike = current_price * (1 - MIN_PROTECTION_PCT)

        candidate_puts = df_puts[
            (df_puts["strike"] >= lower_strike) &
            (df_puts["strike"] <= upper_strike)
        ].sort_values(by="strike", ascending=False) # Closest to money first

        # We will collect rows for two different tables
        efficient_collars = [] # Table 1: Best Upside for < $1.00 cost
        credit_collars = []    # Table 2: Best Protection for <= $0.00 cost

        # --- PROCESS COLLARS ---
        for _, put in candidate_puts.iterrows():
            put_strike = put["strike"]
            put_cost = put["price"]
            protection_depth = (current_price - put_strike) / current_price

            # --- LOGIC FOR TABLE 1 (Efficiency / Growth) ---
            min_credit_efficient = put_cost - MAX_DEBIT_LIMIT
            efficient_calls = df_calls[
                (df_calls["strike"] > current_price) &
                (df_calls["price"] >= min_credit_efficient)
            ].sort_values(by="strike", ascending=False)

            if not efficient_calls.empty:
                best_call = efficient_calls.iloc[0]
                net_debit = put_cost - best_call["price"]
                upside = (best_call["strike"] - current_price) / current_price
                # Score: Upside penalized slightly by cost
                score = upside - (0.01 * max(net_debit, 0))

                efficient_collars.append({
                    "Put Strike": put_strike,
                    "Protection": f"{protection_depth:.1%}",
                    "Call Strike": best_call["strike"],
                    "Upside Cap": f"{upside:.1%}",
                    "Net Cost": f"${net_debit:.2f}",
                    "Score": score
                })

            # --- LOGIC FOR TABLE 2 (Best Protection / Credit) ---
            # Here we want calls that pay for the put entirely (Price >= Put Cost)
            credit_calls = df_calls[
                (df_calls["strike"] > current_price) &
                (df_calls["price"] >= put_cost)
            ].sort_values(by="strike", ascending=False)

            if not credit_calls.empty:
                # We pick the highest strike call that still covers the cost
                best_credit_call = credit_calls.iloc[0]
                net_cost = put_cost - best_credit_call["price"] # Should be negative or 0
                upside = (best_credit_call["strike"] - current_price) / current_price

                credit_collars.append({
                    "Put Strike": put_strike,
                    "Protection": f"{protection_depth:.1%}",
                    "Call Strike": best_credit_call["strike"],
                    "Upside Cap": f"{upside:.1%}",
                    "Net Credit": f"${abs(net_cost):.2f}", # Display as Credit
                    "Raw Put Strike": put_strike # For sorting
                })

        # --- PRINT TABLE 1: GROWTH (Sorted by Score/Upside) ---
        if efficient_collars:
            print("\n  [TABLE 1] Growth Mode (Max Cost $1.00)")
            df_eff = pd.DataFrame(efficient_collars) \
                       .sort_values(by="Score", ascending=False).head(3)
            print(df_eff[["Put Strike", "Protection", "Call Strike", "Upside Cap", "Net Cost"]]
                  .to_string(index=False))
        else:
            print("\n  [TABLE 1] No growth collars found under cost limit.")

        # --- PRINT TABLE 2: FORTRESS (Sorted by Protection Tightness) ---
        if credit_collars:
            print("\n  [TABLE 2] Fortress Mode (Net Credit / Free)")
            # Sort by "Raw Put Strike" DESC (Tightest protection)
            df_cred = pd.DataFrame(credit_collars) \
                        .sort_values(by="Raw Put Strike", ascending=False).head(3)
            print(df_cred[["Put Strike", "Protection", "Call Strike", "Upside Cap", "Net Credit"]]
                  .to_string(index=False))
        else:
            print("\n  [TABLE 2] No zero-cost collars found for this range.")

# Run
for t in TICKERS:
    analyze_collar(t)

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.