Built a Trading Bot With Claude Code or ChatGPT? A Pre-Live Checklist

Yes: Claude Code, ChatGPT and Codex can write a working trading bot, from strategy logic and backtests to the Interactive Brokers or MetaTrader 5 connection. What none of them can tell you is whether that code is safe to run with real money. Anthropic’s own guide notes that Claude “stops when the work looks done,” and in trading code the expensive mistakes, such as look-ahead bias, a duplicate order or a position the bot does not know about, look done too. This checklist turns the step from “it runs” to “it trades live” into 24 verifiable checks, with two tested Python examples, as of November 2026.

Can Claude Code or ChatGPT build a trading bot?

Technically, yes. Anthropic describes Claude Code as an agentic coding tool that reads your codebase, edits files and runs commands. OpenAI’s Codex works similarly in the Codex CLI, the IDE extension or Codex Cloud, and ChatGPT in the browser writes code that you paste and run yourself.

The weak point is verification. Claude Code’s best-practices guide separates guidelines the model may decide not to apply, such as prompts and CLAUDE.md, from hooks, deny rules, managed settings and tests, which are enforced every time. Every rule that must hold with real money therefore needs a check that returns pass or fail, plus a person who reads the order and risk code. To have that review done for you, see trading bot development and code review for Interactive Brokers and MetaTrader 5.

Why “it runs” is not the bar for AI-written trading code

Generated trading code is usually syntactically clean. The typical errors sit in the domain logic, and many make a backtest look better, so they go unnoticed without a dedicated check:

  • Data: a signal computed on the close and filled at that same close, centered rolling windows, scaling over the whole data set, bars labeled with their open time but used as if closed.
  • Costs: returns from price differences without commissions, spread and slippage.
  • Execution: an order sent twice after a timeout, partial fills treated as complete, prices off the minimum tick (IBKR error 110).
  • State: after a restart the bot assumes it is flat while the broker still holds a position.
  • Overfitting: an assistant can test hundreds of parameter sets in minutes. Bailey, Borwein, López de Prado and Zhu (2014) showed that the more configurations are tried, the higher the probability that the best backtest is overfit.

IBKR’s documentation adds a point for generated code: the TWS API contains no logic beyond the integrity of the exchanged messages; validation happens in TWS and on IBKR’s servers, so your code must bring its own limits.

The pre-live checklist: 24 checks

Each pass criterion can be verified without the assistant’s summary. Check the code and setup that will actually go live.

#Check (as of November 2026)Pass criterion
1Secrets: no live credentials near the assistantNo live key, password or token where the assistant works; a secret scan (OWASP names a pre-commit hook as one option) finds none in the repository or its git history.
2No order tools for the assistantNo MCP server or tool with order functions connected while coding (MetaTrader 5: terminal MCP server off, AI Assistant set to not trade); IBKR sessions the tools can reach use Read-Only API.
3Rules enforced in configurationDeny rules or hooks protect secrets and test files; sandbox on; full-access modes only in an isolated container without credentials.
4Human review of order, sizing and risk codeA person can explain every order path (entry, exit, stop, cancel, flatten) without the assistant.
5Dependencies pinnedPinned versions; every package the assistant added is maintained and used.
6Look-ahead biasPrefix test passes for every signal and filter on a long history; fills no earlier than the next bar or tick.
7Bar timestampsA test with constructed bars shows whether timestamps mark the open or the close.
8Costs in every simulated tradeA one-trade test returns the exact expected net result; a backtest without a cost model fails.
9OverfittingThe number of parameter sets tried is logged; final parameters are checked on data not used to choose them.
10Time zones and daylight saving timeTimes stored in UTC; session filters pass tests around clock changes in the exchange and server time zones.
11Units: size, multiplier, tick sizeRisk converts to quantity correctly per instrument type; prices round to the tick (IBKR minTick and market rules, MQL5 SYMBOL_TRADE_TICK_SIZE), volumes to SYMBOL_VOLUME_STEP.
12One signal, one orderIBKR order IDs strictly increase; replaying a signal after a timeout sends no second order.
13RestartAfter a hard stop with an open position and a working order, the restarted bot loads positions and open orders before sending anything.
14Position reconciliationBot and broker positions match at startup and on a schedule; a mismatch blocks new orders and raises an alert.
15Partial fills, rejections, cancellationsTests feed each case to the order handler; internal state matches the simulated broker afterward.
16DisconnectsSimulated IBKR code 1100 pauses new orders; after 1101 or 1102, orders resume only once positions and open orders are rechecked; after 1101, market data is requested again.
17Request rateA throttle stays below the documented limit (IBKR: market data lines divided by two per second, so 50 with the default 100 lines).
18Pre-trade limitsOrders above a maximum quantity or value, or outside a price band, are blocked in code, especially if TWS order precautions are bypassed.
19Daily loss limitReaching the limit stops new orders; the stop survives a restart.
20Kill switchOne action blocks new orders and cancels working ones, works without the strategy process, was triggered once in paper, and only a person clears it.
21Alerts and heartbeatA test alert reaches your phone; a missing heartbeat raises an alert.
22LoggingEach decision is logged in UTC with inputs, order ID, broker response and fills, without secrets, and stored off the bot’s machine.
23Paper phaseExit criteria written down before the start and met; sizing based on live capital, not the paper balance.
24Account guardThe bot checks account ID and type at startup and stops on a mismatch; going live is a deliberate change by a person.

