How to Build an AI Trading Bot with Claude Code and Interactive Brokers

In this article, I’ll show you how to build an AI trading bot using Claude Code, Python, and Interactive Brokers. We’ll create a fully automated paper-trading system that scans for stocks premarket, places trades through the IBKR API, manages risk automatically, sends Telegram alerts, and runs on a schedule… all without manual operations by a human.

This is designed for traders with little or no coding experience. You do NOT need to be a software engineer to build this. Claude handles the coding while you focus on the strategy, execution, and risk management.

By the end of this guide, you’ll have:

  • A working AI trading bot connected to Interactive Brokers
  • An automated stock scanner for S&P 500 gappers
  • A fully scheduled trading workflow
  • Telegram trade alerts and daily summaries
  • A performance dashboard tracking R-multiples and win rate
  • A complete paper-trading environment for testing strategies safely

This is Part 2 of my 3-part AI trading automation series. In Part 1, I showed how Claude reads TradingView charts and create a backtested strategy. In this tutorial, we’ll actually connect Claude to Interactive Brokers and automate the execution process.


What You’ll Build

By the end of this tutorial you’ll have:

  • A working connection between Claude Code and Interactive Brokers
  • A trading bot that reads strategy rules from a JSON file
  • A scanner that filters the S&P 500 for gappers every 30 minutes
  • An autonomous cycle that evaluates rules and places trades automatically
  • Telegram alerts for every trade event plus daily summaries
  • A dashboard that tracks per-trade R multiples
  • Everything scheduled through Windows Task Scheduler

No coding background required. I don’t have one either. Claude does the coding. You’re the trader.


What you’ll need to get started:

You’ll need 3 things installed on your computer:

  1. TWS (Trader Workstation) from Interactive Brokers
  2. Python 3.12 or newer
  3. Claude Code

A separate paper-trading sub-account is recommended so this build doesn’t interfere with anything else you’re testing in your trading account.


Step 1: Configure TWS to Talk to Python

This is the only TWS dialog you’ll touch. Once it’s configured, you rarely need to come back.

  1. Open TWS and log into your paper account.
  2. Go to:
    File > Global Configuration > API > Settings
  3. Check:
    • Enable ActiveX and Socket Clients
    • Allow connections from localhost only
  4. Set Socket Port = 7497 (paper trading)
  5. Uncheck:
    • Read-Only API
  6. Confirm 127.0.0.1 is listed under Trusted IPs.
  7. Restart TWS.

Step 2: Create Your Python Environment

Open a fresh CMD window inside an empty project folder and run:

python -m venv .venv
.venvScriptsactivate
pip install ib_async python-dotenv pandas numpy yfinance requests

This will take about 30 seconds.

Step 3: Test the Interactive Brokers Connection

Now prove Python can talk to TWS. Create a file called test_connect.py with this content:

from ib_async import IB

ib = IB()
ib.connect('127.0.0.1', 7497, clientId=99)
print('Connected:', ib.isConnected())
print('Accounts:', ib.managedAccounts())
ib.disconnect()

Run:

python test_connect.py

You should see:

Connected: True Accounts: ['DU#######']

If you see that, your AI trading bot can now communicate with Interactive Brokers.


Copy-Paste Claude Prompts

Before we start using Claude to write hundreds of lines of strategy code, let’s see it actually do something with this new IBKR connection.

We’ll try to buy one share, then close it all within Claude + IBKR API. If this works, you know the whole AI-to-broker chain is wired up.

Tip: It’s best to do this testing and connection during market hours.

Every prompt below is fully copy-paste ready.

Prompt 1: Test buying 1 share

Place a paper-trading BUY order for 1 share of MU on Interactive Brokers.

PREREQ CHECK:
1. Confirm ib_async is installed: python -c "import ib_async". If not, STOP and tell me to run: pip install ib_async
2. Confirm TWS is running on this machine on port 7497 (paper). Try a quick connection test with clientId=10. If it fails, STOP and tell me to open TWS and verify API is enabled.

CREATE a file named buy_one.py that does exactly this:
- Connect to IBKR at 127.0.0.1:7497 with clientId=10
- Qualify the contract Stock("MU", "SMART", "USD")
- Place a MarketOrder BUY for 1 share with outsideRth=True
- Wait up to 10 seconds for the order status to settle (not just PendingSubmit)
- Print: order ID, fill price (or "pending" if not yet filled), final status
- Disconnect cleanly in a finally block

RUN buy_one.py via subprocess. Print its stdout.
Print: "ORDERED: 1 share MU. Check TWS positions panel."

DO NOT proceed to close the position. That is a separate prompt.

FAILURE HANDLING:
- If the order is rejected, print the trade.log entries showing why
- If the connection raises, print the exception and exit nonzero

You should see IBKR TWS taking one share of MU in your positions window.

Prompt 2: Close that position

Close the paper-trading MU position that buy_one.py just opened.

PREREQ CHECK:
1. Confirm buy_one.py exists in the current directory
2. Confirm TWS is still running on port 7497

