Free engineering tools

Engineering note · Writing the brief

Write Trading Bot Acceptance Criteria Before Code

A trading bot can match the code specification and still miss what you meant. The gap often starts in the brief. Acceptance criteria written before code exists decide what counts as done while changes are still cheap. This guide shows how to freeze four fields: starting state, trigger, exact result and evidence. You get one bounded example, three checks to copy, a revision boundary and the honest limits.

What freezing means, and why it happens before code

Acceptance criteria are the tests delivery must pass. Write them before you hire, or at least before the first line of code. Freezing means three things: the text is written, both sides hold the same copy, and changes go through a named boundary instead of a chat message. Brainstorm freely before the freeze. Once frozen, every change is either a correction to the written brief or a new feature, covered below.

The four fields to freeze

1. Starting state. The exact situation before the bot acts. Keep two things separate here. Configuration is what loads from a settings file: symbol, timeframe, indicator periods, endpoints. Persisted runtime state is what the bot stores itself while running, for example the last bar it alerted. An exported settings file proves configuration. It never proves runtime state. Name the persisted values and ask for them in logs or a state export. Also fix history depth, open positions and platform version. A good test of this field: a second developer could rebuild the same start from your note alone.

2. Trigger. One new event, named exactly. One trigger, one check. If a sentence hides three events, split it into three requirements.

3. Exact result. What must happen and what must not. A positive case states the single action and its identity. A quiet case states zero outbound actions and names the branch that stays silent.

4. Evidence. What the developer files for each case, so you can verify without watching a screen. Name it now, not after delivery.

A worked example, frozen before build

This example is hypothetical. Nothing was run and no bot was built for it. It shows one rule written the way a buyer freezes it, with numbers where numbers belong.

Rule: on EURUSD, M15, the bot watches each closed bar. When the closed bar’s 10-period EMA is strictly above its 30-period EMA, and the preceding bar’s fast EMA was strictly below its slow EMA, the bot sends one webhook alert naming that closed bar. One alert per closed bar. Later ticks in the same bar change nothing. A restart does not resend.

Frozen configuration: EURUSD, M15, fast EMA 10, slow EMA 30, endpoint signal-receiver-A, history depth 60 closed bars. Use a fixed replay fixture with declared EMA values and UTC bar-open timestamps. Equality does not count as a cross. This sheet tests alert decisions; EMA initialization and network-failure recovery need separate cases before production.

Persisted runtime state: one field, last alerted bar, the open time of the last closed bar that produced an alert. Starting value: none.

Hypothetical acceptance matrix. No execution result is claimed.
CaseStarting stateTriggerExact resultEvidence to file
Positive: alert fires onceRuntime state: last alerted bar, none. Bar 12:45 just closed with the 10 above the 30; the 12:30 bar closed below. Configuration matches the frozen file.First tick of bar 13:00, so 12:45 is the just-closed bar.Exactly one alert naming bar 12:45. Runtime state becomes last alerted bar, 12:45. No order and no second alert.Log line with event identity and the evaluated rule; deterministic replay of the tick; settings file; webhook request ID.
Duplicate: same bar, second tickRuntime state: last alerted bar, 12:45. That alert already went out. Configuration unchanged.Another tick inside bar 13:00.Zero outbound actions. No alert, no order, no management step. Runtime state stays 12:45. The log shows the evaluated rule and the already-alerted branch.Log line with event identity and no action; deterministic replay; settings file.
Negative: condition falseLast alerted bar, none. On bar 12:45 the fast EMA is below the slow EMA.First tick of 13:00 with that fixed fixture.No webhook, no order; stored value remains none.Logged EMA inputs, false decision and empty outbound log; fixture and settings.
Restart: same bar after reloadSame start as the duplicate case: last alerted bar, 12:45. The platform restarts while bar 13:00 is still open.First tick after restart, still inside bar 13:00.Zero outbound actions. Runtime state reads 12:45 before and after the restart. The next alert can only come from a fresh cross on a later closed bar.Logs from before and after the restart showing the same persisted value; a state export or logged persisted value; settings file after restart.