How to test for look-ahead bias with a prefix check

Look-ahead bias means a signal uses information that did not exist at the time. A prefix test finds it without reading the code: compute signals on the full history and on truncated copies. A causal signal for bar 300 cannot change because later bars are missing; if it does, the code peeks ahead. The worked example compares a correct moving-average signal with a centered window, a typical leak:

"""Prefix test for look-ahead bias. Python 3 standard library only."""
import unittest

CLOSES = [100, 101, 99, 102, 106, 103, 105, 101, 100, 101, 108, 107]


def causal_signal(closes, n=3):
    """Signal at bar i uses closes[0..i] only: 1 = long, 0 = flat."""
    out = []
    for i in range(len(closes)):
        if i + 1 < n:
            out.append(None)
        else:
            out.append(1 if closes[i] > sum(closes[i + 1 - n:i + 1]) / n else 0)
    return out


def centered_signal(closes, n=3):
    """Typical bug: a centered window reads one bar into the future."""
    half, out = n // 2, []
    for i in range(len(closes)):
        window = closes[max(0, i - half):i + half + 1]
        out.append(1 if closes[i] > sum(window) / len(window) else 0)
    return out


def first_difference(signal_fn, closes, min_bars=4):
    """Recompute on every truncated history; return (cut, bar) of the first change."""
    full = signal_fn(closes)
    for cut in range(min_bars, len(closes)):
        for bar, (a, b) in enumerate(zip(full[:cut], signal_fn(closes[:cut]))):
            if a != b:
                return cut, bar
    return None


class LookAheadTest(unittest.TestCase):
    def test_causal_signal_passes(self):
        self.assertIsNone(first_difference(causal_signal, CLOSES))

    def test_centered_window_is_caught(self):
        self.assertIsNotNone(first_difference(centered_signal, CLOSES))


if __name__ == "__main__":
    print("centered window, first difference (cut, bar):", first_difference(centered_signal, CLOSES))
    unittest.main()

