Θ ThetaGang Θ
ThetaGang is a low-frequency, systematic portfolio execution tool for Interactive Brokers (IBKR). Designed for long-horizon allocation and "get rich slowly" execution, it runs on a scheduled cadence (typically daily, weekly, or monthly via cron) to maintain target portfolio weights, manage hedges, and execute option or share orders within explicit risk controls.
Core capabilities:
- Stage-driven execution pipeline controlled by
run.strategiesorrun.stages - Portfolio accounting model with isolated capital bases (
net_liquidation,wheel_buying_power,regime_rebalance) - Regime-aware equity rebalancing (
regime_rebalance) with volatility targeting, absolute-trend rules, and soft/hard drift bands - Direct share rebalancing (
buy_only,sell_only, orboth) - Systematic options income and rolling (
wheel: put/call writing, target DTE/delta selection, strike caps, and net-contract calculation for spreads/PMCCs) - Long-put tail hedging (
tail_hedge) and VIX call hedging (vix_call_hedge) - Cash sweep management (
cash_management) via short-term treasury ETFs (e.g.,SGOV) - External decision providers for bounded target-weight multipliers and tail-harvest gating
- Exchange-hours enforcement, dry-run validation (
--dry-run), per-symbol order supervision, and SQLite state/execution tracking
See thetagang.toml for the full configuration reference.
Risk Disclaimer
⚠️ WARNING: Trading equities and options on margin involves substantial risk and is not suitable for all investors. Selling options and using leverage can result in losses exceeding your initial investment. Selling naked puts carries substantial downside risk if the underlying asset falls sharply. This is not free money: premiums compensate for real financial risk.
Before using ThetaGang:
- Understand that systematic strategies can lose money, especially in trending or volatile markets. The options-income approach assumes implied volatility exceeds realized volatility on average; when that fails, selling options loses.
- Maintain sufficient capital and margin cushion for worst-case drawdowns.
- Be prepared to take assignment of underlying securities at unfavorable prices when writing options.
- Test configurations thoroughly in a paper account and with
--dry-runbefore live execution.
Architecture & Strategies
ThetaGang executes an ordered pipeline of stages resolved from run.strategies (or run.stages) in thetagang.toml. Only the stages required by the enabled strategies run.
Running Multiple Strategies Safely
ThetaGang operates on account-level state: buying power, position targeting, rebalancing bands, option rolls, cash sweeps, and hedges all inspect the same account. To run materially different strategies without cross-strategy interference, isolate each strategy in its own IBKR sub-account with dedicated capital.
Regime-Aware Rebalancing (regime_rebalance)
Rebalances equity positions across configured symbols when allocations drift beyond relative bands around target weights:
- Soft band: Gated by a choppiness/efficiency regime filter and a trading-session cooldown (tracked via
tg:regime-rebalanceorder tags). Intended to harvest variance in choppy, mean-reverting markets. - Hard band: Safety rail that bypasses the regime filter and cooldown when drift exceeds the hard threshold, optionally rebalancing partway back to target (
hard_band_rebalance_fraction). - Flow & deficit rails: Deploy inferred unallocated capacity or trim excess exposure when the rebalance base shifts. The flow rail infers capacity from base vs. sleeve value, not from broker deposits/withdrawals.
- Target modifiers: Optional per-symbol volatility targeting (
volatility_weight),absolute_trendrisk-off rules, and bounded external decision providers.
regime_rebalance enables only its equity-rebalance stage; it does not enable wheel option stages. (The legacy strategies.regime_rebalance.shares_only setting is deprecated and ignored.) Assumes a daily cadence; running more frequently than daily is not recommended.
[run]
strategies = ["regime_rebalance"]
[strategies.regime_rebalance]
enabled = true
symbols = ["QQQ", "BTAL"]
lookback_days = 40
soft_band = 0.25 # +/-25% relative drift from target weight
hard_band = 0.50 # +/-50% relative drift from target weight
hard_band_rebalance_fraction = 1.0 # 1.0 = full to target, 0.5 = halfway
cooldown_days = 5
choppiness_min = 3.0
efficiency_max = 0.30
order_history_lookback_days = 30
[strategies.regime_rebalance.risk]
margin_usage = 0.8 # Optional override of runtime.account.margin_usage
Direct Share Rebalancing
When running wheel, symbols can be rebalanced directly with shares instead of—or alongside—options when premiums or liquidity are insufficient. Configure the v2 policy (the legacy buy_only_rebalancing symbol flags alone do not enable this; the v2 default mode is off):
[strategies.wheel.equity_rebalance.symbol_overrides.AAPL]
mode = "buy_only" # buy_only | sell_only | both
min_threshold_shares = 10 # Minimum shares to buy
Options Income & The Wheel (wheel)
The wheel strategy acquires and manages target symbol allocations by selling puts instead of buying shares outright, then writing calls on assigned shares:
- Put writing: When a symbol is below its target allocation and conditions pass (buying power, DTE, delta, green/red day filters, minimum open interest), ThetaGang writes puts.
- Rolling: Open short options are rolled when P&L reaches
roll_when.pnlor DTE falls toroll_when.dte(subject tomin_pnl,max_dte, and credit/strike constraints). By default, ITM puts are left to expire and take assignment (roll_when.puts.itm = false), while ITM calls roll out to avoid assignment (roll_when.calls.itm = true). Rolled put strikes are capped based on the prior strike, premium received, and buyback cost. - Call writing: Once shares are held, ThetaGang writes covered calls at or above the higher of average cost basis and any configured
strike_limit(plus high-water-mark ratcheting when enabled). Upside can be left uncapped on a fraction of holdings usingwrite_when.calls.cap_factor,cap_target_floor, orexcess_only = true. - Spreads & PMCCs: Setting
write_when.calculate_net_contracts = trueaccounts for user-managed long option legs (e.g., LEAPs in PMCCs, calendars, or ZEBRAs) when sizing short legs. ThetaGang does not open, roll, or guarantee coverage from the long legs: manage expirations yourself to avoid naked short exposure.
Long-Put Tail Hedging (tail_hedge)
Maintains a rolling ladder of long out-of-the-money puts within a fixed annual cost budget (annual_budget), ranking candidate contracts near target_dte (default 180 DTE) by catastrophe-payout multiple. Entries are sliced across the year (entries_per_year); missed entries are not made up. Harvests trim profitable puts toward harvest_target_weight once the sleeve exceeds harvest_trigger_weight of the regime base, requiring a same-symbol hard-underweight rebalance buy. Requires persistent SQLite storage. See Long-put tail hedging.
VIX Call Hedging (vix_call_hedge)
Purchases monthly VIX calls scaled to VIXMO regimes based on the Cboe VIX Tail Hedge Index (VXTH) methodology, and automatically closes hedges when spot VIX exceeds close_hedges_when_vix_exceeds.
[strategies.vix_call_hedge]
enabled = true
close_hedges_when_vix_exceeds = 50.0 # Auto-close at high VIX
ignore_dte = 5 # Ignore positions expiring within 5 days
See thetagang.toml for tiered VIXMO allocation tables.
Cash Management (cash_management)
Sweeps cash above target_cash_balance + buy_threshold into a short-term treasury ETF (default SGOV) and sells back toward target when projected cash falls below target_cash_balance - sell_threshold (dollar amounts), using VWAP orders by default.
[strategies.cash_management]
enabled = true
cash_fund = "SGOV" # Default short-term treasury ETF
target_cash_balance = 0
buy_threshold = 10000 # Buy fund when cash exceeds target by this much ($)
sell_threshold = 10000 # Sell fund when cash falls below target by this much ($)
[strategies.cash_management.orders.algo]
strategy = "Vwap" # Use VWAP for cash fund orders
See thetagang.toml for full options.
Execution & Risk Controls
Exchange Hours Management
Gate execution on market hours (via exchange_calendars ISO codes):
[runtime.exchange_hours]
exchange = "XNYS" # NYSE by default
action_when_closed = "wait" # Options: "wait", "exit", "continue"
delay_after_open = 1800 # Wait 30 minutes after market open
delay_before_close = 1800 # Stop 30 minutes before market close
max_wait_until_open = 3600 # Max wait time if market is closed
Strike Price Limits
Cap put strikes (upper bound) and floor call strikes (lower bound):
[portfolio.symbols.SPY.puts]
strike_limit = 400 # Don't write puts above $400
[portfolio.symbols.SPY.calls]
strike_limit = 450 # Don't write calls below $450
Write Threshold Sigma
Gate option writes on daily log-return standard deviations instead of fixed percentages (overrides write_threshold):
[strategies.wheel.defaults.constants]
write_threshold_sigma = 1.0 # Write when 1 standard deviation from current price
[portfolio.symbols.QQQ.puts]
write_threshold_sigma = 1.5 # More conservative for this symbol
Rolling Controls
Maintain High Water Mark
Prevent rolling calls to lower strikes:
[strategies.wheel.defaults.roll_when.calls]
maintain_high_water_mark = true
Close If Unable to Roll
Close profitable short positions when no qualifying roll contract is found (requires roll_when.max_dte set, with the position DTE at or below it):
[strategies.wheel.defaults.roll_when]
close_if_unable_to_roll = true
max_dte = 180 # Required: close only applies when DTE <= max_dte
Order Management
Limit-order repricing is opt-in per symbol. Either set adjust_price_after_delay = true (one legacy midpoint-averaging adjustment after a random delay within runtime.orders.price_update_delay) or configure a per-symbol execution policy with fill_timeout for supervised repricing at the configured bid/ask/mid quote. Otherwise unfinished DAY limit orders stay working at the broker.
Per-Symbol Execution Policies
Per-symbol repricing and fallback behavior is opt-in. Without an execution
table, ThetaGang leaves unfinished DAY limit orders working at the broker, but
the final order check emits an ALERT and exits non-zero whenever any submitted
order is rejected or not fully filled.
[portfolio.symbols.SPY.execution]
buy_price = "ask" # bid | ask | mid
sell_price = "bid" # bid | ask | mid
fill_timeout = 300 # Supervise this order for five minutes
on_timeout = "marketable_limit" # leave_open | cancel | marketable_limit | market
on_inactive = "marketable_limit" # Same actions when the broker rejects the order (defaults to on_timeout)
final_wait = 30 # Wait 30 seconds for a replacement to fill
The configured quote is used for initial submission and, when fill_timeout
enables supervision, subsequent repricing. Using the ask for buys and the bid for
sells is the aggressive, marketable configuration. At the timeout, a
marketable_limit replacement retains price protection by using the current ask
or bid. A raw market fallback is explicitly opt-in and is not supported for
combo orders. ThetaGang confirms cancellation of the original order and replaces
only its unfilled quantity; if cancellation or fill state is ambiguous, no
replacement is submitted. A replacement that has not filled after final_wait
seconds stays working at the broker, and the final order check reports it.
Tail-hedge orders retain their separate execution safeguards. Configured limit
prices also preserve the sign of combo orders and the minimum_credit floor, so
a credit order cannot be silently converted into a debit.
When the broker rejects an order, IBKR either reports the terminal Inactive
status or delivers a rejection error (code 201) that arrives as Cancelled;
ThetaGang logs the reason and records it as an order_error event. An unmatched
request ID is audited only when it can be scoped to the current live submission
batch; routine connection and unrelated request errors remain filtered. When one
order can be identified safely, the error is correlated back to that order.
order_statuses and rejection events also retain IBKR's whyHeld value. The
same on_timeout actions apply to rejected orders,
gated by on_inactive (which inherits on_timeout when unset): exactly one
replacement may be submitted for the unfilled quantity, and if the replacement
is itself rejected, nothing further is submitted. External cancellations
(error code 202), ThetaGang's own cancellations, and unrecognized error codes
are never replaced — classification fails closed. Setting
on_inactive = "leave_open" keeps rejected orders unreplaced.
Algorithm Configuration
Customize order execution algorithms:
[runtime.orders.algo]
strategy = "Adaptive"
params = [["adaptivePriority", "Patient"]]
Position Management
Calculate Net Contracts
Enable for spread strategies (PMCCs, calendars):
[strategies.wheel.defaults.write_when]
calculate_net_contracts = true
Excess Only for Calls
Write calls only on shares exceeding target allocation:
[strategies.wheel.defaults.write_when.calls]
excess_only = true
No Trading Flag
Temporarily disable trading for specific symbols:
[portfolio.symbols.TSLA]
no_trading = true # Monitor only, no trades
API Configuration
Fine-tune IBKR API behavior:
[runtime.ib_async]
api_response_wait_time = 60 # Seconds to wait for API responses
logfile = "ib_async.log" # Enable API logging for debugging
Target Limits
Set absolute caps on new contracts:
[strategies.wheel.defaults.target]
maximum_new_contracts = 10 # Absolute limit per run
maximum_new_contracts_percent = 0.5 # Or limit by percentage
Requirements
- An Interactive Brokers account (paper or live)
- ib_async and IBC (bundled IBC config targets TWS/Gateway
1050) - Python 3.11–3.15 and
uv(for local development) - For option-writing strategies (
wheel), sufficient capital/margin to cover 100-share standard option contracts on liquid underlyings. Equity-only strategies (regime_rebalanceor share rebalancing) can size in single shares.
Installation
Test with an IBKR paper account before running live:
pip install thetagang
Either configure a local IBC + TWS/IB Gateway installation, or run ThetaGang with Docker.
Quickstart (paper trading)
- Copy the sample config and edit the required fields:
curl -Lq https://raw.githubusercontent.com/brndnmtthws/thetagang/main/thetagang.toml -o ./thetagang.toml
account.numberibc.useridandibc.passwordibc.tradingMode = "paper"- your
symbols.<SYMBOL>.weightallocations
Secrets don't need to live in the file: any config string supports $VAR,
${VAR}, and ${VAR:-default} from the environment (e.g.
password = "${IBKR_PASSWORD}"), which is handy for ibc.userid,
ibc.password, and account.number. Unset variables without a default abort
startup with an error naming the variable.
If you're running locally (not Docker), update the Docker defaults:
ibc.ibcInishould point to your localconfig.iniib_async.logfileshould be a writable local path (or unset)database.pathis relative to the config file location
- Run a dry run to verify connectivity and config:
thetagang --config ./thetagang.toml --dry-run
Usage
thetagang -h
Common flags:
--configpath to a toml config (required)--dry-runshow proposed orders without submitting trades--without-ibcconnect to a running IB Gateway/TWS you started yourself-v/--verbosityincrease log verbosity (repeatable)
All CLI options support environment variables with the THETAGANG_ prefix.
Example: THETAGANG_CONFIG=./thetagang.toml.
State Database
ThetaGang can persist a SQLite database with order activity, executions, historical bars, account snapshots, and decision gates. Live fills are recorded from IBKR's execution-detail event as they arrive; explicit execution-history requests also backfill the same table idempotently. By default, the database is created relative to your config file, and it is reused across runs to build a long-lived history.
[runtime.database]
enabled = true
path = "data/thetagang.db"
# url = "sqlite:////abs/path/thetagang.db"
For Docker runs, make sure the data/ directory is inside the mounted config
volume so the database persists between runs.
Up and running with Docker
A prebuilt image is published at brndnmtthws/thetagang with IBC and IB Gateway bundled.
Mount a directory containing thetagang.toml and IBC's config.ini (sample: ibc-config.ini):
mkdir ~/thetagang
cd ~/thetagang
curl -Lq https://raw.githubusercontent.com/brndnmtthws/thetagang/main/thetagang.toml -o ./thetagang.toml
curl -Lq https://raw.githubusercontent.com/brndnmtthws/thetagang/main/ibc-config.ini -o ./config.ini
Update ~/thetagang/thetagang.toml with your symbols, weights, userid, password, account.number, and tradingMode. If database.enabled = true, create ~/thetagang/data/ so the SQLite database persists across container runs.
Run with Docker:
docker run --rm -i --net host \
-v ~/thetagang:/etc/thetagang \
brndnmtthws/thetagang:main \
--config /etc/thetagang/thetagang.toml
On macOS/Windows, --net host is not supported. Use explicit port mapping and
set watchdog.host to host.docker.internal in your config:
docker run --rm -i \
-p 7497:7497 \
-v ~/thetagang:/etc/thetagang \
brndnmtthws/thetagang:main \
--config /etc/thetagang/thetagang.toml
To run daily Monday–Friday at 9:00 AM via crontab -e:
0 9 * * 1-5 docker run --rm -i -v ~/thetagang:/etc/thetagang brndnmtthws/thetagang:main --config /etc/thetagang/thetagang.toml
Configuration Examples
Conservative Portfolio
Focus on stability with major index ETFs:
[portfolio.symbols.SPY]
weight = 0.50
delta = 0.20 # Lower delta for safer strikes
[portfolio.symbols.TLT]
weight = 0.30
delta = 0.15
[portfolio.symbols.GLD]
weight = 0.20
delta = 0.15
Growth Portfolio with Hedging
Higher risk tolerance with VIX protection:
[portfolio.symbols.QQQ]
weight = 0.60
delta = 0.30
[portfolio.symbols.ARKK]
weight = 0.30
delta = 0.35
[portfolio.symbols.IWM]
weight = 0.10
delta = 0.30
[strategies.vix_call_hedge]
enabled = true
PMCC Strategy
Poor man's covered calls with net contract calculation:
[strategies.wheel.defaults.write_when]
calculate_net_contracts = true # Essential for spreads
[portfolio.symbols.SPY]
weight = 1.0
# Manage long calls separately
# ThetaGang will write short calls against them
Market Hours Trading
Trade only during stable market hours:
[runtime.exchange_hours]
delay_after_open = 3600 # Wait 1 hour after open
delay_before_close = 3600 # Stop 1 hour before close
action_when_closed = "exit" # Don't run outside hours
Portfolio Selection Notes
- Use liquid ETFs or large-cap equities with tight bid-ask spreads (and high option open interest if trading options).
- Model worst-case drawdown and margin requirements before setting
margin_usage. Useful backtesting tools: Portfolio Visualizer Backtest and Portfolio Optimization. - Community discussion: r/thetagang.
Running without IBC
If you already run IB Gateway/TWS manually, you can skip IBC management:
thetagang --config ./thetagang.toml --without-ibc
Ensure the following match your gateway settings:
watchdog.host,watchdog.port,watchdog.clientIdib_async.api_response_wait_timefor slower connections
Development
Check out the code to your local machine and install the Python dependencies:
# Install the pre-commit hooks
uv run pre-commit install
# Run thetagang
uv run thetagang -h
FAQ
| Error | Cause | Resolution |
|---|---|---|
| Requested market data is not subscribed. | Requisite market data subscriptions have not been set up on IBKR. | Configure your market data subscriptions. The default config that ships with this script uses the Cboe One Add-On Bundle and the US Equity and Options Add-On Streaming Bundle. Note: You must fund your account before IBKR will send data for subscriptions. Without funding you can still subscribe but you will get an error from ibc. |
| No market data during competing live session | Your account is logged in somewhere else, such as the IBKR web portal, the desktop app, or even another instance of this script. | Log out of all sessions and then re-run the script. |
ib_async.wrapper ERROR Error 200, reqId 10: The contract description specified for SYMBOL is ambiguous. |
IBKR needs to know which exchange is the primary exchange for a given symbol. | You need to specify the primary exchange for the stock. This is normal for companies, typically. For ETFs it usually isn't required. Specify the primary_exchange parameter for the symbol, i.e., primary_exchange = "NYSE". |
| IBKey and MFA-related authentication issues | IBKR requires MFA for the primary account user. | Create a second account with limited permissions using the web portal (remove withdrawal/transfer, client management, IP restriction, etc permissions) and set an IP restriction if possible. When logging into the second account, ignore the MFA nags and do not enable MFA. A more detailed set of instructions can be found here, from a different project. |
Metadata
Release files for thetagang 2.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| thetagang-2.1.1.tar.gz | 174.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| thetagang-2.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 368.4 kB
Release files / thetagang-2.1.1.tar.gz
| Download URL | thetagang-2.1.1.tar.gz |
|---|---|
| Size | 174.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f345420d07bad47095bc1289e992cb5d3beffd3da078ddf653e012c73d6fda8a
|
|
BLAKE2b-256 checksum How to use checksums |
17105a51603f5178171f4c08f3a47d6c640d59082bdbdab124fed56681e14d18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / thetagang-2.1.1-py3-none-any.whl
| Download URL | thetagang-2.1.1-py3-none-any.whl |
|---|---|
| Size | 193.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
80f1cd60933266a91c621850c54d37efa466aa8c12bb0b94c4694691f5bc66f0
|
|
BLAKE2b-256 checksum How to use checksums |
5054fd5f4a89f6ddbc6316bbaf7fbe2e34c0b2bee204139fdbc3b71a3038584d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|