CREATE a file named close_one.py that does exactly this:
- Connect to IBKR at 127.0.0.1:7497 with clientId=11 (different from buy_one's 10 so they do not collide)
- Call ib.positions() and find the position where contract.symbol == "MU"
- If no MU position found, print "NO POSITION TO CLOSE" and exit 0
- If found, place a MarketOrder SELL for the full position size, outsideRth=True
- Wait up to 10 seconds for final fill status
- Print: sold quantity, fill price, status
- Disconnect cleanly in a finally block

RUN close_one.py via subprocess. Print its stdout.
Print: "CLOSED: position flat. Verify in TWS."

Now the position should be flat in IBKR TWS. Once the test is successful, we’re ready to build the actual bot setup.

Step 4: Build Your Strategy File

This is the stage where your strategy stops being a feeling. It becomes a JSON file. Every entry or exit rule, every time gate, written down and read by the bot at the start of every cycle.

This is “Trend Join Long strategy” from the last blog post. Backtested by Tradingview + Claude. Long only, 5-minute chart, six filters split into daily and intraday.

Here is a quick review of the filters:

Daily filters:

– D1: price above yesterday’s daily high

– D2: yesterday’s close above the 200-day SMA (trading with the longer trend, not against it)

– D3: gap of at least 3% from the previous close

Intraday filters:

– I1: price above today’s premarket high

– I2: price above today’s high so far (joining strength, not buying a fade)

– I3: relative volume at least 2× the 14-day average

If all six filters pass, the bot takes a trade on the stock. If anyone fails, it doesn’t execute.

Prompt 3: Create the strategy file

Create the strategy file for the IBKR paper-trading bot in the current directory.

PREREQ CHECK:
- Confirm the current directory does NOT already contain rules.json. If it does, STOP and ask me to clean it up first.

CREATE one file named rules.json with this EXACT content (copy literally; do not improvise field names, casing, or values):

{
  "strategy_name": "Trend Join Long",
  "direction": "long_only",
  "trade_timeframe": "5m",

  "universe_filters": {
    "index": "S&P 500",
    "min_price_usd": 3.0
  },

  "daily_filters": {
    "D1_above_prior_day_high": true,
    "D2_prior_close_above_sma200": true,
    "D3_min_gap_pct_from_prior_close": 3.0
  },

  "intraday_filters": {
    "I1_above_premarket_high": true,
    "I2_above_today_hod": true,
    "I3_rvol_min": 2.0,
    "I3_rvol_lookback_days": 14
  },

  "time_filter": {
    "earliest_entry_et": "10:05",
    "latest_entry_et": "15:30",
    "force_close_et": "15:51"
  },

  "exit": {
    "initial_stop_rule": "lod_minus_1pct",
    "partial_profit_trigger_R": 0.75,
    "partial_profit_fraction": 0.3333,
    "breakeven_trigger_R": 1.0,
    "post_breakeven_trail": "swing_low_5m_2_2"
  },

  "risk": {
    "max_risk_per_trade_pct": 1.0,
    "max_position_size_pct_of_portfolio": 10,
    "max_concurrent_positions": 5
  }
}

AFTER CREATING THE FILE:
- Print the file contents back to me as confirmation
- Do NOT create any other files
- Do NOT run anything

If you want to use a different strategy or create your own, just edit the values in rules.json. The bot doesn’t care what’s in there as long as the keys match.

Step 5: Create Configuration Files

Next is the boring part… we’ll create these files:

  • .env
  • .gitignore
  • requirements.txt

These files manage:

  • API ports
  • paper/live safety checks
  • Telegram credentials
  • portfolio sizing
  • dependency management

Create the configuration files for the IBKR paper-trading bot in the current directory.

PREREQ CHECK:
1. Confirm rules.json exists in the current directory (created in Step 4). If not, STOP.
2. Confirm none of these files already exist: .env, .gitignore, requirements.txt. If any do, STOP and ask me to clean up.

CREATE EXACTLY THESE 3 FILES with these EXACT contents:

1. .env
   IBKR_HOST=127.0.0.1
   IBKR_PORT=7497
   IBKR_CLIENT_ID=2
   IBKR_EXEC_CLIENT_ID=3
   PAPER_TRADING=true
   PORTFOLIO_VALUE_USD=25000
   MAX_TRADE_SIZE_USD=2500
   MAX_TRADES_PER_DAY=5
   MAX_RISK_PER_TRADE_PCT=1.0
   TELEGRAM_BOT_TOKEN=
   TELEGRAM_CHAT_ID=

2. .gitignore
   .env
   .venv/
   __pycache__/
   *.pyc
   trades.csv
   logs/
   open_positions.json
   safety-check-log.json
   watchlist.txt

3. requirements.txt (no version pins)
   ib_async
   python-dotenv
   pandas
   numpy
   yfinance
   requests

AFTER CREATING THE FILES:
- Print the file tree
- Print "READY for Step 6: build the bot code"
- Do NOT run anything

Step 6: Build the Trading Bot Code

Now we’re actually coding the bot files. One talks to IBKR. One reads the rules. One orchestrates. One actually places orders. The orchestrator and the trader run as separate processes with separate client IDs so they don’t fight over the TWS connection. I learned this the hard way.

We’ll create:

  • bot.py
  • trade.py
  • strategy.py
  • ibkr_client.py

These files handle:

  • Interactive Brokers API communication
  • strategy evaluation
  • order execution
  • trade logging
  • position sizing
  • risk management
Create the four Python files that make up the IBKR paper-trading bot.

PREREQ CHECK:
1. Confirm these files exist in the current directory: rules.json, .env, .gitignore, requirements.txt.
2. Confirm Python: python --version must be 3.12+. If not, STOP.
3. Confirm deps: python -c "import ib_async, dotenv, pandas". If any ImportError, STOP and tell me to run: pip install ib_async python-dotenv pandas numpy yfinance requests
4. Confirm none of these files already exist: bot.py, trade.py, strategy.py, src/ibkr_client.py.

CREATE EXACTLY THESE 5 FILES:

1. src/__init__.py (empty file)

2. src/ibkr_client.py
   Class IBKRClient with:
   - __init__(host, port, client_id): self.ib = IB(); self.ib.connect(host, port, clientId=client_id)
   - place_order(symbol, side, quantity) -> Trade: qualify Stock(symbol, "SMART", "USD") first, then place MarketOrder with outsideRth=True, then wait up to 10s for the order status to settle (not just PendingSubmit)
   - disconnect(): self.ib.disconnect()

3. strategy.py
   Function evaluate(symbol, ib) -> dict with keys "pass" (bool), "reasons" (list of strings), "price" (float).

   Order of checks (return early on first failure):
   a) Already in position: call ib.positions(); if any position.contract.symbol == symbol AND position.position > 0, return {"pass": False, "reasons": ["already in position"], "price": 0.0}.
   b) Time window: get current ET time via zoneinfo.ZoneInfo("America/New_York"). Read earliest_entry_et and latest_entry_et from rules.json. If outside window, return {"pass": False, "reasons": ["outside entry window HH:MM-HH:MM"], "price": 0.0}.
   c) Otherwise: get current market price via ib.reqMktData snapshot. Return {"pass": True, "reasons": ["time gate ok", "no existing position"], "price": price}.

   Cast every bool to bool() before adding to the result dict (Python 3.14 json encoder rejects numpy bools).
   Append the full result dict to logs/safety_log.jsonl, one JSON object per line. Create logs/ directory if missing.

   IMPORTANT: do NOT implement D1-D3 or I1-I3 in this file. Those filters use yfinance and the prefilter that we build in a later chapter. bot.py is the single-symbol dev tool; it only checks the time gate and the position dedupe.

