Module 5 — Algorithmic trading · Lesson 20 of 23 · 7 min
From idea to rules: writing a testable specification
How to turn a discretionary concept into a formal specification: measurable definitions, pseudocode, event ordering, parameters, edge cases and unit tests.
The hardest step in algorithmic trading isn't programming: it's deciding exactly what you want to program. A discretionary concept ("a strong move after a sweep") must become a formal specification with no ambiguity. In this lesson you learn to write it, to avoid the most common logic errors and to test it even before running a backtest.
From hypothesis to specification document
Start from a one-sentence hypothesis, then develop it into a document with five sections:
- Hypothesis: "After the sweep of the Asian low during the London killzone, if a bullish MSS with displacement appears and price returns into the FVG created, price tends to reach the Asian high before hitting the stop".
- Definitions: every term as a formula.
- Entry, exit, management, risk rules.
- Parameters with default values and allowed ranges.
- Execution assumptions (costs, slippage, event order).
Measurable definitions
Every concept translates into numerical conditions on candles:
| Concept | Formal definition (example) |
|---|---|
| ATR | 14-period moving average of the True Range |
| Swing high (n=3) | high[i] is the highest among candles i−3…i+3; known at candle i+3 |
| Asian range | min and max of low/high between 00:00 and 06:00 UTC; known from 06:00 |
| Sweep of the low L | low < L and close ≥ L and (L − low) ≥ 0.2 × ATR |
| Displacement | abs(close − open) > 1.5 × ATR(14) |
| Bullish MSS | close > last confirmed swing high and displacement |
| Bullish FVG | low[i] − high[i−2] > 0.3 × ATR (zone [high[i−2], low[i]]) |
| Return into the FVG | low ≤ CE = (lower_zone + upper_zone) / 2 within 20 candles of the MSS |
The pseudocode
Before writing code, write the logic as a state machine. Every day starts from scratch:
STATE = "idle"
for each closed candle t of the day:
if STATE == "idle":
if sweep_of_low(t) and t in killzone:
STATE = "wait_MSS"; store sweep_low
if STATE == "wait_MSS":
if low(t) < sweep_low or elapsed_candles > 12: STATE = "idle"
if bullish_MSS(t) and a valid FVG exists in the last 3 candles:
STATE = "wait_return"; store FVG_zone
if STATE == "wait_return":
if low(t) < sweep_low or elapsed_candles > 20: STATE = "idle"
if low(t) <= CE(FVG_zone) and candle_t is bullish:
LONG signal (entry at the open of candle t+1)
stop = sweep_low - margin ; target = entry + RR * risk
STATE = "idle"Advantages of the state machine: every condition is explicit, the order of events is clear, and it's easy to write tests that verify one transition at a time.
Order of events inside a candle
A thorny problem: a candle has a high and a low, but you don't know which happened first. If in the same candle price touches both the stop and the target, what happened? Prudent rules:
- Stop and target in the same candle → the stop wins (pessimistic assumption: the backtest shouldn't be optimistic).
- Entry and stop in the same candle (the entry candle immediately touches the stop): counts as a loss.
- If you have lower-resolution data (M1 inside an M5) you can rebuild the real order: more precise but heavier.
"No look-ahead" rules: a checklist
Before every backtest, for each feature ask yourself: at which candle is this information really known?
- A swing with n candles per side is known only n candles after the high/low.
- An FVG is known at the close of the third candle.
- The Asian range is known at the end of the Asian session.
- The day's high/low is known only at the end of the day (during the day you only have the high so far).
- An indicator calculated on the forming candle changes until the candle closes: use only closed candles.
- Entry happens at the open of the candle after the signal, with spread.
Parameters: few, with defaults
| Parameter | Default | Range for the robustness test |
|---|---|---|
| n (swing) | 3 | 2-5 |
| displacement | 1.5 × ATR | 1.2-1.8 |
| minimum FVG size | 0.3 × ATR | 0.2-0.5 |
| minimum sweep wick | 0.2 × ATR | 0.1-0.3 |
| max MSS candles | 12 | 8-16 |
| target RR | 2.0 | 1.5-3.0 |
Don't optimise looking for the "best" value: use the range to verify robustness (the result must stay similar).
Edge cases to decide beforehand
- Weekends and gaps: what happens if the signal is generated on Friday evening?
- Clock changes: define everything in UTC.
- Missing data: a missing candle can create a false FVG.
- Overlapping multiple signals: one position at a time? If so, what do you do with a signal that arrives while a position is open?
- Stop too wide or too tight: filter out trades with risk in pips above a maximum (for example > 3 × ATR) or below a minimum.
- Abnormal spread: ignore signals when the spread exceeds a threshold.
Testing the specification: unit tests
Before the backtest, verify that each function does what you say with small hand-built cases. Example for the FVG:
import pandas as pd
from lib_structure import find_fvg
def test_bullish_fvg():
# C1: high 1.1000 | C2: strong candle | C3: low 1.1012 -> bullish FVG [1.1000, 1.1012]
df = pd.DataFrame({
"open": [1.0990, 1.1002, 1.1015],
"high": [1.1000, 1.1030, 1.1030],
"low": [1.0985, 1.1001, 1.1012],
"close": [1.0998, 1.1028, 1.1025],
})
fvg = find_fvg(df)
assert bool(fvg["bull"].iloc[2]) is True
assert abs(fvg["bull_low"].iloc[2] - 1.1000) < 1e-9
assert abs(fvg["bull_high"].iloc[2] - 1.1012) < 1e-9
def test_no_fvg_when_ranges_overlap():
df = pd.DataFrame({
"open": [1.1000, 1.1005, 1.1008],
"high": [1.1010, 1.1020, 1.1022],
"low": [1.0995, 1.1000, 1.1005],
"close": [1.1005, 1.1015, 1.1018],
})
assert not find_fvg(df)["bull"].iloc[2]For swings too, write a test: a known sequence of highs in which you know exactly which indices must be recognised, and check that confirmation occurs n candles later. These tests protect you when you modify the code.
Typical specification mistakes
- Ambiguous rules that everyone interprets their own way.
- Conditions that depend on the future without noticing.
- Too many parameters or values "tweaked" on the data.
- Forgetting costs or assuming perfect executions.
- Not handling edge cases (gaps, missing data).
- Not versioning the specification: when you change a rule, you must be able to compare results with the previous version.
In short
- Write a specification document: hypothesis, definitions, rules, parameters, execution assumptions.
- Translate every concept into formulas on candles and the logic into a state machine.
- For every piece of information ask when it's really known (no look-ahead) and use only closed candles.
- Event order within a candle: when in doubt, the stop wins.
- Use few parameters with defaults and test the functions with hand-built cases.
Practical exercise
- Write the full specification of the sweep-MSS-FVG model on one page (definitions, states, parameters).
- For each definition state at which candle the information becomes known.
- Write three unit tests (swing, FVG, sweep) with hand-built data.
Test what you've learned
1. Why use a state machine for the pseudocode?
2. If stop and target are touched in the same candle, what's assumed?
3. When is a swing with n candles per side known?
4. What are unit tests for?
Have a question or want to share your exercise?
Post in the community, or join the free signals room on Telegram.
Educational content, not financial advice. Trading involves risk. ICT is a term referring to the materials of Michael Huddleston: this course is independent and not affiliated.