# ChatGPT trading bot: what breaks and how to test the code

URL: https://fincodelabs.com/en/blog/chatgpt-trading-bot · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Trading bots · Published: 2026-11-06 · Updated: 2026-11-06

A ChatGPT trading bot can place an order and still fail on lost replies, partial fills and restarts. The faults to check, with a tested Python harness.

**Short answer:** A **ChatGPT trading bot** is ordinary trading code drafted by a chat model: in a plain chat it can write the plumbing, but it cannot test against your account, see your positions or make your rules profitable. Such code can work while every reply arrives, so test for lost replies, partial fills, restarts and exchange filters. Run it through fault injection on a fake exchange and then a testnet before real money; tests show how software behaves, not future results.

A first order on a testnet proves little, whoever wrote the code. The trouble starts afterward: one lost reply can double an order while the script reports success.

Every code pattern here is a typical pattern, hand-written illustration, not captured from a chat; no claim is made about what a named model does. Fincode has no affiliation with OpenAI. This is general education, not a recommendation of any exchange or strategy, and automated trading can lose money.

## Can ChatGPT write a trading bot that makes money?

It can write the code, and nothing shows that this makes the rules profitable, because profit is not a property of code.

Regulators treat the sales pitch as a warning sign: the CFTC's advisory of 25 January 2024 (release 8854-24) says, "AI technology can't predict the future or sudden market changes," and the joint SEC, NASAA and FINRA article of the same date calls claims of high guaranteed investment returns with little or no risk classic warning signs of fraud.

**What a plain chat can do for a bot**

| Task | In a plain chat | What to do |
| --- | --- | --- |
| Draft a REST client, retry loop or config | Yes, as a first draft | Read it and test it |
| Know today's endpoints and filters | Not unless you paste them | Paste dated extracts from official documents |
| Run the code against an exchange | No | Run it yourself on a testnet |
| Know your balance, open orders or fills | No | Read them from the exchange at startup |

In Perry et al. (CCS 2023), participants using an assistant based on codex-davinci-002 wrote significantly less secure code yet were more likely to believe it secure. Read that older study as a caution about confidence, not a measure of current models.

## How to create a trading bot with ChatGPT: what to expect

Plan for a happy-path draft and no more: a config block, an API wrapper, a polling loop, an indicator function and a place-order call are enough to see one order on a testnet.

Check first for the parts that let software run unattended: durable state, an order ID chosen before the request, rounding to the venue's filters, reconciliation at startup, limits outside the strategy code and alerts. [Trading bot development](https://fincodelabs.com/en/blog/trading-bot-development) covers that architecture. A workable order: write your own rules, ask for the plumbing only, read the code against the table below, run the harness, then a testnet.

## Eight faults to look for in generated bot code

Most of these faults stay hidden while every request succeeds, so a one-order demo misses them.

**Typical patterns, hand-written illustrations, each with its fix**

| Fault | Before | After |
| --- | --- | --- |
| Outdated names | `filters["MIN_NOTIONAL"]` | Read live `exchangeInfo`; stop if a filter you need is absent |
| Invented package | `pip install` whatever the answer names | Check its PyPI page and repository first, then pin the version |
| Float sizes | `math.floor(0.3 / 0.1) * 0.1` | `D("0.3").quantize(step, ROUND_DOWN)` |
| No saved client order ID | `place(new_id(), qty)` in a retry loop | Save one ID before sending; look it up before any resend |
| Partial fills ignored | `position += qty` | Set position from the executed quantity the exchange reports |
| State lost on restart | `position = 0` at startup | Read balances and open orders at startup; stop on a mismatch |
| Clock drift | `recvWindow=60000` as the cure | Compare with server time at startup, fix the clock, alert |
| Secrets in code or chat | `API_SECRET = "..."`, or a key pasted into a prompt | Secret store, testnet keys in development, revoke anything pasted |

