Ithy Logo

Comprehensive Analysis of Your Python Options Filtering Script

Ensuring Accurate Filtering and Data Export for Stock Options

stock options trading chart

Key Takeaways

  • Accurate Filtering: Your script correctly filters call and put options within 10% of the current stock price.
  • Sorting Mechanism: Currently, the CSV is sorted by bid price instead of ask price, which requires modification.
  • Export Issue Diagnosis: The absence of exported data is likely due to an invalid API key or overly restrictive filtering criteria.

1. Filtering Call and Put Options Within 10% of Current Share Price

Your Python script is designed to filter both call and put options whose strike prices are within ±10% of the current stock price. This ensures that only relevant option contracts are considered for further analysis. Here's a breakdown of how this is achieved:

a. Implementation of the 10% Filter

The script utilizes the following condition to enforce the 10% strike price range:

if abs(stock_price - strike_price) / stock_price > PRICE_WITHIN_PERCENT:
    continue
    

With PRICE_WITHIN_PERCENT set to 0.1, this condition effectively includes only those options where the strike price is within 10% above or below the current stock price. For instance, if stock_price = $1.00, only strike prices between $0.90 and $1.10 are considered.

b. Coverage of Both Call and Put Options

The script processes both 'calls' and 'puts' within each option chain, ensuring comprehensive coverage of available options that meet the specified criteria. This dual processing ensures that both bullish and bearish strategies are accommodated.

2. Sorting the CSV by Ask Price

While your intention is to sort the CSV output by the ask price, the current implementation sorts the data by the bid price. This discrepancy needs to be addressed to align the output with your requirements.

a. Current Sorting Mechanism

The existing sorting logic is as follows:

for option in sorted(filtered_options, key=lambda x: x['bid']):
    writer.writerow(option)
    

This sorts the options in ascending order based on the bid price.

b. Modifying to Sort by Ask Price

To sort the CSV by the ask price instead, you should modify the sorting key to ask as shown below:

for option in sorted(filtered_options, key=lambda x: x['ask']):
    writer.writerow(option)
    

This change ensures that the options are listed from the lowest to the highest ask price, facilitating better analysis based on ask prices.

3. Addressing the "No Options Data to Export" Issue

Encountering the message "No options data to export." indicates that no options met all the filtering criteria specified in your script. Several factors could contribute to this outcome:

a. Invalid or Expired API Key

Your script uses an API key set to "cu", which is likely invalid. An invalid API key would prevent successful data retrieval from Finnhub.io. To resolve this:

  • Obtain a valid API key from Finnhub.io.
  • Replace the placeholder in your script:
API_KEY = "YOUR_VALID_API_KEY"  # Replace with your actual API key

b. Overly Restrictive Filtering Criteria

The combination of filters might be too stringent, leading to no options passing all the criteria. Key areas to review include:

  • OPEN_INTEREST_THRESHOLD set to 100: If few options have open interest above this threshold, they will be excluded.
  • EXPIRY_MIN_DAYS and EXPIRY_MAX_DAYS set to 25 and 120 respectively: This limits the options to those expiring within 25 to 120 days.
  • PRICE_WITHIN_PERCENT set to 0.1: Only options within 10% of the current price are considered.

To diagnose and potentially mitigate this issue:

  • Temporarily lower the OPEN_INTEREST_THRESHOLD to a smaller number (e.g., 10) to see if more options are included.
  • Expand the expiration date range to include more options.
  • Adjust the PRICE_WITHIN_PERCENT to a higher value (e.g., 0.2 for 20%) to include a broader set of strike prices.

c. API Rate Limits and Data Availability

Exceeding API rate limits can result in incomplete data retrieval. Ensure that your script respects the rate limits imposed by Finnhub.io by appropriately setting the RATE_LIMIT_DELAY.

Additionally, verify that options data is available for the tickers you're querying. Some large-cap tickers may have limited or no options data available.

d. Enhancing Debugging for Better Insights

Incorporate additional print statements to monitor the data being fetched and processed. For example:

print(f"Stock price for {ticker}: {stock_price}")
    print(f"Options data for {ticker}: {options_data}")
    

This will help identify whether the issue stems from data retrieval failures or stringent filtering.

e. Updated Code with Debugging Enhancements

Here's an updated snippet of your script incorporating the necessary corrections and debugging statements:


import requests
import csv
from datetime import datetime, timedelta
import time

# Finnhub.io API key
API_KEY = "YOUR_VALID_API_KEY"  # Replace with your actual API key
BASE_URL = "https://finnhub.io/api/v1"

# Large-cap tickers
LARGE_CAP_TICKERS = [
    "A", "AA", "AAOI", "AAL", "AAMC", "AAMRQ", "AAN", "AAPL", "AAPU", "AAXJ", "AB", "ABCB"
]

# Define filters
OPEN_INTEREST_THRESHOLD = 100
EXPIRY_MIN_DAYS = 25
EXPIRY_MAX_DAYS = 120
PRICE_WITHIN_PERCENT = 0.1  # 10%
RATE_LIMIT_DELAY = 1  # Delay between API calls to respect rate limits

def get_stock_price(ticker):
    """Get current stock price from Finnhub"""
    url = f"{BASE_URL}/quote"
    params = {
        "symbol": ticker,
        "token": API_KEY
    }

    try:
        response = requests.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        print(f"Current price for {ticker}: {data.get('c')}")
        return data.get('c')  # Current price
    except Exception as e:
        print(f"Error fetching price for {ticker}: {e}")
        return None