The example runs with Python 3.9 and 3.12, standard library only; both tests pass, and first_difference returns (4, 3) for the centered window: bar 3 gets a different signal with four bars than with the full history. The test only catches leaks that change a signal in the data you feed it, so run it on a long real history. Have the assistant write it before the strategy code, and protect test files with a deny rule such as Edit(./tests/**) so a red test cannot be “fixed” by editing it; with the sandbox on, the rule also binds scripts the assistant runs.

How to reconcile positions and stop on a mismatch

Reconciliation compares what the bot believes it holds with what the broker reports. With the TWS API, reqPositions() returns the positions of all accessible accounts, ends with positionEnd() and then streams changes; in MQL5, PositionsTotal() and the position functions serve the same purpose. The worked example wraps every order in a persistent kill switch, a size limit and daily order and loss limits; a position mismatch trips the switch, which blocks all orders until a person clears it:

"""Reconciliation, pre-trade limits and a persistent kill switch.
Python 3 standard library only. StubBroker replaces the broker API:
no connection, no orders leave this process."""
import json
import os
import tempfile

# Production: a fixed path that survives restarts; only a person deletes it.
KILL_FILE = os.path.join(tempfile.mkdtemp(), "kill_switch.json")


class StubBroker:
    def __init__(self, positions):
        self._positions = dict(positions)  # e.g. collected via reqPositions() up to positionEnd()
        self.sent = []

    def positions(self):
        return dict(self._positions)

    def place_order(self, symbol, qty):
        self.sent.append((symbol, qty))  # recorded only


def kill(reason):
    with open(KILL_FILE, "w") as f:
        json.dump({"reason": reason}, f)


def kill_reason():
    if not os.path.exists(KILL_FILE):
        return None
    with open(KILL_FILE) as f:
        return json.load(f)["reason"]


def reconcile(expected, broker):
    actual = broker.positions()
    diff = {s: (expected.get(s, 0), actual.get(s, 0))
            for s in sorted(set(expected) | set(actual))
            if expected.get(s, 0) != actual.get(s, 0)}
    if diff:
        kill(f"position mismatch (bot, broker): {diff}")
    return diff


def guarded_order(broker, symbol, qty, limits, today):
    if kill_reason():
        return "blocked: " + kill_reason()
    if abs(qty) > limits["max_qty"]:
        return "blocked: size above max_qty"
    if today["orders"] >= limits["max_orders_per_day"]:
        kill("daily order count reached")
        return "blocked: order count"
    if today["pnl"] <= -limits["max_daily_loss"]:
        kill("daily loss limit reached")
        return "blocked: daily loss"
    broker.place_order(symbol, qty)
    today["orders"] += 1
    return "sent to stub"


if __name__ == "__main__":
    limits = {"max_qty": 200, "max_orders_per_day": 20, "max_daily_loss": 500.0}
    today = {"orders": 0, "pnl": 0.0}
    broker = StubBroker({"AAPL": 100})
    print(guarded_order(broker, "AAPL", 10, limits, today))   # sent to stub
    print(guarded_order(broker, "AAPL", 900, limits, today))  # blocked: size
    broker = StubBroker({"AAPL": 110, "MSFT": 50})           # restart: unknown MSFT position
    print(reconcile({"AAPL": 110}, broker))                  # {'MSFT': (0, 50)}
    print(guarded_order(broker, "AAPL", 10, limits, today))   # blocked: position mismatch
    print("orders after the restart:", broker.sent)          # []

It runs with Python 3.9 and 3.12 against a stubbed broker: no broker connection, no orders. In a real bot, the kill-switch file sits at a fixed path outside the code directory, daily P&L comes from the broker, and the checks run in a process separate from the strategy, so a strategy bug cannot skip them.

How to keep the AI assistant away from the live account

The OWASP Top 10 for LLM applications lists excessive agency as a risk and recommends human approval for high-impact actions; a live order is one. The rule behind this checklist: the assistant writes and tests code against stubs and recorded data, gets no broker credentials and places no orders, not even in paper; you start the bot, first in paper, later live. Current tool defaults make this a deliberate setup step (as of November 3, 2026):

  • Claude Code: from v2.1.283, auto mode is the starting mode in the terminal and VS Code, and its classifier by default allows “Reading .env and sending credentials to their matching API.” Read deny rules such as Read(./.env) do not cover a Python script that opens the file itself unless the sandbox is on; it is off by default and, once on, by default still lets commands read most of the machine and inherit secrets from environment variables.
  • Codex: in version-controlled folders the recommended Auto preset is workspace-write with on-request approvals and no network access; danger-full-access removes these limits.
  • MetaTrader 5: the built-in AI Assistant has a Trading option (execute trades, request explicit confirmation, or not trade at all), and the terminal MCP server, which clients such as Claude Code or Codex can use, has tools for placing and managing orders. “Enable internal server” switches only that terminal server, not the separate metaeditor server for MQL5 coding. For development: a demo login, the terminal server off and “not trade at all.”
  • Interactive Brokers: IBKR’s TWS API setup page lists disabling “Read-Only API” among the key connection settings; a third-party guide republished on IBKR Campus suggests enabling it so orders are not executed accidentally while testing. Keep it on for every TWS or IB Gateway session your development tools can reach.

More on assistants and broker connections: IBKR MCP with Claude or ChatGPT: what works and what does not.

What the paper phase proves, and what it does not

Paper trading tests the plumbing under real market data: connection, order handling, reconciliation, logging and alerts. It does not test real fills. IBKR’s paper trading guide lists differences, among them:

  • fills are simulated from the top of the book, with no deep book access;
  • stops and other complex order types are always simulated;
  • the remainder of an exchange-directed market order that partially executes is rejected;
  • every paper account starts with USD 1,000,000 in paper equity, so a percent-of-equity sizer can trade far larger than it will live.

For MetaTrader 5, run the Strategy Tester with “Every tick based on real ticks,” which uses tick data accumulated by brokers, then a demo account. Before the paper phase starts, write down exit criteria: a number of sessions fixed in advance, zero unexplained position differences, one deliberate kill-switch trigger, one restart with an open position, one disconnect, and paper fills compared with the backtest for the same days. An Expert Advisor can also refuse an unexpected account; this OnInit() guard is illustrative and was not compiled, so check it against the MQL5 reference before use:

// Illustrative, not compiled: check against the MQL5 reference before use.
input bool AllowNonDemoAccount = false; // set to true only after the demo phase
input long ExpectedLogin       = 0;     // 0 = no check; otherwise the only allowed account

int OnInit()
  {
   if(MQLInfoInteger(MQL_TESTER))
      return(INIT_SUCCEEDED);

   ENUM_ACCOUNT_TRADE_MODE mode = (ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE);
   if(mode != ACCOUNT_TRADE_MODE_DEMO && !AllowNonDemoAccount)
     {
      Print("Account is not a demo account and AllowNonDemoAccount is false. EA stops.");
      return(INIT_FAILED);
     }
   if(ExpectedLogin != 0 && AccountInfoInteger(ACCOUNT_LOGIN) != ExpectedLogin)
     {
      Print("Unexpected account ", AccountInfoInteger(ACCOUNT_LOGIN), ". EA stops.");
      return(INIT_FAILED);
     }
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || !MQLInfoInteger(MQL_TRADE_ALLOWED)
      || !AccountInfoInteger(ACCOUNT_TRADE_EXPERT))
      Print("Automated trading is disabled in the terminal, the EA settings or on the server.");
   return(INIT_SUCCEEDED);
  }

MetaTrader 5 can also disable automated trading when switching accounts, an option its help describes as useful when moving from a demo to a real account.

Limits of the checklist, and what to do next

Passing all 24 checks lowers the risk of technical failure. It says nothing about whether the strategy works, and automated trading can lose money; with leverage, depending on the product, more than the deposit. For investment firms engaged in algorithmic trading, RTS 6 requires a kill functionality (Article 12) and pre-trade controls such as price collars, maximum order values and volumes and message limits (Article 15); for a private bot they are a useful benchmark.

A practical order of work:

  1. Write the prefix, cost and reconciliation tests first, and protect them from edits by the assistant.
  2. Lock the setup: no live credentials, deny rules, sandbox, no order tools.
  3. Read the order and risk code yourself or have it reviewed.
  4. Run the paper phase against written exit criteria, then go live with reduced size.

Whether to build it yourself with AI or hand it over is weighed in what a trading bot costs: hire a developer or build it with AI; an independent review of an existing bot or Expert Advisor also covers code written with ChatGPT or Claude.

Frequently asked questions

Can Claude Code write a trading bot?

Yes. Claude Code can write strategy logic, backtests and broker connections for Interactive Brokers or MetaTrader 5, and run the tests itself. Whether the bot is correct is decided by tests that return pass or fail and by a person who reads the order and risk code. The assistant should not place orders or hold live credentials; you start the bot, first in paper, later live.

Can I build a trading bot with ChatGPT?

ChatGPT can write the code, and OpenAI’s Codex can also edit files and run commands in your project. The result needs the same checks as any other bot: look-ahead and cost tests, position reconciliation, a kill switch and a paper phase with written exit criteria. Whether a strategy makes money is a separate question that no code generator answers.

Should I give Claude or ChatGPT access to my brokerage account?

No. OWASP recommends human approval for high-impact actions, and a live order is one. Keep broker credentials off the machine the assistant works on, connect no MCP server with order tools while coding, set the MetaTrader 5 AI Assistant to not trade, and keep IBKR sessions that development tools can reach in Read-Only API mode. The bot, started by you, talks to the paper account first.

How do I test a trading bot for look-ahead bias?

Use a prefix test. Compute all signals on the full history, then on truncated copies of the same history, and compare the values before each cut. A causal signal does not change when later bars are missing; any difference means the code uses future data. Run it on a long real history, because a leak only shows up where it changes a signal.

How long should a trading bot run in paper trading before going live?

No fixed duration fits every strategy. Set exit criteria before you start: a fixed number of sessions, zero unexplained position differences, one deliberate kill-switch trigger, one restart with an open position and one disconnect. Keep in mind that paper fills are simulated; IBKR states that they come from the top of the book, so live execution can differ, especially for larger orders.

Is Claude Code’s auto mode safe for a trading bot repository?

Auto mode lets a classifier review actions instead of you. According to Anthropic it blocks risky steps such as sending sensitive data to external endpoints, but by default it allows reading .env files and sending credentials to their matching API. Keep live credentials out of the repository and machine, add deny rules for secrets and test files, enable the sandbox and review order and risk code yourself.

Sources and status

As of November 2026. Third-party rules, prices and APIs may have changed since; the providers' current documentation and the official texts are authoritative.

Built a bot with Claude Code or ChatGPT and want it checked before it goes live? Get in touch — a free intro call clarifies the scope of a review: order and risk code, tests, reconciliation and the paper phase. More: Trading bot development for Interactive Brokers and MetaTrader 5.