Gamma scalping trading bot in Python

An S&P 500 straddle bought on an Interactive Brokers paper account and delta hedged with Micro E-mini futures, live for one afternoon. Straddle, gamma, theta and the hedge band, with charts, code and the real fills.

Full code on GitHub Open the live dashboard

Engine, greeks, hedge band and an offline test with replayed days are in the repository. Behind the dashboard button sits the desk from the reel: a simulated session that starts from the bot's entry on 2 Nov, where every variable can be dragged and every hedge shows up on the chart.

S&P 500 gamma scalping desk: net P&L, theta paid against earnings from moves, straddle payoff, net delta inside the band and every MES hedge on the S&P chart

Open the gamma scalping desk: shock the S&P by 1%, change the band or the implied vol, and watch theta and gamma trade places.

What the bot does

Gamma scalping buys a position that gains from movement, then trades the underlying to keep its delta near zero. Here that position is one at the money SPX straddle (a call and a put on the same strike and expiry), and the hedge is Micro E-mini S&P 500 futures (MES). Each time the index moves far enough for delta to reach 1 MES, the bot trades 1 MES back: it sells after a rally and buys after a drop. Every round trip locks in a small gain, and those gains have to pay the option's time decay.

Paper account only. No real money has gone through it, and research on buying these straddles is not encouraging: Coval and Shumway (2001) find at the money S&P 500 straddles lose about 3% a week on average, because implied vol usually sits above the vol that follows. Read the limitations before using any number here.

Watch the bot trade live

Fifteen minutes: the straddle, gamma, theta and the hedge band, then this engine running on a paper account for an afternoon, and the P&L taken apart leg by leg. Subtitles in English, Portuguese and Spanish.

Gamma Scalping Trading Bot in Python: Live on Interactive Brokers

On X

Follow me on X, my models are there

I post the models as I build them, the changes I make to them, and the results that did not work. Shorter than these write ups, and there are a lot more of them.

Follow me on X
Newsletter

Get exclusive models, prompts and code

One short email when there's something new. finance models, code walkthroughs, agentic prompts. No spam.

Built with
Anthropic Claude Python R ChatGPT GitHub Jupyter Excel PowerPoint VS Code SQL MATLAB
Full code on GitHub Open the live dashboard

Each section below takes one piece of the trade: its chart, its numbers from the 2 Nov run, and the function in gamma_core.py that computes it.

1. Buying the straddle

At 12:27 New York time on 2 Nov 2026 the bot bought the SPXW 7,615 straddle expiring 30 Nov, 28 days out: call 136.60 plus put 117.00, so 253.6 index points, or $25,360 with the $100 multiplier. Implied vol on the fill was 15.0%.

Profit or loss of the SPX 7,615 straddle against the S&P 500, at expiry and on the day it was bought
Black line: value at expiry minus the 253.6 points paid. Blue line: Black-Scholes value with 28 days left, at 15.0% implied vol.

Held to expiry, it only makes money beyond 7,361 or 7,869, a 3.3% move either way. Before expiry, though, the blue curve bends upward on both sides of the strike. That curvature is gamma, and gamma scalping collects it every day by trading futures, long before expiry.

Straddle greeks, Black-Scholes (gamma_core.py)

def straddle(s, k, t, vol, r, q, n=1):
    """1 call + 1 put; t in trading-day years, so theta is per trading day."""
    sd = vol * math.sqrt(t)
    d1 = (math.log(s / k) + (r - q + 0.5 * vol * vol) * t) / sd
    d2 = d1 - sd
    dq, dr = math.exp(-q * t), math.exp(-r * t)
    call = s * dq * N(d1) - k * dr * N(d2)
    put = k * dr * N(-d2) - s * dq * N(-d1)
    delta = dq * (N(d1) - N(-d1))                 # call delta + put delta
    gamma = 2 * dq * pdf(d1) / (s * sd)
    common = -s * dq * pdf(d1) * vol / (2 * math.sqrt(t))
    th_call = common + q * s * dq * N(d1) - r * k * dr * N(d2)
    th_put = common - q * s * dq * N(-d1) + r * k * dr * N(-d2)
    m = 100 * n                                   # SPX multiplier
    return Greeks(price=call + put, delta=delta * m, gamma=gamma * m,
                  theta_day=(th_call + th_put) / 252 * m)

2. Gamma and the hedge band

Delta is counted in MES. One MES pays $5 a point, so it equals 5 SPX deltas. With gamma at 0.25, the straddle's delta changes by 0.25 for every point the index moves, so 20 points move it by 5 SPX deltas: exactly one MES. At entry the straddle sat at +1.09 MES (a call slightly above 0.5 delta), and the first order sold 1 MES to bring it back near zero.

Straddle delta in MES against the S&P 500, with a dot every time it crosses a whole MES
Straddle delta in MES at entry, 28 days to expiry. Each dot is a hedge: above the entry the bot sells another MES, below it buys one.

