← Back to Blog

How to Backtest Credit Spreads Using Free Python Libraries

How to Backtest Credit Spreads Using Free Python Libraries

How to Backtest Credit Spreads Using Free Python Libraries

Backtesting is the cornerstone of developing a disciplined, evidence-based options trading strategy. For traders using credit put spreads, a defined-risk strategy, knowing how a setup would have performed historically is invaluable. While specialized backtesting platforms exist, they can be expensive and rigid. Using free Python libraries offers a powerful, flexible, and educational alternative. This guide will walk you through the process of building your own simple yet effective credit spread backtesting script.

Why Backtest Credit Spreads?

Before diving into code, it's crucial to understand the "why." A credit put spread involves selling a put option at one strike price and buying a further out-of-the-money (OTM) put, both with the same expiration. Your maximum profit is the net credit received, and your maximum loss is the difference between strikes minus the credit. Backtesting this strategy allows you to:

  • Test Assumptions: Does selling 30-delta puts 30 days out consistently work for stock XYZ?
  • Quantify Risk & Reward: Understand the real-world win rate, average profit/loss, and maximum drawdown.
  • Build Confidence: Execute trades based on historical analysis rather than emotion or guesswork.

Setting Up Your Python Environment

First, ensure you have Python installed. We'll use three core libraries, all free and installable via pip.

  • yfinance: To fetch historical stock and options price data.
  • pandas: For data manipulation and analysis.
  • numpy: For numerical calculations.

Install them by running: pip install yfinance pandas numpy in your terminal or command prompt. You can use a simple text editor or an IDE like VS Code or PyCharm to write your script.

Step 1: Fetching Historical Stock Data

Our backtest needs a source of truth: the historical price of the underlying asset. We'll use yfinance for this. Let's fetch daily data for the SPY ETF.

import yfinance as yf
import pandas as pd

# Define the ticker and period
ticker = "SPY"
data = yf.download(ticker, start="2020-01-01", end="2023-12-31")

# View the first few rows
print(data.head())

This gives us a DataFrame with Open, High, Low, Close, and Volume columns. Our primary focus for backtesting entry/exit logic will be the Adj Close price.

Step 2: Defining the Backtesting Logic

The core of a backtest is a set of rules that simulates placing trades based on past data. For a credit put spread, we need to define key parameters.

Strategy Parameters:

  • Expiration Cycle: How many days until expiration? (e.g., 30-45 days out is common).
  • Delta Target: What delta should the short put have? (e.g., 0.30, or 30-delta).
  • Spread Width: The distance between strikes (e.g., $5 for SPY).
  • Exit Rule: When to close the trade? Common rules are: at expiration, at 50% of max profit, or at a defined loss (e.g., 2x credit received).

Since getting true historical options pricing with Greeks is complex, we will simulate it. A reasonable approximation for backtesting is to use the stock's price and implied volatility to estimate if a put was OTM by a certain percentage.

Step 3: Simulating Trades with a Rolling Window

We'll simulate opening a new credit put spread every month (approx. 21 trading days). Our script will loop through the historical data, check if our entry conditions are met, and then track the trade's outcome based on the exit rule.

# Initialize lists to store trade results
trade_results = []
current_trade = None
entry_date = None
entry_price = None
short_strike = None
long_strike = None
credit_received = 2.50 # Simulated net credit for example
spread_width = 5

# Simple loop example (conceptual)
for i in range(len(data)):
current_date = data.index[i]
current_close = data['Adj Close'].iloc[i]

# ENTRY: Every 21 bars and if price is above 200-day SMA (example condition)
if i % 21 == 0 and current_close > data['Adj Close'].rolling(200).mean().iloc[i]:
short_strike = round(current_close * 0.97) # Simulate a ~30-delta short put (3% OTM)
long_strike = short_strike - spread_width
current_trade = 'open'
entry_date = current_date
entry_price = current_close

# EXIT Logic for an open trade
if current_trade == 'open':
days_in_trade = (current_date - entry_date).days
# Rule 1: Exit at 50% Max Profit
profit_target = credit_received * 0.50
# Rule 2: Exit at Expiration (simulated as 30 days)
if days_in_trade >= 30:
# P&L at expiration: If stock price > short_strike, keep full credit.
# If between strikes, calculate loss.
if current_close >= short_strike:
pnl = credit_received
elif current_close <= long_strike:
pnl = - (spread_width - credit_received) # Max loss
else:
pnl = (short_strike - current_close) - credit_received
trade_results.append({'entry': entry_date, 'exit': current_date, 'pnl': pnl})
current_trade = None

Step 4: Analyzing the Backtest Results

Once the simulated loop is complete, we analyze the trade_results list using pandas to generate key performance metrics.

results_df = pd.DataFrame(trade_results)

# Calculate key metrics
total_trades = len(results_df)
winning_trades = results_df[results_df['pnl'] > 0]
losing_trades = results_df[results_df['pnl'] <= 0]

win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
avg_win = winning_trades['pnl'].mean() if len(winning_trades) > 0 else 0
avg_loss = losing_trades['pnl'].mean() if len(losing_trades) > 0 else 0
profit_factor = abs(winning_trades['pnl'].sum() / losing_trades['pnl'].sum()) if losing_trades['pnl'].sum() != 0 else None
total_net_profit = results_df['pnl'].sum()

print(f"Total Trades: {total_trades}")
print(f"Win Rate: {win_rate:.2f}%")
print(f"Average Win: ${avg_win:.2f}")
print(f"Average Loss: ${avg_loss:.2f}")
print(f"Profit Factor: {profit_factor:.2f}" if profit_factor else "Profit Factor: Undefined (No Losses)")
print(f"Total Net Profit: ${total_net_profit:.2f}")

These metrics give you a quantitative view of your strategy's historical performance. A robust strategy should have a positive expectancy (Avg Win * Win Rate > |Avg Loss| * Loss Rate).

Limitations and Next Steps

It's vital to understand the limitations of this simplified backtest:

  • Simulated Options Pricing: We estimated strikes and credits. For greater accuracy, you would need a historical options database, which is harder to find for free.
  • Slippage & Commissions: Real trading includes fees and bid-ask spreads, which eat into profits.
  • Look-Ahead Bias: Ensure your entry logic uses only data available at the time of the trade.

To improve your model, consider:

  1. Incorporating implied volatility data (available via yfinance for recent history).
  2. Adding more sophisticated exit rules (e.g., rolling the spread, volatility-based exits).
  3. Testing on multiple underlyings to check for robustness.

Conclusion: Empowering Your Trading Decisions

Building your own backtesting script with Python demystifies the process and puts you in complete control. While this guide provides a foundational framework, it’s a starting point. You can expand it to test different delta targets, expiration cycles, and underlying assets. The goal isn't to find a "holy grail" but to understand the statistical profile of your chosen strategy. By learning to backtest, you move from trading on hope to trading with a quantified, historical edge. Remember, past performance does not guarantee future results, but informed preparation is the best tool any trader can have.