Technique explained as code
Mean reversion to a moving average
Measure how far price has stretched from its own average and act as if it will come back. Code you can run, and an honest account of the one way this fails badly.
Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.
Teaching material only. This page explains how a technique works as code and how to test an idea honestly. It is not investment advice, not a recommendation and not an invitation to trade. Every price on it is invented, so no result here says anything about any real market, and nothing here suggests that a technique makes money.
What it is
Mean reversion is the opposite bet to following a trend. It measures how far the price is from its recent average and treats a large gap as something that will close.
In this version the rule gets in when price is more than three percent below its twenty day average, and gets out when price is back at the average.
The idea behind it
The thinking is that prices overshoot. A sharp fall brings in forced sellers and frightened ones, and once they are done the price drifts back towards where it was.
Whether a given market behaves like that is a question about that market. Some do some of the time. A random walk never does, and the file used here is a random walk, so this page can only show the mechanics.
The code
Besides the usual totals, this script records two things that matter for this technique in particular: how many days were spent in a trade, and how far under water an open trade went before it closed.
Data the script reads: invented-prices.csv. Invented values, never a real market. Save it in one folder and run the script from that folder.
The file that was run: mean_reversion.py
# Mean reversion to a moving average: act when price is stretched away from its average.
# Prices come from invented-prices.csv: a random walk made by a script, never a real market.
# The result therefore says nothing about any real market. Read it for the method.
import csv
closes = []
with open("invented-prices.csv", newline="") as f:
for row in csv.DictReader(f):
closes.append(float(row["close"]))
def average(values):
return sum(values) / len(values)
# Example values, picked only because they are easy to follow. They are not a suggestion.
DAYS = 20 # length of the average
STRETCH = 3.0 # how far below the average, in percent, counts as stretched
COST = 0.10 # points paid on every completed trade, a stand in for the spread
holding = False
entry = 0.0
results = []
days_held = 0
worst_while_open = 0.0 # the deepest any open trade went under water, in points
for day in range(DAYS + 1, len(closes)):
yesterday = day - 1
# The average ends at yesterday. Today's close is not in it.
mean = average(closes[yesterday - DAYS + 1:yesterday + 1])
gap = (closes[yesterday] - mean) / mean * 100
price = closes[day]
if holding:
days_held += 1
worst_while_open = min(worst_while_open, price - entry)
if not holding and gap < -STRETCH:
holding, entry = True, price # stretched below the average: get in
elif holding and gap >= 0:
results.append(price - entry - COST) # back at the average: get out
holding = False
if holding:
results.append(closes[-1] - entry - COST)
wins = [r for r in results if r > 0]
print("Trades:", len(results))
print("Trades that ended above zero:", len(wins))
if results:
print("Smallest and largest trade:", round(min(results), 2), "and", round(max(results), 2))
print("Days spent in a trade:", days_held, "of", len(closes) - DAYS - 1)
print("Deepest an open trade went under water:", round(worst_while_open, 2))
print("Total points after costs:", round(sum(results), 2))
print("Simply holding over the same days:", round(closes[-1] - closes[DAYS + 1] - COST, 2))What the run printed
Trades: 12 Trades that ended above zero: 5 Smallest and largest trade: -8.95 and 2.92 Days spent in a trade: 208 of 729 Deepest an open trade went under water: -11.18 Total points after costs: -3.71 Simply holding over the same days: -10.82
Output of the run on , copied as printed. Every input to this run was invented, so the result says nothing about any real market.
How to read it
Look at the smallest and largest trade before the total. The largest gain is a few points and the largest loss is about three times that. This is the signature of mean reversion: many small gains and an occasional large loss, because the rule has no exit other than price coming back.
The deepest an open trade went under water is larger still. A test that only reports closed trades hides this. Someone following the rule would have had to sit through it.
The total is below zero and happens to be above what holding did over the same days. That is because the rule was out of a falling series most of the time, not because stretched prices came back. In a random walk they do not. It says nothing about any real market.
How it fails or misleads
- The falling knife. Sometimes a price is far below its average because something has changed for good. The rule buys, the price keeps falling, the average follows it down, and the exit never comes at a gain.
- The losses are larger than the gains. A high share of winning trades means little here, because one loss can undo many gains.
- There is no stop in the rule as written. Adding one changes the technique: it turns some trades that would have recovered into losses. That trade off has to be tested, not assumed.
- It needs the market to be going nowhere. In a strong trend, price stays stretched for a long time and the rule is on the wrong side throughout.
- The stretch that counts as far enough is a free setting, and it is very easy to tune it to a past that will not repeat.
The page on how a backtest lies shows the mistakes that make any rule look better in a test than it is.
What to try changing
These are programming exercises. Each one changes the code and asks you to explain what you see. None of them is a search for settings to use.
- Add a stop: get out if the trade is more than a set number of points under water. Compare the largest loss and the total with and without it.
- Add a time limit: get out after ten days whatever has happened. How many trades does it cut short?
- Change STRETCH to 2 and to 4 and count the trades each time. Notice how quickly the number of trades falls.
- Record the gap on every day, not only on entry days, and print its largest and smallest values. That shows you what is normal for the file.
- Print the longest time any single trade was held.
How this page was made
Each script on this page was run with Python 3.12 on Windows, standard library only, from a folder holding the data files listed. No network and no account. Each exited with code 0, and what is shown is everything it printed.
The data files whose names begin with the word invented are written by make_invented_data.py from fixed seeds, so anyone can make the same files again and get the same output. This site is not published if the code or a data file has changed since the run.