4. bot.py
   CLI args (argparse): --symbol (required), --check-only (flag).
   Order of operations:
   1. Load .env via python-dotenv
   2. Hard guard: if PAPER_TRADING=true and IBKR_PORT in {7496, 4001}, sys.exit("ABORT: paper flag but live port"). Same in reverse.
   3. Read trades.csv (create with header timestamp_iso,symbol,side,size,fill_price,order_id,status if missing). Count today's BUY rows (timestamp_iso starts with today's ET date). If count >= MAX_TRADES_PER_DAY, print message and exit 0.
   4. Connect via IBKRClient(IBKR_HOST, IBKR_PORT, IBKR_CLIENT_ID).
   5. Call strategy.evaluate(symbol, ibkr.ib). Print result dict.
   6. If --check-only: disconnect, exit 0.
   7. If not pass: print reasons, disconnect, exit 0.
   8. Size position: budget = min(MAX_TRADE_SIZE_USD, PORTFOLIO_VALUE_USD * 0.10). quantity = int(budget / price). If quantity < 1, print "position too small", exit 0.
   9. Spawn subprocess: python trade.py --symbol <S> --side BUY --size <Q>. 30s timeout. Print stdout.
   10. Read last row of trades.csv. Print success or failure based on status column.
   Prefix every print line with [HH:MM:SS ET].

5. trade.py
   CLI args: --symbol, --side, --size (int).
   - Load .env. Use IBKR_EXEC_CLIENT_ID (NOT IBKR_CLIENT_ID).
   - Build IBKRClient. Call place_order(symbol, side, size).
   - Inspect trade.orderStatus.status. If in {"Cancelled", "ApiCancelled", "Inactive"}, exit nonzero and print trade.log entries.
   - Otherwise, append one row to trades.csv: timestamp_iso (UTC), symbol, side, size, fill_price (trade.orderStatus.avgFillPrice or 0), order_id (trade.order.orderId), status.
   - Always call ibkr.disconnect() in a finally block.

FAILURE HANDLING:
- If IBKR connection raises, print the exception and exit 1.
- If trades.csv is locked by another process, retry once after 1 second, then exit 1.
- All subprocess calls use a 30-second timeout. If exceeded, kill and exit 1.

CONSTRAINTS:
- Use ib_async, NOT ib_insync.
- Python 3.12+ on Windows. Use pathlib.Path everywhere, not os.path string concat.
- All output files in project root, not user home.
- Do not pip install anything; assume deps are installed.

AFTER CREATING FILES:
- Print the file tree
- Print "READY: run `python bot.py --symbol NVDA --check-only`"
- Do NOT run anything yourself.

Test the bot in two stages. First a dry run:

python bot.py --symbol NVDA --check-only

The bot connects, checks the time gate, prints what would happen, exits without placing a trade. If you see a clean evaluation, the wiring is good.

Then for real:

python bot.py --symbol NVDA

If the time gate is open, the bot places a paper BUY. Switch to your TWS to verify.

Step 7: Build the S&P 500 Scanner

Now we’ll create a stock scanner that:

  • Pulls S&P 500 data using yfinance
  • Detects gappers
  • Filters stocks above 3%
  • Generates a dynamic watchlist every 30 minutes

This becomes the trading universe for the bot.

Note: This needs to be refreshed every few months if there is new additions or removals to the S&P500.

Create the S&P 500 ticker universe file for the IBKR paper-trading bot in the current directory.

PREREQ CHECK:
1. Confirm these files exist in the current directory: rules.json, bot.py, strategy.py, src/ibkr_client.py. If not, STOP.
2. Confirm src/sp500_tickers.py does NOT already exist. If it does, STOP and ask me to clean up.

CREATE one file: src/sp500_tickers.py

