Automated Options Trading with Interactive Brokers (ib_insync) | FlashAlpha

Automated Options Trading with Interactive Brokers (ib_insync)

A complete, paper-first reference implementation: poll FlashAlpha dealer positioning and flow signals via the Python SDK and route defined-risk options orders through Interactive Brokers with ib_insync/ib_async - brackets, position caps, a daily kill switch, and the disconnect/partial-fill paths handled in ~100 lines.

T
Tomasz Dobrowolski Quant Engineer
Jul 24, 2026
9 min read
Automation InteractiveBrokers Execution Python TradingBot OptionsAPI

If you searched for automated options trading with Interactive Brokers in Python, you have probably found the two halves separately: signal content with no execution, and ib_insync tutorials that trade on moving-average crosses. What follows is the joined-up version for options: real dealer-positioning inputs on one side, real order mechanics on the other.

~100
Lines for a complete guarded executor
60 s
Poll cadence - minute-scale signals, not HFT
7497
The paper port. Start here, stay here a while

Architecture

[FlashAlpha API] ->(poll 60s)-> [decide: regime + signal rules] ->(qualified contract)-> [IBKR via ib_insync] ->(fills, brackets)-> [state + kill switch]

Design choices worth stating: this is minute-scale signal trading, not latency-sensitive execution - a 60-second poll against cached-tier analytics is the right cadence, and colocation buys you nothing here (see the latency tax). All strategy state lives in your process; IBKR is the executor, FlashAlpha is the sensor.

Prerequisites

  • An IBKR account with a paper login, and TWS or IB Gateway running with API connections enabled. Paper connects on port 7497 (live is 7496/4001 - do not touch live until the paper log says you should).
  • pip install flashalpha ib_async - ib_async is the community-maintained continuation of the ib_insync library - same API, one import rename away (ib_insync to ib_async).
  • A FlashAlpha key: full-chain GEX is Growth tier; the scored flow-signals feed used below is Alpha, which also unlocks historical replay to validate the rule before automating it (see the backtesting guide).

The sensor side, verified

Both reads used below, exactly as the SDK returns them (run today - at the time of writing SPY sat in a short-gamma regime, net GEX -13.5B, flip at 746.17 vs spot 738.55):

from flashalpha import FlashAlpha

fa = FlashAlpha(api_key=API_KEY)

gex = fa.gex("SPY")
# {'symbol': 'SPY', 'underlying_price': 738.545, 'as_of': ..., 'gamma_flip': 746.165,
#  'net_gex': -13463206856.07, 'net_gex_label': ..., 'strikes': [...]}

sigs = fa.flow_signals("SPY")
# {'symbol': 'SPY', 'as_of': ..., 'window_minutes': 240, 'underlying_price': ...,
#  'chain': {...}, 'count': N, 'signals': [{'signal': {'score': 83, 'intent': 1,
#  'tags': ['sweep','opening','golden'], ...}, 'enrichment': {...}}, ...]}

Intent is the scorer's classification (1 = bullish, 2 = bearish); score is 0-100. Whether any given rule on these fields carries edge is YOUR research problem - we have published honest nulls on naive versions (the flow-signal study) - this article is about executing whatever rule survived your validation.

The reference implementation

Download the full script (single file, ~120 lines with comments) or read it below.

"""FlashAlpha -> IBKR options executor. PAPER FIRST (port 7497)."""
import time
from dataclasses import dataclass, field
from datetime import date, datetime

from flashalpha import FlashAlpha
from ib_async import IB, Option, Stock

API_KEY = "YOUR_FLASHALPHA_KEY"
SYMBOL, POLL_S = "SPY", 60
MAX_OPEN, DAILY_LOSS_CAP = 1, -500.0     # concurrent entry order groups, USD
SCORE_MIN = 80

@dataclass
class State:
    open_orders: int = 0
    day: date = field(default_factory=date.today)
    day_pnl: float = 0.0
    killed: bool = False

def decide(fa: FlashAlpha) -> str | None:
    """Return 'C' (buy call) / 'P' (buy put) / None. Replace with YOUR validated rule."""
    gex = fa.gex(SYMBOL)
    sigs = fa.flow_signals(SYMBOL)
    hc = [s["signal"] for s in sigs.get("signals", [])
          if s["signal"]["score"] >= SCORE_MIN and "opening" in s["signal"].get("tags", [])]
    if gex.get("net_gex", 0) < 0:          # short-gamma regime: moves extend
        if any(s["intent"] == 1 for s in hc):
            return "C"
        if any(s["intent"] == 2 for s in hc):
            return "P"
    return None

def pick_contract(ib: IB, right: str, spot: float):
    """Nearest weekly expiry >= 7 DTE, strike nearest spot, SMART-routed."""
    chains = ib.reqSecDefOptParams(SYMBOL, "", "STK", ib.qualifyContracts(
        Stock(SYMBOL, "SMART", "USD"))[0].conId)
    chain = next(c for c in chains if c.exchange == "SMART")
    expiry = sorted(e for e in chain.expirations
                    if (datetime.strptime(e, "%Y%m%d").date() - date.today()).days >= 7)[0]
    strike = min(chain.strikes, key=lambda k: abs(k - spot))
    opt = Option(SYMBOL, expiry, strike, right, "SMART", currency="USD")
    ib.qualifyContracts(opt)
    return opt

