Academy/Strategy Code Breakdown/Bull Call Spread — Real-World Execution
Strategy Code BreakdownLesson 11

Bull Call Spread — Real-World Execution

Learn how to execute a bull call spread with actual options chain data: strike selection, bid-ask slippage, executability assessment, and practical order entry.

16 minute read
5 key takeaways

Bull Call Spread — Real-World Execution

The CRR pricing model is mathematically beautiful, but theory meets reality when you hit the "buy" button. Real options chains have discrete strikes, wide bid-ask spreads, and liquidity that varies by strike. This lesson teaches you how to go from "I want a 30 DTE ATM/5% OTM spread" to "I just filled both orders at $2.65 net debit." We'll cover strike selection, slippage calculations, executability scoring, and practical order sequencing.

Step 1 — Understanding Bid-Ask Slippage

When you trade options, you never fill at the theoretical "mid" price. Instead:

  • You BUY your long call AT THE ASK (paying what sellers want)
  • You SELL your short call AT THE BID (getting what buyers will pay)
  • This gap between mid and your actual fills is slippage.
typescript
// Example: SPY $450 ATM call, $455 5% OTM call (both 30 DTE)
const longLeg = {
  strike: 450,
  bid: 4.50,
  mid: 4.65,    // theoretical mid-market price
  ask: 4.80,    // what you'll PAY to buy
  volume: 100,
  openInterest: 500,
};

const shortLeg = {
  strike: 455,
  bid: 2.00,    // what you'll RECEIVE to sell
  mid: 2.15,
  ask: 2.30,
  volume: 150,
  openInterest: 600,
};

// Theory: pay $2.50 per share (4.65 - 2.15)
const theoreticalDebit = (longLeg.mid - shortLeg.mid) * 100; // $250 per contract

// Reality: pay $2.80 per share (4.80 - 2.00)
const realisticDebit = (longLeg.ask - shortLeg.bid) * 100;   // $280 per contract

// Slippage cost
const slippage = realisticDebit - theoreticalDebit;            // $30 per contract = 12%
MetricLong CallShort CallImpact
Your FillAsk $4.80Bid $2.00Pay worst-case
TheoreticalMid $4.65Mid $2.15Never happens in practice
Your Cost+$0.15 vs mid−$0.15 vs mid+$0.30 total slippage
As % of Spread(0.30 / $10) × 1003% of spread width

Step 2 — Filter Chain by Liquidity

Not all strikes are equally liquid. Illiquid strikes have wide bid-ask spreads that will destroy your P&L. Always filter BEFORE selecting strikes.

typescript
import { filterByLiquidity, ChainOption } from 'bull-call-spread-strategy';

const chain: ChainOption[] = await getOptionsChain('AAPL', '2026-05-15');

// Minimum thresholds for liquid strikes
const liquid = filterByLiquidity(chain, minVolume=50, minOI=500);

console.log(`Raw chain: ${chain.length} strikes`);
console.log(`Liquid strikes: ${liquid.length} strikes`);

// Now select strikes from the liquid subset, not the full chain
const strikes = [...new Set(liquid.map(o => o.strike))].sort((a, b) => a - b);
const atm = selectStrike(spot * 1.0, strikes);        // find $450 (or closest)
const otm = selectStrike(spot * 1.05, strikes);       // find $472.50 (or closest)
python
from bull_call_spread_strategy import filter_by_liquidity, select_strike

chain = await get_options_chain('AAPL', '2026-05-15')  # e.g., fetch from broker API

# Minimum thresholds for liquid strikes
liquid = filter_by_liquidity(chain, min_volume=50, min_open_interest=500)

print(f'Raw chain: {len(chain)} strikes')
print(f'Liquid strikes: {len(liquid)} strikes')

# Now select strikes from the liquid subset, not the full chain
strikes = sorted(set(o.strike for o in liquid))
atm = select_strike(spot * 1.0, strikes)        # find $450 (or closest)
otm = select_strike(spot * 1.05, strikes)       # find $472.50 (or closest)

Liquidity Matters More Than Theory

A theoretically perfect strategy (2:1 risk/reward) with 1% bid-ask slippage is worse than a mediocre strategy (1.5:1 R/R) with 0.2% slippage. Wide spreads erode edge faster than anything else. If volume < 10 on either leg, skip the trade and wait.

Step 3 — Assess Executability

typescript
import { analyzeSpreadFromChain, assessExecutability } from 'bull-call-spread-strategy';

const config = {
  lowStrikeOffset: 0.0,    // ATM long
  highStrikeOffset: 0.05,  // 5% OTM short
  daysToExpiry: 30,
  contracts: 1,
};

const analysis = analyzeSpreadFromChain(spot, liquidChain, config);
const assessment = assessExecutability(analysis);

// Executability Score: 0–100
// 100 = Maximum liquidity (execute immediately)
//  70 = Safe to trade (execute with limit orders)
//   0 = Too illiquid (skip this trade)

console.log(`Score: ${assessment.score}/100`);
console.log(`Execute Now: ${assessment.executeNow ? '✅' : '❌'}`);