def fetch_options_chain(ticker):
    """Fetch options chain data from Finnhub"""
    url = f"{BASE_URL}/stock/option-chain"
    params = {
        "symbol": ticker,
        "token": API_KEY
    }

    try:
        response = requests.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        print(f"Options data for {ticker}: {data}")
        return data
    except Exception as e:
        print(f"Error fetching options chain for {ticker}: {e}")
        return None

def parse_expiry_date(date_str):
    """Parse expiry date string to datetime object"""
    try:
        # Try parsing ISO format (YYYY-MM-DD)
        return datetime.strptime(date_str, "%Y-%m-%d")
    except ValueError:
        try:
            # Try parsing other common formats
            return datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
        except ValueError:
            print(f"Unrecognized date format: {date_str}")
            return None

def fetch_and_filter_options(tickers):
    filtered_options = []
    today = datetime.now()

    for ticker in tickers:
        print(f"Fetching options for {ticker}...")

        # Get current stock price
        stock_price = get_stock_price(ticker)
        if not stock_price:
            print(f"Could not get price for {ticker}, skipping...")
            continue

        # Get options chain
        options_data = fetch_options_chain(ticker)
        if not options_data or 'data' not in options_data:
            print(f"No options data available for {ticker}")
            continue

        # Process each expiration date
        for chain in options_data.get('data', []):
            expiry_date = parse_expiry_date(chain.get('expirationDate'))
            if not expiry_date:
                continue

            # Check if expiration is within our desired range
            days_to_expiry = (expiry_date - today).days
            if not (EXPIRY_MIN_DAYS <= days_to_expiry <= EXPIRY_MAX_DAYS):
                print(f"Option for {ticker} expired in {days_to_expiry} days, excluding...")
                continue

            # Process calls and puts
            for option_type in ['calls', 'puts']:
                for option in chain.get(option_type, []):
                    try:
                        strike_price = float(option.get('strike', 0))

                        # Check if strike is within our desired range of current price
                        if abs(stock_price - strike_price) / stock_price > PRICE_WITHIN_PERCENT:
                            print(f"Strike price {strike_price} out of range for {ticker}, excluding...")
                            continue

                        # Check open interest threshold
                        if option.get('openInterest', 0) < OPEN_INTEREST_THRESHOLD:
                            print(f"Open interest {option.get('openInterest')} too low for {ticker}, excluding...")
                            continue

                        filtered_options.append({
                            "ticker": ticker,
                            "expiration_date": expiry_date.strftime("%Y-%m-%d"),
                            "strike_price": strike_price,
                            "option_type": option_type.rstrip('s'),  # 'calls' -> 'call'
                            "bid": option.get('bid', 0),
                            "ask": option.get('ask', 0),
                            "last_price": option.get('lastPrice', 0),
                            "open_interest": option.get('openInterest', 0),
                            "implied_volatility": option.get('impliedVolatility', 0),
                            "delta": option.get('delta', 0),
                            "underlying_price": stock_price
                        })

                    except Exception as e:
                        print(f"Error processing option: {e}")
                        continue

        # Respect API rate limits
        time.sleep(RATE_LIMIT_DELAY)

    return filtered_options

def export_to_csv(filtered_options):
    if not filtered_options:
        print("No options data to export.")
        return

    with open("options_research.csv", mode="w", newline="") as file:
        writer = csv.DictWriter(file, fieldnames=[
            "ticker", "expiration_date", "strike_price", "option_type",
            "bid", "ask", "last_price", "open_interest", "implied_volatility",
            "delta", "underlying_price"
        ])
        writer.writeheader()
        # Sort by ask price for easier analysis
        for option in sorted(filtered_options, key=lambda x: x['ask']):
            writer.writerow(option)

def main():
    print("Fetching and filtering options...")
    filtered_options = fetch_and_filter_options(LARGE_CAP_TICKERS)
    print(f"Found {len(filtered_options)} options matching criteria.")

    print("Exporting to CSV...")
    export_to_csv(filtered_options)
    print("Export complete: options_research.csv")

if __name__ == "__main__":
    main()
    

4. Additional Recommendations

To further enhance the reliability and functionality of your script, consider implementing the following recommendations:

a. Dynamic Configuration of Filters

Instead of hardcoding filter values like OPEN_INTEREST_THRESHOLD and PRICE_WITHIN_PERCENT, allow these to be configurable parameters. This provides flexibility to adjust the filtering criteria without modifying the codebase.

b. Enhanced Error Handling

Implement robust error handling to manage unexpected API responses or data anomalies. This includes handling cases where specific fields might be missing or contain unexpected values.

c. Logging Mechanism

Incorporate a logging system to record the script's operations, successes, and failures. This aids in troubleshooting and provides a historical record of script executions.

d. Rate Limiting Compliance

Ensure that your script adheres to the API rate limits specified by Finnhub.io. Adjust the RATE_LIMIT_DELAY as necessary to prevent throttling or temporary bans.

e. Data Validation

Before processing, validate the integrity and completeness of the fetched data. This ensures that only reliable and accurate data is used for analysis and export.


Conclusion

Your Python script effectively filters call and put options within a 10% range of the current stock price, ensuring focus on the most relevant option contracts. However, the current sorting mechanism prioritizes bid prices instead of ask prices, which can be rectified by modifying the sorting key. The issue of no data export is primarily due to an invalid API key or overly stringent filtering criteria. By addressing these areas and implementing the recommended enhancements, your script will function more reliably and provide valuable insights through the exported CSV data.


References


Last updated January 23, 2025
Ask me more