def place_bracket(ib: IB, opt, qty: int):
    [tkr] = ib.reqTickers(opt)
    mid = (tkr.bid + tkr.ask) / 2 if tkr.bid and tkr.ask else tkr.marketPrice()
    entry = round(mid, 2)                              # alpha=0: improve to taste
    bracket = ib.bracketOrder("BUY", qty, limitPrice=entry,
                              takeProfitPrice=round(entry * 1.5, 2),
                              stopLossPrice=round(entry * 0.5, 2))
    for o in bracket:
        ib.placeOrder(opt, o)
    return bracket

def main():
    fa = FlashAlpha(api_key=API_KEY)
    ib = IB()
    ib.connect("127.0.0.1", 7497, clientId=17)         # PAPER port
    st = State()

    def on_exec(trade, fill):
        print(f"FILL {fill.contract.localSymbol} {fill.execution.shares} @ {fill.execution.price}")
    ib.execDetailsEvent += on_exec

    def on_commission(trade, fill, report):
        pnl = report.realizedPNL
        if pnl and abs(pnl) < 1e300:       # IB's unset sentinel is DBL_MAX - filter it
            st.day_pnl += pnl
    ib.commissionReportEvent += on_commission

    while True:
        try:
            if date.today() != st.day:                 # new session: reset caps
                st = State()
            if st.day_pnl <= DAILY_LOSS_CAP and not st.killed:
                st.killed = True
                for t in ib.openTrades():
                    if t.order.parentId == 0:      # cancel ENTRY orders only -
                        ib.cancelOrder(t.order)    # bracket TP/stops stay working
                print("KILL SWITCH: daily loss cap hit; entries cancelled, going flat-only")
            open_parents = sum(1 for t in ib.openTrades() if t.order.parentId == 0)
            if not st.killed and open_parents < MAX_OPEN:
                right = decide(fa)
                if right:
                    spot = fa.gex(SYMBOL)["underlying_price"]
                    opt = pick_contract(ib, right, spot)
                    place_bracket(ib, opt, qty=1)
                    print(f"ORDER {opt.localSymbol} bracket placed")
        except ConnectionError:
            print("IBKR disconnected; retrying in 30s")
            ib.disconnect(); time.sleep(30)
            ib.connect("127.0.0.1", 7497, clientId=17)
        except Exception as e:
            print("loop error (continuing):", e)
        ib.sleep(POLL_S)                                # keeps the event loop serviced

if __name__ == "__main__":
    main()

The parts that make it a system

  • Bracket on entry, always. The take-profit and stop ride server-side at IBKR, so a crashed script does not leave a naked position unattended.
  • Position cap before signal check - the cheapest risk control is refusing to look for trades while one is on.
  • Daily kill switch cancels working ENTRY orders (parents only, parentId == 0) and stops new ones; the bracket TP/stops of filled positions keep working at IBKR. Decide deliberately if you want flatten-on-kill instead.
  • Reconnect path: TWS restarts nightly; the loop must survive it. ib.sleep() (never time.sleep() alone) keeps ib_async's event loop breathing.
  • Partial fills: bracket children are linked server-side to their parent at IBKR; verify the scaling behavior on your paper account before trusting it at size - and it is still one more reason to prefer brackets over hand-managed exits.
  • Entry price is mid here (alpha = 0) - deliberately optimistic so the fill discipline is a conscious choice. Before believing any backtest of the same rule, price fills honestly: the fill-model reference.

Go-live checklist

  1. Run on paper (7497) through at least one full expiry cycle including an FOMC or CPI session.
  2. Read every fill in the TWS log against your script log - order-state surprises show up here first.
  3. Validate the RULE separately from the plumbing via historical replay - same SDK, base-URL swap.
  4. Size for the strategy's worst replayed week, not its average.
  5. Only then move the port.

Educational reference code, not investment advice. Options can lose more than premium when strategies go beyond defined-risk longs; automation multiplies both discipline and mistakes.

The distance from "I have a signal" to "orders execute unattended and nothing catastrophic can happen" is exactly the ~100 lines above: a verified sensor read, an explicit rule slot, contract qualification, brackets, caps, a kill switch, and a reconnect path. The FlashAlpha side is two SDK calls; the IBKR side is standard ib_async; the discipline is the product. Related: automated commentary bots, the QuantConnect LEAN bridge if you would rather live inside a backtest engine, and Kelly-based sizing for the caps. The signals side needs a Growth key for GEX, Alpha for the scored flow feed.

Live Market Pulse

Get fast visibility into market shifts with full-chain analytics over low-latency REST and MCP polling.

Intelligent Screening

Screen millions of option pairs per second using your custom EV rules, filters, and setups.

Export-Ready

Export structured signals to your own execution stack or broker integration - FlashAlpha delivers the analytics, you keep control of order routing.

Join the Community

Discord

Engage in real time conversations with us!

Twitter / X

Follow us for real-time updates and insights!

GitHub

Explore our open-source SDK, examples, and analytics resources!