CONTENTS:
- A single Python module-level constant named SP500_TICKERS
- A Python list of strings
- Every current S&P 500 ticker symbol, in IBKR format (use a SPACE, not a hyphen, for class B shares: "BRK B" not "BRK-B"; "BF B" not "BF-B")
- Use the current Wikipedia "List of S&P 500 companies" composition as the source list
- The list MUST be hardcoded; do not fetch at import time
- 503 entries total (S&P 500 includes some companies with multiple share classes)
- Sort alphabetically for readability

AFTER CREATING THE FILE:
- Print the number of tickers in the list (must be ~503)
- Print the first 5 and last 5 entries as a sanity check
- Do NOT create any other files
- Do NOT run anything

Step 8: Build the morning prefilter scanner

The previous step adds the ticker list. Now we build the actual scanner.

Pulls 2-day bars for every S&P 500 ticker from Yahoo Finance, computes gap from yesterday’s close, filters for gaps above 3%, writes the top 20 to watchlist.txt.

Why yfinance and not IBKR?

IBKR paper market-data subscription caps at around 400 concurrent requests. The S&P 500 is 503 names. yfinance is free, fast, and has no such limit. yfinance handles the scanning; IBKR still handles execution.

Create the morning prefilter script for the IBKR paper-trading bot in the current directory.

PREREQ CHECK:
1. Confirm src/sp500_tickers.py exists with a SP500_TICKERS list. If not, STOP.
2. Confirm yfinance is installed: python -c "import yfinance". If not, STOP and tell me to: pip install yfinance
3. Confirm morning_prefilter.py does NOT already exist. If it does, STOP.

CREATE one file: morning_prefilter.py

This is a standalone CLI script. Top-level constants:
    WATCHLIST_PATH = Path("watchlist.txt")
    DEFAULT_MIN_GAP_PCT = 3.0
    DEFAULT_MIN_PRICE = 3.0
    MAX_SURVIVORS = 20
    ET = ZoneInfo("America/New_York")

CLI args (argparse):
- --min-gap (float, default DEFAULT_MIN_GAP_PCT)
- --min-price (float, default DEFAULT_MIN_PRICE)
- --dry-run (flag; if set, do not write watchlist.txt)

Logic in this order:
1. Convert IBKR ticker format to Yahoo format: "BRK B" becomes "BRK-B".
2. Call yf.download(
       tickers=" ".join(yahoo_tickers),
       period="2d",
       interval="1d",
       group_by="ticker",
       threads=5,
       progress=False,
       auto_adjust=True,
   )
   IMPORTANT: threads=5, not 10. Higher concurrency exhausts file descriptors when run under Task Scheduler.
3. For each ticker:
   - yesterday_close = bars.iloc[-2]["Close"]
   - today_open = bars.iloc[-1]["Open"]
   - today_close = bars.iloc[-1]["Close"]   (this is the LIVE intraday print during market hours)
   - today_high = bars.iloc[-1]["High"]
   - today_low = bars.iloc[-1]["Low"]
   - gap_pct = (today_close - yesterday_close) / yesterday_close * 100
   - Filter: today_close >= min_price AND gap_pct >= min_gap
   - Count rejections into below_gap, below_price, failed buckets.
4. Sort survivors by gap_pct descending. Cap at MAX_SURVIVORS.
5. If --dry-run is NOT set, write watchlist.txt with EXACTLY this format:

    # Auto-generated by morning_prefilter.py at YYYY-MM-DD HH:MM ZONE
    # Filters: gap >= X%, price >= $Y
    # Source: yfinance (screening only); IBKR handles execution
    # Survivors: N (capped at 20) of 503
    #
    # ticker  # gap +X.XX%  open $X.XX  prev $X.XX
    AAPL  # gap +3.45%  open $185.20  prev $180.00
    ...

6. Print a JSON summary to stdout with these keys:
   success, total_screened, survivors_count, below_gap, below_price, failed, elapsed_seconds, top_20_survivors (list of "TICKER (+X.XX%)" strings), watchlist_path.

DEGRADATION TRIPWIRES (print to stderr if triggered):
- If failed/total >= 0.95: print "ALERT: Yahoo-wide failure suspected; check yfinance changelog"
- If failed/total >= 0.30 but below 0.95: print "ALERT: yfinance degradation (failed N/M)"
(We wire these to Telegram in the notifications chapter; for now just stderr.)

FAILURE HANDLING:
- If yf.download returns an empty dataframe, print error JSON and exit 1.
- If an individual ticker raises KeyError or IndexError or ValueError, add to failed list and continue. Do NOT crash the whole run.

CONSTRAINTS:
- Do NOT modify any existing files (bot.py, strategy.py, rules.json, etc.).
- Use ZoneInfo from zoneinfo, not pytz.

AFTER CREATING THE FILE:
- Print "READY: run `python morning_prefilter.py --dry-run` to test"
- Do NOT run anything yourself.

Test it:

python morning_prefilter.py --dry-run

You’ll see a JSON summary with the top 20 gappers and how many tickers got filtered out. If you’re happy, run it for real:

python morning_prefilter.py
type watchlist.txt

watchlist.txt now has the candidates the cycle will trade.

Heads up: if you run this on a flat day with no gappers above 3%, the watchlist will be empty. For testing, lower --min-gap to 1.0 to ensure survivors.

Step 9: Create the Autonomous Trading Cycle

This is the “brain” of the AI trading bot. This is the longest prompt in the post but also the most important.

Every 5 minutes the bot will:

  • Check market hours
  • Manage open trades
  • Trail stops
  • Scan for new entries
  • Place orders
  • Force-close positions before market close

Create the autonomous trading cycle for the IBKR paper-trading bot in the current directory.

