Build an offline control test before a trading bot

AI Trading Ranked

Last Updated: 2026-10-26

This is a runnable paper-only control fixture, not a ready-to-trade bot. It sends no orders, reads no account and needs no API key. Its purpose is to demonstrate three failures that a restart loop can otherwise hide: unfinished data, duplicate events and a loss halt that disappears when the process restarts. The Python example uses only the standard library.

Separate the jobs before choosing a provider

Scroll sideways to see every column. Keyboard: focus the table region and use the arrow keys.

LayerInput and outputQuestion the test must answer
DataTimestamped observationsIs this bar closed, fresh and from the intended market?
SignalVersioned rule and available dataWas the signal knowable before the assumed fill?
RiskEquity, exposures and pending ordersWhat stops new actions, and does that state survive restart?
ExecutionIntent, request ID and venue responseWas an order rejected, filled, partially filled or left unknown?
ReconciliationOrders, fills and balancesDoes recorded state agree with the venue after interruption?

The fixture below covers only part of the risk/data boundary. It does not implement a signal or execution layer. A moving average crossing is not evidence of a profitable strategy; custom code does not create an advantage merely by being custom.

Run a reproducible local example

Save the code as paper_control.py and run python paper_control.py. The demonstration creates its own temporary synthetic state file. Expected statuses are recorded_only, halted, halted; orders_sent is zero in every result. It records a fictional $1,000 peak, observes $890 and retains the halt when a later sample says $1,100.

"""Offline, single-process teaching fixture. No exchange or order calls."""
import json
from decimal import Decimal, InvalidOperation
from pathlib import Path


def positive(value):
    if isinstance(value, bool):
        raise ValueError("boolean is not a numeric input")
    try:
        number = Decimal(str(value))
    except (InvalidOperation, ValueError):
        raise ValueError("invalid numeric input") from None
    if not number.is_finite() or number <= 0:
        raise ValueError("finite positive number required")
    return number


def inspect(path, sample, now):
    path = Path(path)
    state = json.loads(path.read_text()) if path.exists() else {
        "halted": False, "peak": "1000", "seen": []
    }
    # Corrupt/unknown saved state raises instead of silently resetting the halt.
    if (type(state.get("halted")) is not bool
            or not isinstance(state.get("seen"), list)
            or not all(isinstance(x, str) for x in state["seen"])):
        raise ValueError("invalid saved state")
    peak = positive(state["peak"])
    if state["halted"]:
        return {"status": "halted", "orders_sent": 0}
    stamp = sample.get("closed_at")
    if (type(now) is not int or type(stamp) is not int
            or not 0 <= now - stamp <= 10):
        return {"status": "stale_or_future", "orders_sent": 0}
    if sample.get("closed") is not True:
        return {"status": "bar_not_closed", "orders_sent": 0}
    event = sample.get("event_id")
    if not isinstance(event, str) or not event:
        return {"status": "missing_event_id", "orders_sent": 0}
    if event in state["seen"]:
        return {"status": "duplicate", "orders_sent": 0}
    equity = positive(sample.get("equity"))
    peak = max(peak, equity)
    state["peak"] = str(peak)
    # 10% is a toy boundary, not a recommended account-risk limit.
    state["halted"] = equity <= peak * Decimal("0.9")
    state["seen"].append(event)
    temporary = path.with_name(path.name + ".pending")
    temporary.write_text(json.dumps(state))
    temporary.replace(path)
    # A failed write raises before any accepted result is returned.
    return {"status": "halted" if state["halted"] else "recorded_only",
            "orders_sent": 0}


if __name__ == "__main__":
    import tempfile
    with tempfile.TemporaryDirectory() as folder:
        saved = Path(folder) / "synthetic-state.json"
        base = {"closed": True, "closed_at": 100}
        print(inspect(saved, dict(base, event_id="a", equity="1000"), 101))
        print(inspect(saved, dict(base, event_id="b", equity="890"), 102))
        # A new call reloads the persisted halt even after equity recovers.
        print(inspect(saved, dict(base, event_id="c", equity="1100"), 103))

The ten-second freshness allowance and 10% boundary are arbitrary fixture values. They are not a trading schedule or suitable loss limit. A later process call loads the halt from disk; restarting the program's decision function does not clear it. The small demonstration uses a new temporary folder each run so it cannot touch an existing account record.

Test refusals, not only the happy path

Try an open bar, a timestamp in the future, an old timestamp and the same event ID twice. Each should produce a named refusal without a new action. Nonfinite or nonpositive equity must fail validation. A malformed saved state must raise, rather than becoming a fresh account. If the file cannot be written, the function raises before reporting a recorded decision.

The example assumes a single process and a single writer. It is not a transactional event ledger, does not bound state growth and does not establish power-loss durability or concurrent-writer safety. It has no reset API. A real operating procedure would require a deliberate, recorded reconciliation before clearing a halt; a service's Restart=always setting is not that procedure.

What remains before any execution integration

Do not convert a fixed number of paper days into automatic permission to increase capital. Record the actual integration, operational and strategy evidence still missing. Hosting, data, monitoring and maintenance can cost money and time even when the code is free. This guide provides no expected date of profitability.

For the reader-level order and loss arithmetic, see the beginner reference. For a separate prediction-market example, the paper payoff checker validates stated inputs without forecasting an outcome.

Educational engineering example. No live trading system, account protection guarantee or investment recommendation is supplied. AITR may earn commissions from other commercial links on the site; that does not validate a bot.