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:

  1. 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".
  2. Definitions: every term as a formula.
  3. Entry, exit, management, risk rules.
  4. Parameters with default values and allowed ranges.
  5. Execution assumptions (costs, slippage, event order).

Measurable definitions

Every concept translates into numerical conditions on candles:

ConceptFormal definition (example)
ATR14-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 rangemin and max of low/high between 00:00 and 06:00 UTC; known from 06:00
Sweep of the low Llow < L and close ≥ L and (L − low) ≥ 0.2 × ATR
Displacementabs(close − open) > 1.5 × ATR(14)
Bullish MSSclose > last confirmed swing high and displacement
Bullish FVGlow[i] − high[i−2] > 0.3 × ATR (zone [high[i−2], low[i]])
Return into the FVGlow ≤ 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

ParameterDefaultRange for the robustness test
n (swing)32-5
displacement1.5 × ATR1.2-1.8
minimum FVG size0.3 × ATR0.2-0.5
minimum sweep wick0.2 × ATR0.1-0.3
max MSS candles128-16
target RR2.01.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

  1. Write the full specification of the sweep-MSS-FVG model on one page (definitions, states, parameters).
  2. For each definition state at which candle the information becomes known.
  3. 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.