PREREQ CHECK:
1. Confirm these files exist: bot.py, trade.py, strategy.py, rules.json, morning_prefilter.py, src/sp500_tickers.py, src/ibkr_client.py.
2. Confirm watchlist.txt exists. If not, tell me to run `python morning_prefilter.py` first and STOP.
3. Confirm cycle.py does NOT already exist. If it does, STOP.

CREATE one file: cycle.py

This script runs every 5 minutes from Windows Task Scheduler. The main() function does these 9 steps in order:

1. TIME GATE — returns one of: "weekend", "too_early", "closed", "manage_only", "force_close", "ok".
   - Saturday/Sunday → exit immediately
   - Before 10:00 ET or after 16:00 ET → exit immediately
   - 10:00-10:05 ET or 15:30-15:51 ET → "manage_only" (manage existing, no new entries)
   - 15:51-16:00 ET → "force_close"
   - 10:05-15:30 ET → "ok"
   Must exit in under 1 second on weekend/too_early/closed (this is what makes "every 5 min, all day" cheap).

2. LOAD STATE — read open_positions.json. Return [] if missing. Each position dict has: symbol, entry_price, entry_time_iso, qty, initial_stop, stop_order_id, state, R.

3. CHECK STOP-OUTS — for each open position, query ibkr.ib.fills() in the last hour. Match by `order_id == position["stop_order_id"]`. (CRITICAL: do NOT match by quantity. Quantity matching causes false stop-outs after partials. This was a real bug in the Mac version.) Remove stopped-out positions.

4. MANAGE EACH POSITION — three states:
   - "pre_breakeven": if price >= entry + 1R → cancel old stop, place new stop at entry, state="post_breakeven_no_partial". If price >= entry + 0.75R → sell ceil(qty/3) at market, place new stop at entry*0.99 on remaining, state="post_breakeven_partial_done".
   - "post_breakeven_*": compute 5-minute swing lows (a bar whose low is lower than the 2 bars before AND the 2 bars after). If newest swing low > current stop, cancel old stop, place new stop at swing_low - 0.01. Stops only ratchet up, never down.

5. SAVE STATE — atomic write (write .tmp, os.replace to final).

6. IF FORCE_CLOSE — cancel every stop_order_id, market-SELL every position, clear open_positions.json, exit.

7. IF MANAGE_ONLY — exit (no new entries).

8. ENTRY SCAN (status == "ok"):
   - Count today's BUYs in trades.csv. If >= MAX_TRADES_PER_DAY, exit.
   - Read watchlist.txt (skip lines starting with #).
   - CRITICAL: call ibkr.ib.positions() FIRST. Skip any ticker we already hold. (This prevents the silent double-entry bug where a subprocess crash after fill leads to re-attempt. Also from the Mac version.)
   - For each remaining ticker, pull 5-min bars via yfinance and run rules.json's 6 filters (D1-D3 daily + I1-I3 intraday).
   - For each passing ticker: initial_stop = low_of_day * 0.99. R = price - initial_stop. risk_dollars = PORTFOLIO_VALUE_USD * (MAX_RISK_PER_TRADE_PCT / 100). size = min(floor(risk_dollars / R), floor(PORTFOLIO_VALUE_USD * 0.10 / price)). If size 

Test it:

python cycle.py

Outside market hours it exits in under a second. During market hours it does the full loop. We’ll automate it in Step 12.

Step 10: Set up Telegram Notifications

Next we set up notifications to your phone. The bot will send Telegram alerts for:

  • New entries
  • Stop-outs
  • Partial profit takes
  • Trailing stop updates
  • Daily summaries
  • Crash alerts

This allows you to monitor the system remotely.

Set up your Telegram bot (2 minutes)

  1. On Telegram, message @BotFather/newbot → follow the prompts → save the bot token it gives you.
  2. Message @userinfobot/start → save the chat ID it gives you.
  3. Open .env and paste both values into TELEGRAM_BOT_TOKEN= and TELEGRAM_CHAT_ID=.

Add Telegram push notifications to the existing IBKR paper-trading bot.

PREREQ CHECK:
1. Confirm bot.py, cycle.py, morning_prefilter.py, src/__init__.py exist. If not, STOP.
2. Confirm requests installed: python -c "import requests". If not, STOP and tell me to: pip install requests
3. Confirm src/notify.py does NOT exist yet.

CREATE ONE NEW FILE: src/notify.py

- At module top, load TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, NOTIFY_URL from environment via python-dotenv.
- Single function: def notify(title: str, body: str, priority: str = "default") -> None
- If TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are both set: POST https://api.telegram.org/bot{token}/sendMessage with chat_id, text=f"*{title}*n{body}", parse_mode="Markdown". 5-second timeout.
- If NOTIFY_URL is set (ntfy fallback): POST to it with body=body, headers={"Title": title, "Priority": priority}. 5-second timeout.
- Wrap both in try/except. Log exceptions to logs/notify_errors.log but NEVER raise. Fire-and-forget.

MODIFY morning_prefilter.py (preserve all existing logic, only add notify calls):

At the end of main(), unless --dry-run is set:
- On success: notify(f"Prefilter {hh:mm ET}", "{N}/503 survivors in {elapsed}sn" + bullet list of top 20 survivors with their gap %, "default")
- On failure: notify(f"Prefilter FAILED {hh:mm ET}", error_message, "high")
- On degradation tripwire: notify with priority="high" and the alert message.

MODIFY cycle.py (preserve all existing logic, only add notify calls at these events):
- New entry: notify(f"BUY {symbol}", f"@ ${price:.2f}, stop ${stop:.2f}, qty {qty}", "default")
- Stop-out: notify(f"STOP {symbol}", f"exit ${price:.2f}, P&L ${pnl:+.2f}", "default")
- Partial fill at 0.75R: notify(f"PARTIAL {symbol}", f"sold {sold}/{total} @ ${price:.2f}", "default")
- Breakeven flip: notify(f"BE {symbol}", f"stop -> ${new_stop:.2f}", "default")
- Stop trail up: notify(f"TRAIL {symbol}", f"stop ${old:.2f} -> ${new:.2f}", "default")
- Force-close starts: notify("EOD Force Close", f"flattening {N} positions", "high")
- Unhandled exception: notify("Cycle CRASHED", str(exc)[:500], "high")

CONSTRAINTS:
- Every notify() call must be wrapped so failure does NOT break trade logic.
- Do NOT modify any trading logic. Only add notify lines.
- Do NOT print tokens or chat IDs anywhere.

AFTER CHANGES:
- Print "READY: set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in .env, then test with: python -c "from src.notify import notify; notify('Test', 'Hello')""
- Do NOT run anything yourself.

Test:

python -c "from src.notify import notify; notify('Test', 'Hello from IBKR bot')"

Check your phone. If the message arrives, Telegram is wired up.

Step 11: Daily summary + log rotation

Real-time alerts are great, but at the end of the day you want one consolidated summary. Trade count, win rate, P&L, best name, worst name. Same prompt also writes a log rotation script so this thing can run for months without filling the disk.

Add the daily performance summary and log rotation to the existing IBKR paper-trading bot.

PREREQ CHECK:
1. Confirm cycle.py and src/notify.py exist. If not, STOP.
2. Confirm pandas installed.
3. Confirm neither compute_perf.py nor rotate_logs.py already exists.

CREATE 2 FILES:

1. compute_perf.py
   - Read trades.csv. Filter to today's rows (timestamp_iso starts with today's ET date). If trades.csv is missing, treat as zero trades.
   - Pair BUY rows with SELL rows by symbol (FIFO).
   - For each closed pair, compute: P&L dollars, P&L %, hold time minutes.
   - Aggregate:
     * total_trades (closed pairs)
     * wins, losses, win_rate_pct
     * gross_pnl_usd
     * largest_winner (symbol + amount), largest_loser
     * avg_winner, avg_loser
     * profit_factor (sum wins / abs(sum losses); "inf" if no losses; "n/a" if zero trades)
   - Print JSON summary to stdout.
   - Send Telegram via src.notify.notify():
     title = f"Daily Summary {YYYY-MM-DD}"
     body = f"Trades: {N} ({W}W / {L}L, {pct}%)nP&L: ${pnl:+.2f}nBest: {best_sym} ${best:+.2f}nWorst: {worst_sym} ${worst:+.2f}nPF: {pf}"
   - If zero closed trades today, body = "No closed trades today."
   - Do NOT modify trades.csv.