- **Outdated names.** On 5 November 2026 Binance's live `exchangeInfo` listed 3,742 Spot symbols, 1,372 of them trading, with `NOTIONAL` on every one and `MIN_NOTIONAL` on none, while its filter document still describes both. Code looking up the older name finds nothing, and a default silently skips the check.
- **Invented packages.** In Spracklen et al. (USENIX Security 2025), 576,000 code samples from 16 models averaged at least 5.2% hallucinated packages for commercial models and 21.7% for open-source ones. OWASP's LLM09 describes attackers registering such names with malicious code, so an existing name still needs checking. PyPI's JSON API shows whether it exists and its release history.
- **Float sizes.** In Python, `math.floor(0.3 / 0.1) * 0.1` returns 0.2, one step short, and `0.3 % 0.1` returns 0.09999999999999998, so a float version of the documented `quantity % stepSize == 0` check refuses a valid size.
- **Clock drift.** Binance's timing rules, read 5 November 2026, reject a timestamp 1 second or more ahead of server time whatever the `recvWindow`, and recommend 5,000 ms or less. In the fake below, a 60,000 ms window still rejected a clock 2 seconds ahead and accepted a 6-second-old request. See [Binance API error codes](https://fincodelabs.com/en/blog/binance-api-error-codes) for `-1021`.

## A tested fault-injection harness in Python

A fake exchange that loses a reply on demand shows the doubled order and the fix, without keys or network.

`FakeExchange` encodes rules from Binance's Spot documents, read 5 November 2026: the timestamp check, `LOT_SIZE`, precision and client IDs unique among open orders.

```python
from decimal import Decimal as D, ROUND_DOWN
from uuid import uuid4

class LostReply(Exception): pass   # accepted by the venue, reply never arrives
class Rejected(Exception): pass
class Stop(Exception): pass        # refuse to trade and alert a person

class FakeExchange:
    def __init__(self, lost_replies=0, fill="1"):
        self.lost, self.fill = lost_replies, D(fill)
        self.book, self.now = [], 1_000_000
        self.step, self.max_qty = D("0.001"), D("100000")  # minQty = step

    def place(self, cid, qty, ts, window=5000):
        if not (ts < self.now + 1000 and self.now - ts <= window):
            raise Rejected("-1021 timestamp")
        q = D(qty)
        if not q.is_finite():
            raise Rejected("-1100 illegal characters")
        if -q.as_tuple().exponent > 8:
            raise Rejected("-1111 precision")
        if not self.step <= q <= self.max_qty or q % self.step:
            raise Rejected("-1013 LOT_SIZE")
        if any(o["cid"] == cid and o["open"] for o in self.book):
            raise Rejected("ID in use by an open order")
        filled = q * self.fill
        order = {"cid": cid, "filled": filled, "open": filled < q}
        self.book.append(order)
        if self.lost:
            self.lost -= 1
            raise LostReply
        return order

    def query(self, cid):              # all orders with this ID
        return [o for o in self.book if o["cid"] == cid]

def typical_buy(ex, ledger, qty, ts, cid=None, tries=3):
    """Typical pattern, hand-written illustration."""
    for _ in range(tries):
        try:
            ex.place(cid or uuid4().hex, str(qty), ts)
            ledger["position"] = ledger.get("position", 0) + qty
            return
        except LostReply:
            continue

def checked_buy(ex, store, qty, ts, tries=3):
    if abs(ts - ex.now) > 500:         # illustrative limit, use server time
        raise Stop("clock drift")
    q = D(str(qty))                    # inf, nan, zero and negatives stop here
    if not (q.is_finite() and ex.step <= q <= ex.max_qty):
        raise Stop("size outside LOT_SIZE")
    size = str(q.quantize(ex.step, ROUND_DOWN))
    # Saved before the first send; the store survives restarts.
    intent = store.setdefault("intent", {"cid": uuid4().hex, "qty": size})
    if not intent["qty"] == size:
        raise Stop("earlier order unresolved")
    for _ in range(tries):
        found = ex.query(intent["cid"])
        if len(found) > 1:
            raise Stop("one ID, several orders")
        if not found:
            try:
                found = [ex.place(intent["cid"], intent["qty"], ts)]
            except LostReply:
                continue
        # Save both changes in one write.
        store["position"] = store.get("position", 0) + found[0]["filled"]
        del store["intent"]            # the next buy gets a new ID
        return
    raise Stop("order status unknown")

def scenario(bot, lost=0, fill="1", qty=1.0, skew=0, restart=False, **kw):
    ex, state, note = FakeExchange(lost, fill), {}, ""
    try:
        if restart:                    # the process dies awaiting the first reply
            try:
                bot(ex, state, qty, ex.now + skew, tries=1)
            except Stop:
                pass
            if bot is typical_buy:
                state = {}             # memory is gone; the checked store is a file
        bot(ex, state, qty, ex.now + skew, **kw)
    except (Rejected, Stop) as e:
        note = f" {e}"
    filled = sum(o["filled"] for o in ex.book)
    return f'{len(ex.book)} / {filled:.3f} / {float(state.get("position", 0)):.3f}{note}'

cases = [("reply lost, new ID", dict(lost=1), {}),
         ("reply lost, same ID resent", dict(lost=1), {"cid": "fixed"}),
         ("40% partial fill", dict(fill="0.4"), {}),
         ("float size 0.1 * 3", dict(qty=0.1 * 3), {}),
         ("restart after send", dict(lost=1, restart=True), {}),
         ("clock 2 s ahead", dict(skew=2000), {})]
print(f'{"fault":28}{"typical":35}checked')
for name, c, extra in cases:
    print(f"{name:28}{scenario(typical_buy, **c, **extra):35}{scenario(checked_buy, **c)}")
```