Three checks to copy into your brief

1. Starting state check. Load the named settings file. Confirm symbol, timeframe, periods, endpoint and history depth match the brief. Record the persisted runtime value before any trigger. Pass means the start matches the written state exactly.

2. Trigger check. Feed the case one named event. A positive case must show the single action with its identity. A quiet case must show zero outbound actions plus a log line naming the evaluated rule. Pass means the observed result equals the exact result, with nothing extra.

3. Restart check. Restart mid-case, then feed the same trigger again. Pass means the persisted runtime value is identical before and after, and no repeat outbound action appears.

Warning signs the brief is not ready

  • A verb with nothing exact behind it, like manage trades well. Not every condition needs a number. Periods, thresholds and lot sizes are numbers. Symbol, timeframe and endpoint names are identities. Every condition needs an unambiguous comparison or named state, written down.
  • Grouped triggers. One sentence holding three events is three requirements.
  • A result with no quiet half. If silence matters, write which actions must not fire.
  • No named evidence. Without logs, a replay, the settings file or a persisted state proof, a case cannot close.

Evidence to name in the brief

Keep proof close to the behavior. Ask per case for platform logs that show event identity and the evaluated rule. Ask for a short deterministic replay another person can run. Ask for the settings file for configuration and the persisted runtime value, logged or exported, for state. If anything leaves the bot, add an external ticket or request ID so both sides can reconcile. A long screen recording without event identity proves little.

Revision and scope boundary

Freeze entry rules, position sizing, broker constraints, platform version and ownership of existing trades before implementation. These are separate scope fields, each with its own positive and negative cases on top of this sheet.

A revision corrects the build where it differs from the frozen brief. A new feature adds what the freeze never contained: another rule, another market, a time filter or a management step. A new feature gets its own brief, its own tests and a separately agreed price. Past acceptance does not reopen because a new feature arrived.

Limits

Passing these cases proves one bounded thing: initialization, one new event and recovery follow the frozen behavior, and handled work does not repeat. It does not prove profitability, favorable fills, low drawdown or challenge success. Broker fills, spread, slippage, platform updates and account history can still change live results. Strategy logic needs its own tests beyond this sheet. Acceptance proves the build matches the brief. It says nothing about markets.

A free next step

Use the free Blueprint intake at Free Blueprint intake to describe the intended behavior. Fill one row per requirement: starting state, trigger, exact result, evidence. Bring that sheet to your first developer talk. A precise brief can speed up quotes and reduce revision rounds.

Related guide: Tests to run at handoff

Trading risk disclosure

Futures and forex trading contains substantial risk and is not for every investor. An investor could potentially lose all or more than the initial investment. Risk capital is money that can be lost without jeopardizing one's financial security or lifestyle. Only risk capital should be used for trading, and only those with sufficient risk capital should consider trading. Past performance is not necessarily indicative of future results.

Hypothetical performance disclosure

Hypothetical performance results have many inherent limitations. No representation is being made that any account will or is likely to achieve profits or losses similar to those shown. There are frequently sharp differences between hypothetical performance results and the actual results subsequently achieved by any particular trading program. Hypothetical results are generally prepared with the benefit of hindsight, do not involve financial risk, and cannot completely account for the impact of financial risk in actual trading. Market conditions, the ability to withstand losses, and adherence to a trading program can all materially affect actual results.

NinjaTrader trademark disclosure

NinjaTrader® is a registered trademark of NinjaTrader Group, LLC. No NinjaTrader company has any affiliation with the owner, developer, or provider of the products or services described herein, or any interest, ownership or otherwise, in any such product or service, or endorses, recommends or approves any such product or service.

Testimonials, when shown, may not represent the experience of other clients and are not a guarantee of future performance or success. Virtual-currency trading carries additional risks. See the CFTC customer advisories. Read the full financial risk disclaimer.