From Idea to Testable Trading Rules
beginner
Where This Fits: From a Hunch to Something a Computer Can Run
In Module 1 you loaded price data, cleaned it, and computed returns and volatility. You now have a trustworthy series of prices. This lesson is the bridge between that data and an actual backtest: taking a vague trading hunch and rewriting it as rules precise enough that a program produces the same trades every single time it runs.
By the end of this lesson you can:
- Name the four slots every testable rule set must fill, plus the timing detail most beginners forget
- Write a moving average crossover as exact entry, exit, and sizing rules
- Spot ambiguous phrasing that a backtester cannot execute
- Turn those rules into pseudocode you can hand to the next lesson
The Four Slots Every Rule Set Must Fill
A strategy is not a feeling about the market. It is a decision procedure. Every testable rule set answers four questions, and a fifth that quietly ruins backtests when left out.
| Slot | The question it answers | Example |
|---|---|---|
| Universe | What am I allowed to trade? | SPY, daily bars, one instrument |
| Entry | What condition makes me buy? | 20-day SMA above 60-day SMA |
| Exit | What condition makes me sell? | 20-day SMA at or below 60-day SMA |
| Sizing | How much do I commit? | All equity, one position, no leverage |
| Timing | Which price, on which bar? | Decide at the close, fill at the next open |
That last row is where most beginner backtests go wrong, and we will come back to it.
The test for a finished rule is simple: if you cannot write it as a comparison of numbers, it is not yet a rule. "Buy when the stock is trending up" fails, because "trending up" has no number attached. "Buy when the 20-day average close is above the 60-day average close" passes, because two people, or two runs of the same script, will always agree.
Note: Sizing counts as a rule, not an afterthought. "Buy when the signal fires" leaves open whether you buy one share or your whole account. Those two versions are different strategies with completely different results.
Practice (capstone thread). Take this sentence and rewrite it into the five slots: "I want to buy when the market is going up and sell when it looks toppy."
Click to reveal the answer
A workable rewrite: universe = SPY daily bars; entry = close above the 200-day SMA; exit = close below the 200-day SMA; sizing = 100% of equity, long only; timing = evaluate at the close, fill at the next open. Any numeric version is acceptable. The point is that every vague word ("up", "toppy") has been replaced by an observable condition.
A Crossover, Written So a Machine Cannot Misread It
Here is the idea in its natural, human form: buy when the short-term average price rises above the long-term average, and sell when it falls back below.
It sounds complete. It is not. Which average? Simple or exponential? How many days? Which price series? Does "rises above" mean the day it crosses, or every day it stays above?
Here is the same idea, pinned down:
- Universe: SPY, daily bars, long-only, one position at a time.
- Fast average: 20-day simple moving average of the daily closing price.
- Slow average: 60-day simple moving average of the daily closing price.
- Signal state: the strategy wants to be long on any bar where fast > slow.
- Entry: if the state is long and you are flat, buy at the next bar's open.
- Exit: if the state is not long and you hold a position, sell at the next bar's open.
- Sizing: all available equity, whole shares, no leverage, no shorting.
- Warm-up: no trading before bar 60, because the slow average does not exist yet.
Notice the choice in steps 4 and 6: the rule is written as a state ("be long while fast > slow"), not as an event ("buy on the day of the crossover"). State-based rules are more forgiving. If one bar of data is missing, an event-based rule silently loses the trade forever, while a state-based rule simply re-checks the condition on the next bar.
flowchart TD
A["๐ Bar closes"] --> B["Compute fast SMA 20 and slow SMA 60"]
B --> C{"Warm-up finished?"}
C -->|No| D["โธ๏ธ No trade"]
C -->|Yes| E{"fast > slow?"}
E -->|Yes| F{"Already long?"}
F -->|No| G["๐ข Buy at next open"]
F -->|Yes| H["Hold"]
E -->|No| I{"Already long?"}
I -->|Yes| J["๐ด Sell at next open"]
I -->|No| K["Stay flat"]
Practice (capstone thread). Write the pseudocode for steps 5 to 7 as a loop over daily bars. Keep it short.
Click to reveal a workable version
for bar in range(60, len(prices)):
fast = sma(prices, bar, 20)
slow = sma(prices, bar, 60)
want_long = fast > slow
if want_long and position == 0:
order = ("BUY", next_open(bar))
elif not want_long and position > 0:
order = ("SELL", next_open(bar))
The exact syntax matters less than the shape: one condition, one comparison, and an order that points at the next bar.
Edge Cases That Decide Whether Your Backtest Is Honest
Five details separate a rule set that survives contact with real data from one that quietly cheats.
- Warm-up and NaN. The 60-day average is undefined for the first 59 bars. If you let a NaN slip into a comparison, Python may treat it as False, or raise, or silently produce a position. Skip those bars explicitly.
- Deciding on the same close you trade at. You cannot know today's closing price until the bar is finished, so filling your order at that same close assumes information you did not have. Decide at the close, execute at the next open.
- Ties. What if fast equals slow exactly? Pick a convention and write it down; the rules above use fast > slow for long and fast <= slow for exit, so equality means flat.
- Missing bars. Holidays, halts, and data gaps exist. Decide whether a missing day simply does not exist in your loop, and never forward-fill prices across long gaps.
- Fixed parameters. Write 20 and 60 down before you run anything. If you later change them, note it, because every tweak after seeing results nudges you toward the overfitting problems of lesson 2.3.
Practice (capstone thread). Find three ambiguities in this sentence: "Sell when the price drops a lot from the recent high."
Click to reveal the answer
(1) "drops a lot" has no threshold, for example 5% or 2 ATR. (2) "recent high" has no window, for example the highest close of the last 20 bars. (3) It does not say which price is measured, close or intraday low, or when the check happens. Any three concrete ambiguities with a proposed numeric fix are correct.
Cloze check
- A testable rule set must specify the universe, the _____ rule, the _____ rule, and the position size.
- In the 20/60 crossover, 20 and 60 are the _____ of the two simple moving averages.
- No signal exists before bar _____, because the slow average is undefined during the warm-up period.
- A state-based rule stays long while fast > slow, while an event-based rule only reacts on the _____ bar.
- To avoid using information you could not have had, decide at the close and fill at the _____.
Flashcard review deck
Front: Which four slots must every testable rule set fill? Back: Universe, entry, exit, and sizing (plus a timing convention).
Front: What makes a trading rule unambiguous? Back: A program can evaluate it to true or false with no human judgment.
Front: In the lesson's crossover example, what is the fast average? Back: The 20-day simple moving average of daily closes.
Front: What is the slow average in that example? Back: The 60-day simple moving average of daily closes.
Front: State-based rule vs event-based rule: what is the difference? Back: A state rule checks the condition every bar; an event rule reacts only on the crossover bar and misses the trade if that bar is lost.
Front: Why decide at the close but fill at the next open? Back: The closing price is unknown until the bar ends, so filling at that same close uses information you did not have.
Front: What is the warm-up period here, and why does it matter? Back: The first 59 bars, where the 60-day average is undefined and no valid signal exists.
Front: How does the lesson handle fast equal to slow? Back: Equality is not long: entry needs fast > slow, exit triggers on fast <= slow.
Front: Why fix the 20 and 60 parameters before running the backtest? Back: Changing them after seeing results is the start of overfitting; record any change.
Front: Which price series do the moving averages use? Back: The daily closing price, stated explicitly in the rules.
Front: What must a sizing rule state? Back: How much equity per position, whether leverage is used, and whether shorting is allowed.
Front: Why is 'buy when the stock is trending up' not a rule? Back: 'Trending up' has no numeric definition, so two runs can produce different trades.
Rules of Thumb Before You Write a Single Line of Code
- Write the rule in plain English first, then read it as if you were a stubborn computer. Every word you cannot turn into a number is a bug waiting to happen.
- Prefer state-based conditions over crossover events. They survive one missing bar; event rules do not.
- Keep the whole rule set on one page. If entry, exit, and sizing live in three different places, you will change one and forget the others.
- Common mistake: quietly changing the parameters after seeing a bad result. That is not debugging, that is fitting, and lesson 2.3 will show you how it destroys live performance.
- Rule of thumb: if a friend cannot reproduce your trades from your written rules alone, the rules are not finished.
What You Can Now Write Down
You started this lesson with a sentence about moving averages and ended with a numbered rule set: universe, 20/60 closing-price averages, a state condition, next-open execution, full-equity long-only sizing, and an explicit 60-bar warm-up. That is a specification a program can run, not an opinion about the market.
Next up: Building the Backtest: Fills, Slippage, and Fees. You have the rules; the next lesson gives them a realistic cost of doing business, so the equity curve you produce is one you could plausibly have earned.