> **Output on Python 3.11.3, 5 November 2026, synthetic data.** Each result reads orders / filled at the venue / position the bot believes.
>
> ```
> fault                       typical                            checked
> reply lost, new ID          2 / 2.000 / 1.000                  1 / 1.000 / 1.000
> reply lost, same ID resent  2 / 2.000 / 1.000                  1 / 1.000 / 1.000
> 40% partial fill            1 / 0.400 / 1.000                  1 / 0.400 / 0.400
> float size 0.1 * 3          0 / 0.000 / 0.000 -1111 precision  1 / 0.300 / 0.300
> restart after send          2 / 2.000 / 1.000                  1 / 1.000 / 1.000
> clock 2 s ahead             0 / 0.000 / 0.000 -1021 timestamp  0 / 0.000 / 0.000 clock drift
> ```

- **Row 2** is the subtle one: Binance's documentation makes `newClientOrderId` unique only among open orders, so after a fast fill a blind resend of the same ID doubles the order too. Look it up first; [client order ID](https://fincodelabs.com/en/blog/client-order-id) rules differ by venue.
- **Row 3:** the bot would try to sell 1.000 while holding 0.400.

This tests your logic against one reading of the documents, not the venue. Binance says HTTP 5XX and `-1007` leave execution status unknown, and a testnet cannot lose a reply on demand, so keep the fake and add testnet runs. [Crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration) covers general retry design.

## What to give the model, and what never to paste

Give the model your rules, dated document extracts and test requirements, and never give it keys or ask it to choose the rules.

- **Your written rules:** entry, exit, size and failure behavior, in your words.
- **Dated extracts:** the order endpoint, filters and error codes you use, from official pages.
- **Test requirements:** one test per fault-table row, with expected values from the documents.
- **An environment rule:** testnet host only, and key names rather than values.

Never paste live keys or account statements: OWASP's LLM02 lists credentials among the data at risk, and a prompt leaves your machine. If one was pasted, revoke it; see [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots). Do not ask what to trade or which settings to use: a model's suggestion is not evidence of anything.

