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.
| Case | Starting state | Trigger | Exact result | Evidence to file |
|---|---|---|---|---|
| Positive: alert fires once | Runtime 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 tick | Runtime 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 false | Last 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 reload | Same 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