if (!assessment.executeNow) {
  console.warn('Warnings:');
  assessment.warnings.forEach(w => console.warn(`  - ${w}`));
  return; // skip this trade
}
python
from bull_call_spread_strategy import (
    BullCallSpreadStrategy, analyze_spread_from_chain, assess_executability
)

config = BullCallSpreadStrategy(
    low_strike_offset=0.0,      # ATM long
    high_strike_offset=0.05,    # 5% OTM short
    days_to_expiry=30,
    contracts=1,
)

analysis = analyze_spread_from_chain(spot, liquid_chain, config)
assessment = assess_executability(analysis)

# Executability Score: 0–100
# 100 = Maximum liquidity (execute immediately)
#  70 = Safe to trade (execute with limit orders)
#   0 = Too illiquid (skip this trade)

print(f'Score: {assessment.score}/100')
print(f'Execute Now: {"✅" if assessment.execute_now else "❌"}')

if not assessment.execute_now:
    print('Warnings:')
    for w in assessment.warnings:
        print(f'  - {w}')
    return  # skip this trade
Score RangeAssessmentAction
90–100Excellent liquidityUse market orders during peak hours
70–89Good liquidityUse limit orders, likely to fill same day
50–69Mediocre liquidityUse tight limit orders or skip
< 50Poor liquidity❌ Do not trade this strike combination

Step 4 — Compare Theoretical vs Realistic Costs

typescript
import { compareCosts } from 'bull-call-spread-strategy';

const costs = compareCosts(analysis);

console.log(`
Theoretical Debit:   $${costs.theoreticalDebit.toFixed(2)}     (mid prices)
Realistic Debit:     $${costs.realisticDebit.toFixed(2)}     (ask/bid fills)
Slippage:            $${costs.slippageDollars.toFixed(2)}     ($${costs.slippagePct.toFixed(1)}%)

Max Profit Adjusted:
  Theory:   $${(analysis.maxProfit).toFixed(2)}
  Reality:  $${(analysis.maxProfit - costs.slippageDollars).toFixed(2)}   (lower due to slippage)
`);

// Example output:
// Theoretical Debit:   $250.00
// Realistic Debit:     $280.00
// Slippage:            $30.00 (12.0%)
// Max Profit Adjusted: $720.00 → $690.00
python
from bull_call_spread_strategy import compare_costs

costs = compare_costs(analysis)

print(f'''
Theoretical Debit:   ${costs.theoretical_debit:.2f}     (mid prices)
Realistic Debit:     ${costs.realistic_debit:.2f}     (ask/bid fills)
Slippage:            ${costs.slippage_dollars:.2f}     ({costs.slippage_pct:.1f}%)

Max Profit Adjusted:
   Theory:   ${analysis.max_profit:.2f}
   Reality:  ${analysis.max_profit - costs.slippage_dollars:.2f}   (lower due to slippage)
''')

# Example output:
# Theoretical Debit:   $250.00
# Realistic Debit:     $280.00
# Slippage:            $30.00 (12.0%)
# Max Profit Adjusted: $720.00 → $690.00

Step 5 — Execute the Spread

  • BUY the long call FIRST at your limit (at or better than ASK)
  • WAIT for confirmation fill (usually instant during liquid hours)
  • SELL the short call IMMEDIATELY after (at or better than BID)
  • Recommended: use a spread order if your broker supports it (executes both legs atomically)
  • Avoid market orders at market open/close—use 10 AM–3 PM ET for best fills
typescript
// Execution Example: AAPL Spread

// LEG 1: Buy the long call
BUY 1 AAPL May 190 Call @ $5.70 limit
// Your broker might fill this @ $5.65 (better)

// LEG 2: Immediately after long fills, sell the short
SELL 1 AAPL May 195 Call @ $2.10 limit
// Your broker might fill this @ $2.15 (slightly worse)

// Net Result:
// Entry Cost: ($5.65 - $2.15) × 100 = $350
// vs Theory: ($5.55 - $2.25) × 100 = $330
// Slippage: $20 (6% cost)

Never "Leg Into" Spreads During Volatility

If your long fills but your short doesn't execute the same minute, you're temporarily naked long a call. If the market gaps 2% in that 5 minutes, your "defined-risk" spread suddenly has $500 of risk. Always use spread orders when available, or leg in during the calmest market hours.

Step 6 — Calculate Greeks with Real IVs

Each strike in the options chain has its own implied volatility (IV varies by strike—this is called "volatility skew"). Use each leg's actual IV, not a flat IV assumption.

typescript
// Each leg gets its own IV from the chain
const longGreeks = crrGreeks({
  spotPrice: 190,
  strikePrice: 190,     // ATM long call
  volatility: longLeg.iv,  // 0.22 (lower IV at ATM)
  timeToExpiry: 0.082,  // 30 days
  riskFreeRate: 0.05,
});

const shortGreeks = crrGreeks({
  spotPrice: 190,
  strikePrice: 195,     // OTM short call
  volatility: shortLeg.iv, // 0.20 (even lower IV OTM)
  timeToExpiry: 0.082,
  riskFreeRate: 0.05,
});