Hedging on every tick would pay commissions all day; hedging rarely lets delta run. Whalley and Wilmott (1997) give the band that balances hedging cost against hedging error, and for this straddle it lands at about one contract. So the rule is simple: trade only when net delta leaves plus or minus 1 MES, then trade back to the nearest whole contract.

Net delta, the band rule and the next trigger levels

MES_MULT = 5.0   # 1 MES = $5 a point = 5 SPX deltas

def net_delta(straddle_delta, mes_position):
    """Dollars per point: options plus the MES hedge."""
    return straddle_delta + MES_MULT * mes_position

def hedge_order(net, band_mes=1.0):
    """MES to trade (+ buy, - sell) once net delta leaves the band."""
    if abs(net) < band_mes * MES_MULT:
        return 0
    return -int(round(net / MES_MULT))

def next_levels(s, net, gamma, band_mes=1.0):
    """S&P levels where net delta reaches -band and +band."""
    edge = band_mes * MES_MULT
    return s + (-edge - net) / gamma, s + (edge - net) / gamma

3. Theta, the rent

Owning gamma costs theta. Time alone took about $648 a trading day off this straddle. A move of size ΔS earns roughly ½ × gamma × ΔS² from the hedges, so the two cancel at one specific daily move:

½ × 0.25 × 72² ≈ $648  so the breakeven is about 72 S&P points a day
Gamma earnings against the S&P daily move, compared with $648 of theta a day, crossing at 72 points
Days that move more than 72 points pay the rent; quieter days do not. Shaded: where gamma wins.

Carr and Madan (1998) put the same idea in vol terms: a delta hedged option earns when realized vol beats the implied vol paid for it. That is why the bot checks the Goyal and Saretto filter first. On 2 Nov realized vol was only 0.79 of implied, so the filter said no, and the run went ahead with --demo to show the mechanics.

Breakeven move and realized vol

def breakeven_move(g):
    """Daily index move whose gamma P&L, 1/2 gamma dS^2, pays one day of theta."""
    return math.sqrt(2 * abs(g.theta_day) / g.gamma)

def realized_vol(closes):
    """Annualised close to close volatility."""
    rets = [math.log(b / a) for a, b in zip(closes, closes[1:])]
    m = sum(rets) / len(rets)
    var = sum((x - m) ** 2 for x in rets) / (len(rets) - 1)
    return math.sqrt(var * 252)

4. One afternoon on the paper account

After the first hedge, nothing happened for 93 minutes: the S&P drifted inside the band. Both buys came between 14:00 and 14:21, when the index broke lower and delta reached minus 1 MES twice. At 15:01 the hedge was closed, and the options a few minutes later.

S&P 500 every minute on 2 Nov 2026 from 12:27 to 15:10, inside the hedge band, with the four MES trades marked
Shaded: the range where net delta stays within 1 MES. Every trade resets it, which is why it steps down at 14:00 and 14:21.
Every MES trade, 2 Nov 2026, New York time. MES trades about 31 to 32 points above the index, the carry to its December expiry.
TimeS&P 500TradeMES priceWhy
12:277,614.82sell 17,647.00first hedge, delta +1.09 MES
14:007,592.11buy 17,623.25delta reached -1 MES
14:217,571.99buy 17,602.75delta reached -1 MES again
15:017,573.97sell 17,606.75exit

Gamma barely moved all afternoon (0.250 at entry, 0.254 at the close), because at 28 days a straddle's gamma is nearly flat across a 50 point range. So every 20 points kept meaning one MES, and the band never needed resizing.


5. Where the $900 came from

Profit split by leg (call, put, MES hedges, costs) and by source (implied vol rise, scalping against theta)
Left: real fills. Right: the same total split into the implied vol move and everything else.
Paper account fills, 1 straddle and 1 MES
LegBoughtSoldP&L
Call 7,615136.60120.90-$1,570.00
Put 7,615117.00140.40+$2,340.00
MES hedges4 trades+$138.75
Commissions8 orders-$8.96
Total+$899.79

Most of the profit came from vol. As the S&P fell 37 points, implied vol rose from 15.0% to 15.6%. With vega near $1,700 per vol point, those 0.6 points were worth about $1,010. Strip that out and gamma plus the hedges against theta lost about $110: realized vol during the run was 10.9% against 15% implied, exactly the case Carr and Madan describe. One afternoon says nothing about the strategy's edge. What it shows is every moving part with real fills.


Run it

Full code in the repository. TWS or IB Gateway on a paper account with the API enabled; the offline test needs neither.

Clone and run

git clone https://github.com/davidalmeida90/GEX-scalping-tradingbot
cd GEX-scalping-tradingbot
pip install -r requirements.txt
cd gamma_scalping
python test_offline.py                    # the math and three replayed days, no TWS
python gamma_engine.py --check            # market, filter and the straddle it would buy
python gamma_engine.py                    # dry run, simulated fills
python gamma_engine.py --paper            # orders on the paper account
python gamma_engine.py --paper --demo     # trade even when the filter says no

Options: --expiry YYYYMMDD, --band 1.0 (half width in MES), --qty 1, --minutes 30, --resume. Every trade is appended to out/gamma_trades.csv.


Limitations


Papers