With an agent mode or a connected trading tool, a model holding a trade-enabled key can send any order that key allows, including one prompted by text it read. Give it testnet keys only and keep limits in code it cannot edit; see [crypto trading MCP servers](https://fincodelabs.com/en/blog/crypto-trading-mcp-server), and [Claude trading bot](https://fincodelabs.com/en/blog/claude-trading-bot) for agents with shell access. Read your AI provider's usage policies and the exchange's API terms before a model decides any order.

## Review checklist before a generated bot trades

Treat generated code as a stranger's pull request and pass each item before any live key exists.

1. Every import is the project you expected, with its version pinned.
2. Every endpoint and field appears in today's official documents.
3. Sizes and prices are Decimal, rounded to the live step and tick, sizes downward.
4. Position comes from reported fills, and a restart stops on any mismatch.
5. Order, position and loss limits sit outside the strategy code, as in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).
6. No secret appears in code, logs or chat, and the key has no withdrawal right.
7. Every harness row passes, testnet logs exist, and you can explain each failure path.

A second reader helps when real money is close and you cannot explain how the code handles a timeout, a partial fill or a restart. Fincode sells [trading bot code review](https://fincodelabs.com/en/services/trading-bot-code-review), so that suggestion is not neutral; the checklist works with any competent reviewer.

Rules depend on your jurisdiction and each exchange's terms. Binance is named only for its public documents, which is not a recommendation and does not mean Fincode integrates it or has built on it. For readers in Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.

## Frequently asked questions

### Can ChatGPT create a crypto trading bot for me?

It can draft the code. You still read it, test it, host it and carry the result of every order it sends.

### Can AI write profitable trading bots?

It can write the code, and nothing shows that this makes the rules profitable. The CFTC warns that AI cannot predict the future.

### What is vibe coding a trading bot?

Accepting generated code without reading it. With real orders, an unread retry loop can double a position.

### Should I paste my API key into ChatGPT?

No. A prompt leaves your machine, so use key names rather than values, and revoke any live key that was pasted.

### Why did my bot place the same order twice?

Usually a retry after a lost reply. Save the client order ID before sending and look it up before any resend.

## Sources

- [Perry et al., Do Users Write More Insecure Code with AI Assistants? (arXiv 2211.03622, CCS 2023)](https://arxiv.org/abs/2211.03622)
- [Spracklen et al., We Have a Package for You: package hallucinations by code-generating LLMs (arXiv 2406.10279, USENIX Security 2025)](https://arxiv.org/abs/2406.10279)
- [OWASP Gen AI Security Project: LLM09:2025 Misinformation (unsafe code generation, invented packages)](https://genai.owasp.org/llmrisk/llm092025-misinformation/)
- [OWASP Gen AI Security Project: LLM02:2025 Sensitive Information Disclosure](https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/)
- [CFTC Release 8854-24, 25 January 2024, with the Customer Advisory: AI Won't Turn Trading Bots into Money Machines](https://www.cftc.gov/PressRoom/PressReleases/8854-24)
- [SEC, NASAA and FINRA: Artificial Intelligence (AI) and Investment Fraud, 25 January 2024 (FINRA copy)](https://www.finra.org/investors/insights/artificial-intelligence-and-investment-fraud)
- [Binance Spot API documentation: Filters (LOT_SIZE, NOTIONAL, MIN_NOTIONAL)](https://github.com/binance/binance-spot-api-docs/blob/master/filters.md)
- [Binance Spot API documentation: Error codes (-1007, -1013, -1021, -1111)](https://github.com/binance/binance-spot-api-docs/blob/master/errors.md)
- [Binance Spot API documentation: REST API (timing security, newClientOrderId, HTTP 5XX)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Spot exchangeInfo, live filters via the market data host, read 5 November 2026](https://data-api.binance.vision/api/v3/exchangeInfo)
- [PyPI: JSON API documentation](https://docs.pypi.org/api/json/)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/chatgpt-trading-bot

Cite as: Fincode, "ChatGPT trading bot: what breaks and how to test the code", updated November 6, 2026, https://fincodelabs.com/en/blog/chatgpt-trading-bot