const netGreeks = {
  delta: longGreeks.delta - shortGreeks.delta,        // ~0.28 (bullish)
  theta: longGreeks.theta - shortGreeks.theta,        // ~+$0.40/day (positive)
  vega:  longGreeks.vega - shortGreeks.vega,          // ~−$0.05 (short vega)
};

console.log(`Net Delta: ${netGreeks.delta.toFixed(2)} → bull on $1 moves`);
console.log(`Daily Theta: +$${netGreeks.theta.toFixed(2)} → profit from time`);
console.log(`Vega Exposure: ${netGreeks.vega.toFixed(2)} → hurt by IV spikes`);
python
from bull_call_spread_strategy import crr_greeks

# Each leg gets its own IV from the chain
long_greeks = crr_greeks(
    S=190, K=190,            # ATM long call
    T=0.082,                 # 30 days
    r=0.05,
    sigma=long_leg.iv,       # 0.22 (lower IV at ATM)
    option_type='call',
    steps=100
)

short_greeks = crr_greeks(
    S=190, K=195,            # OTM short call
    T=0.082,
    r=0.05,
    sigma=short_leg.iv,      # 0.20 (even lower IV OTM)
    option_type='call',
    steps=100
)

net_greeks = {
    'delta': long_greeks['delta'] - short_greeks['delta'],     # ~0.28 (bullish)
    'theta': long_greeks['theta'] - short_greeks['theta'],     # ~+$0.40/day
    'vega':  long_greeks['vega']  - short_greeks['vega'],      # ~−$0.05 (short vega)
}

print('Net Delta: {:.2f} → bull on $1 moves'.format(net_greeks["delta"]))
print('Daily Theta: +${:.2f} → profit from time'.format(net_greeks["theta"]))
print('Vega Exposure: {:.2f} → hurt by IV spikes'.format(net_greeks["vega"]))

Step 7 — Set Profit Targets & Stop Losses

Exit RuleTriggerBenefit
Profit TargetSpread value ≤ 70% of entryLock in $$ before momentum reverses
Stop LossSpread value ≥ 110% of entryProtect capital; don't let losers run
Time ExitClose at 21 DTE remainingAvoid final week gamma risk; reduce position size
typescript
// Example: AAPL spread entered at $3.50 net debit

const entryDebit = 3.50;
const maxProfit = 10.00 - entryDebit; // $10 spread width - $3.50 cost = $6.50

// Exit rules per contract:
const profitTargetValue = entryDebit - (maxProfit * 0.70);  // $3.50 - $4.55 = close @ $2.75
const stopLossValue = entryDebit * 1.10;                    // $3.85

// Daily monitoring:
if (spreadValue <= profitTargetValue) {
  closeSpread('PROFIT_TARGET__70_percent_max_reached');
} else if (spreadValue >= stopLossValue) {
  closeSpread('STOP_LOSS__110_percent_entry_cost');
} else if (daysToExpiry <= 21) {
  closeSpread('TIME_EXIT__roll_to_next_month');
}
python
# Example: AAPL spread entered at $3.50 net debit

entry_debit = 3.50
max_profit = 10.00 - entry_debit  # $10 spread width - $3.50 cost = $6.50

# Exit rules per contract:
profit_target_value = entry_debit - (max_profit * 0.70)  # $3.50 - $4.55 = close @ $2.75
stop_loss_value = entry_debit * 1.10                     # $3.85

# Daily monitoring:
if spread_value <= profit_target_value:
    close_spread('PROFIT_TARGET__70_percent_max_reached')
elif spread_value >= stop_loss_value:
    close_spread('STOP_LOSS__110_percent_entry_cost')
elif days_to_expiry <= 21:
    close_spread('TIME_EXIT__roll_to_next_month')

Full Execution Flow (Real Example)

Here's a complete workflow from market data to order entry:

  • Fetch options chain for AAPL May monthly expiry
  • Filter to liquid strikes only (volume > 100, OI > 1000)
  • Calculate spreads for all ATM/5% OTM combos
  • Score each candidate using assessExecutability()
  • Pick highest-scored candidate with good theta
  • Leg in: Buy long call @ ASK, then sell short call @ BID
  • Monitor daily: Mark spread to market every morning
  • Close early if profit target hit, stop loss triggered, or 21 DTE

See Full Example Code

Open the execution example in the Lab to see real AAPL chain data flow through the entire process.

Key Takeaway: Account for Slippage ALWAYS

12% slippage sounds extreme, but it's real on illiquid strikes. On liquid strikes (SPY, QQQ), slippage is often 1–3%. On small-cap stocks, it's 5–10%. Know your slippage before entering; if it exceeds 10% of your max loss, skip the trade or wait for better liquidity.

Key Takeaways
  • Use real options chain data to select strikes (filter by liquidity first)
  • Account for bid-ask slippage: buy long at ASK, sell short at BID
  • Assess executability before trading—score < 70 means skip
  • Execute long leg first, then short leg; use limit orders during volatile hours
  • Monitor daily P&L and close at 70% of max profit or 110% of entry cost

Open Bull Call Spread in Lab

Run the real execution example and see slippage in action