2. rotate_logs.py
   - For each file in logs/ ending in .log or .jsonl: if mtime's ET date is earlier than today, move to logs/archive/YYYY-MM-DD/ via os.replace (atomic). Create logs/archive/ if missing.
   - For trades.csv: keep last 90 days of rows; archive older rows to logs/archive/trades_YYYYMMDD.csv.
   - For safety-check-log.json: if file size > 5 MB, archive the entire file with today's date prefix and start fresh.
   - Print "Rotated N files to logs/archive/".
   - Exit 0 even if there is nothing to do.

CONSTRAINTS:
- Use atomic moves (os.replace), not copy-then-delete.
- Both scripts must be safe to run any time of day.

AFTER CHANGES:
- Print "READY: run `python compute_perf.py` (best after market close) and `python rotate_logs.py`"
- Do NOT run anything yourself.

Test:

python compute_perf.py
python rotate_logs.py

The daily summary lands on your phone via Telegram. Log rotation happens silently in the background.

Step 12: Schedule everything

The bot needs to run itself. We will use Task Scheduler on windows. Five categories, eleven tasks total: log rotation, keep-awake, prefilter (7×/morning), cycle (every 5 min), and EOD summary.

Instead of clicking through Task Scheduler eleven times, Claude writes one Python script that registers all of them in one go. Plus a teardown script for when you’re done.

We’ll automate:

  • Morning scans
  • Trading cycles
  • Daily summaries
  • Log rotation
  • Keep-awake functions

At this point, the AI trading bot runs itself.

Create the Task Scheduler setup and teardown scripts for the IBKR paper-trading bot.

PREREQ CHECK:
1. Confirm these files exist in the current directory: cycle.py, morning_prefilter.py, compute_perf.py, rotate_logs.py.
2. Confirm .venvScriptspython.exe exists. If not, STOP and tell me to create the venv first.
3. Confirm we are on Windows: `where schtasks` must succeed. If not, STOP.
4. Confirm neither setup_schedule.py nor cleanup_schedule.py already exist.

CREATE 2 FILES:

1. setup_schedule.py
   Registers eleven Windows Task Scheduler entries.

   At the top:
   - VENV_PY = absolute path to .venvScriptspython.exe via pathlib + .resolve()
   - PROJECT_DIR = absolute path to current directory
   - ET = ZoneInfo("America/New_York")
   - For each task's ET time, convert to local machine time at runtime using zoneinfo (so the script works whether you are on PT, ET, or anywhere else).
   - DETECT date format: run `powershell -c "(Get-Culture).DateTimeFormat.ShortDatePattern"` to get the locale-specific format, then format today's date to match. Common results: "M/d/yyyy" (US default) and "yyyy/MM/dd" (other locales). schtasks rejects mismatched formats.

   Task list (all use user context, no admin elevation, no SYSTEM account):

   - HT_LogRotate: 09:25 ET, Mon-Fri WEEKLY. Runs: VENV_PY rotate_logs.py
   - HT_KeepAwake: 09:30 ET, Mon-Fri WEEKLY. Runs raw command: powercfg /change standby-timeout-ac 0
   - HT_Prefilter_01 through HT_Prefilter_07 (7 separate tasks; schtasks WEEKLY supports only one /ST per task):
       09:55, 10:25, 10:55, 11:25, 11:55, 12:25, 12:55 ET, Mon-Fri WEEKLY. Each runs: VENV_PY morning_prefilter.py
   - HT_Cycle: starts 10:00 ET, /SC MINUTE /MO 5, runs daily. Runs: VENV_PY cycle.py. (cycle.py's time_gate handles weekend/off-hours, exits in 
   - /tr """ """ (properly quoted for spaces in paths)
   - /sc WEEKLY /D MON,TUE,WED,THU,FRI /ST HH:MM for weekday tasks
   - /sc MINUTE /MO 5 /SD  /ST HH:MM for HT_Cycle
   - /F (overwrite if exists)
   - Do NOT use /RL HIGHEST. Do NOT use /RU SYSTEM.

   If any command returns non-zero, print stderr and continue with the remaining tasks (do not abort).

   At the end:
   - Run: schtasks /query /tn HT_* /fo TABLE. Print the table.
   - Print "SCHEDULED: 11 tasks created. The bot will fire on its own starting next market open. Watch Telegram."

2. cleanup_schedule.py
   Tears down everything setup_schedule.py created.

   Logic:
   - Query all HT_* task names: parse output of `schtasks /query /fo CSV /nh`.
   - For each, run: schtasks /delete /tn  /f
   - Restore default power settings: powercfg /change standby-timeout-ac 30
   - Print "CLEANUP DONE:  tasks deleted; default power settings restored."
   - If zero HT_* tasks exist, print "Nothing to clean." and exit 0.

CONSTRAINTS:
- Both scripts must be safe to re-run (idempotent).
- Use pathlib.Path everywhere.
- Use ZoneInfo from zoneinfo (not pytz).

AFTER CREATING FILES:
- Print "READY: run `python setup_schedule.py` to register all scheduled tasks."
- Print "When the demo is done, run `python cleanup_schedule.py` to tear everything down."
- Do NOT run either script yourself.

Register everything:

python setup_schedule.py

You’ll see eleven lines, one per task, then a schtasks /query table. The bot is live.

Step 13: Build the Performance Dashboard

We’ll generate:

  • Daily P&L summaries
  • R-multiple histograms
  • Open position tracking
  • Win-rate analytics
  • Recent trade logs

All displayed in a self-refreshing HTML dashboard.

Extend the IBKR paper-trading bot's compute_perf.py to also generate an HTML dashboard.

PREREQ CHECK:
1. Confirm compute_perf.py exists. If not, STOP.
2. Confirm pandas installed.
3. Confirm trades.csv exists or will be created (zero-trade case is allowed).

MODIFY compute_perf.py (preserve the existing Telegram summary and JSON stdout behavior; add HTML generation as an additional side effect):

After computing the existing aggregates, also do this:

1. Compute per-trade R for each closed BUY/SELL pair:
   - R = (sell_price - buy_price) / (buy_price - initial_stop_price)
   - initial_stop_price comes from open_positions.json if available, OR from a stop_price column in trades.csv if present, OR fall back to (buy_price * 0.01) as a proxy if neither exists.
   - Per-trade R uses the ACTUAL dollars risked on that trade, not a fixed 1% of the account. This isolates the edge from the position-size cap.

2. Build R-multiple histogram buckets and count trades per bucket:
   (-inf, -2R], (-2R, -1R], (-1R, 0R], (0R, +1R], (+1R, +2R], (+2R, +3R], (+3R, +inf)

3. Read open_positions.json (empty list if missing). Get current open positions with: symbol, qty, entry, current stop, unrealized R based on most recent price.

4. Read safety-check-log.json (last line). Capture last cycle timestamp and status. If missing, show "no cycle data yet".

5. Write dashboard/index.html. Create the dashboard/ directory if missing. Layout requirements:
   -  includes Bootstrap 5 CSS from CDN and a meta tag: 
   - Header strip: "Bot Status: ACTIVE", last cycle time
   - Card 1: Today's P&L summary (total P&L, wins, losses, win rate)
   - Card 2: R-multiple histogram. Use pure CSS bar chart (div widths proportional to bucket counts). No external JS chart libraries.
   - Card 3: Open positions table (symbol, qty, entry, stop, unrealized R)
   - Card 4: Recent closed trades, last 20 rows, with per-trade R color-coded green for positive R, red for negative R.

6. Always preserve the existing JSON stdout summary and Telegram notification (do not remove or break either).

CONSTRAINTS:
- Do NOT break the existing Telegram + JSON behavior.
- No JavaScript libraries; pure HTML + CSS via Bootstrap CDN.
- HTML must work when opened directly in the browser (no server required).
- Cast all numpy types to Python types before HTML rendering (Python 3.14 JSON quirk).

AFTER CHANGES:
- Print "READY: run `python compute_perf.py`, then open dashboard/index.html in browser"
- Do NOT run anything yourself.


Troubleshooting

Connected: False

  • TWS is not running
  • Wrong API port
  • Trusted IPs missing
  • API access disabled

Orders Rejected

  • Missing market data subscription
  • Trading permissions not enabled
  • Wrong account type

Scheduled Tasks Not Running

  • Machine is asleep
  • User is logged out
  • Task Scheduler permissions issue

Important Expectations

I’m not going to pretend this is a money printer. The backtested numbers from Part 1 (TradingView + Pine) don’t perfectly transfer to live execution on IBKR. Real reasons for that:

  • The backtest used a limited ticker set and a specific timeframe
  • Manual execution involves judgment calls I haven’t fully codified into rules
  • There are edge cases in live trading (gap risk, halt risk, partial fills) that backtests don’t capture

The honest framing: the bot is a starting point, not a finished product. Use it to learn the architecture, then iterate on the strategy file (rules.json) until your paper account shows real edge before you ever think about live capital.

Paper trade for at least 2 weeks before touching anything beyond paper. Watch the daily Telegram summaries, watch the dashboard, watch how often the bot does the right thing vs the wrong thing.

Bonus Tip: try IB Gateway instead of TWS for production

TWS is what I used in this video because it has a visible interface you can follow along with. For actual production, switch to IB Gateway. Different default port (4002 for paper instead of 7497), no GUI, less memory, much more reliable for long-running automation. Same prompts work; just change IBKR_PORT in your .env.


Frequently Asked Questions

Cost and access

Do I need a paid Claude subscription? Yes. Claude Code requires Claude Pro or Claude Max. The free tier doesn’t include it.

Do I need to pay for IBKR market data? For paper trading on US stocks, no. IBKR includes free delayed US equity data with paper accounts. Real-time live data is a separate subscription you add inside Account Management if you ever switch to live.

What’s the minimum balance to open an IBKR account? For paper accounts, zero. They’re free. For live accounts, IBKR has no minimum on the standard retail accounts.

Platform and infrastructure

Can I run this on a Mac? Yes. Install TWS for Mac instead of Windows, and use launchd instead of Task Scheduler. Everything else (Python code, prompts, strategy file) is identical.

Can I run this on a cloud server so my computer doesn’t have to stay on? Yes, but TWS needs a GUI which most VPS setups don’t have. IB Gateway is the headless version designed for this. It runs fine on a $5-10/month VPS. Change IBKR_PORT to 4002 (paper) or 4001 (live) and use cron instead of Task Scheduler. I might cover this setup in a future video.

Customizing the bot

Can I use a different strategy than Trend Join Long? Yes. That’s the whole point of putting the strategy in rules.json. Edit the values to match yours. If you want to add new filter types (something not in D1-D3 or I1-I3), you’ll also need to update strategy.py and cycle.py to evaluate them.

How do I add another indicator (RSI, MACD, etc.)? Add the computation in a helper module like src/indicators.py, add a new filter type to rules.json, then update strategy.py to call your new function. The pattern: indicator function takes a pandas DataFrame of bars, returns a Series; filter function reads the value and compares to your threshold.

Can I add short selling? Yes, but it’s not a one-line change. Add a "direction": "long_short" or "short_only" mode to rules.json, then update strategy.py and cycle.py to evaluate short filters (inverse of long) and place SELL-to-open orders. Treat this as a Part 2.5 video for yourself.

Can I use this for options or futures? Not as-is. The current code uses Stock contracts. IBKR supports options and futures through different contract types in ib_async, but the strategy logic, sizing, and exit ladder would all need redesigning for those instruments.

Can I use another broker (Robinhood, Schwab, TastyTrade)? Not without replacing the IBKR client. Most retail brokers either don’t have an API or have very different ones. IBKR’s API is the most mature for retail algo trading. If you must use another broker, rewrite src/ibkr_client.py for that broker’s API.

Going live

When should I switch from paper to live? After at least 2 weeks of paper trading where the bot’s results match your expectations. Specifically: daily Telegram summaries make sense, positions in open_positions.json match TWS exactly, force-close fires reliably at 3:51 ET, and you’ve had at least one bug-free week. Don’t rush this.

How do I actually switch to live? Change IBKR_PORT to 7496 and PAPER_TRADING=false in .env. Adjust your PORTFOLIO_VALUE_USD and MAX_TRADE_SIZE_USD to match your live account. The paper/live port guard will refuse to start if the flag and port don’t agree, which is intentional.

Does this work with IBKR Lite (commission-free) vs IBKR Pro? Yes. The API connection is identical. The only practical difference is that IBKR pays for order flow on Lite, so live fill prices may be slightly worse on small orders. For paper trading there’s no difference at all.

Maintenance

How do I update the S&P 500 ticker list? Run the universe prompt (Prompt 6) again and tell Claude to compare the current src/sp500_tickers.py against the latest Wikipedia list and produce a diff. Aim to refresh every 2-3 months. Stale ticker lists cause yfinance errors when delisted symbols show up.

Do I need coding experience?

Not at all… I personally have 0 coding experience. Claude handles the coding for you. Your job is to understand trading and risk management.


Final Thoughts

This AI trading bot is a starting point, not a finished product.

The real edge comes from:

  • refining your strategy
  • improving filters
  • monitoring behavior
  • understanding risk management

Claude accelerates development dramatically, but strategy quality still matters more than automation.

Part 3 will cover how Claude analyzed my trading process and helped generate strategy ideas automatically.

Want my weekly market recap and watchlist?

You can sign up for FREE

Compliance: All trades shown are in a paper account. Nothing in this post is investment advice. I am not a registered financial advisor. FTC disclosure: Interactive Brokers is a partner.

The post How to Build an AI Trading Bot with Claude Code and Interactive Brokers appeared first on Humbled Trader.

admin