Technique explained as code
Moving average crossover
Two averages of recent prices, one short and one long. The rule acts when the short one crosses the long one. Here it is as code you can run, with an honest account of how it fails.
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
A moving average is the average of the last so many closing prices, worked out again each day. A short one follows price closely. A long one follows it slowly.
A crossover rule compares the two. When the short average is above the long one the rule is in the market, and when it drops below, the rule is out. That is the whole technique.
The idea behind it
The thinking is that when recent prices are higher than older prices, something is pushing them up, and it may carry on for a while. The rule does not try to pick the turn. It waits for the averages to confirm that a move has already begun, and accepts being late in and late out.
It is usually the first rule anyone codes, which makes it a good place to learn the bookkeeping every later test needs: a signal built only from the past, a cost on each trade, and a comparison against doing nothing.
The code
The script reads the closing prices, works out both averages for each day from the days before it, and keeps a list of the result of every completed trade. The two lengths are example values chosen to be easy to follow.
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: ma_crossover.py
# Moving average crossover, written out in full.
# 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.
FAST = 10 # days in the short average
SLOW = 50 # days in the long average
COST = 0.10 # points paid on every completed trade, a stand in for the spread
holding = False
entry = 0.0
results = [] # the result of each completed trade, in points
for day in range(SLOW, len(closes)):
# Decide with prices up to yesterday only. closes[day - FAST:day] stops before today.
fast = average(closes[day - FAST:day])
slow = average(closes[day - SLOW:day])
price = closes[day] # then act at today's close
if not holding and fast > slow:
holding, entry = True, price
elif holding and fast < slow:
results.append(price - entry - COST)
holding = False
if holding: # count a position that is still open at the last price
results.append(closes[-1] - entry - COST)
wins = [r for r in results if r > 0]
print("Days of prices:", len(closes))
print("Trades:", len(results))
print("Trades that ended above zero:", len(wins))
print("Total points after costs:", round(sum(results), 2))
print("Simply holding over the same days:", round(closes[-1] - closes[SLOW] - COST, 2))What the run printed
Days of prices: 750 Trades: 15 Trades that ended above zero: 3 Total points after costs: -20.36 Simply holding over the same days: -16.72
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
Trades is how many times the rule got in and out again. Fifteen trades over 750 days is very few to judge anything by.
Three of the fifteen ended above zero. That shape is normal for this kind of rule: most trades are small losses, and the total depends on a handful of long ones. On this file those did not arrive.
The last two lines are the comparison that matters. The rule ended lower than simply holding did over the same days, after paying its costs. The prices are a random walk with nothing in them to follow, so a loss of roughly the costs and the lateness is what the arithmetic predicts. It says nothing about any real market.
How it fails or misleads
- Sideways prices. When price drifts back and forth across its averages, the rule buys just after a rise and sells just after a fall, again and again. Each round trip is a small loss plus a cost.
- It is late by design. An average of fifty days cannot react until a good part of a move has already happened, and it gives a good part back before it gets out.
- The result rests on very few trades. One long trade can decide the whole total, so two people testing slightly different dates can reach opposite conclusions from the same rule.
- The two lengths are free choices. Try enough pairs and one will look good on any file, including this random one. The page on how a backtest lies shows that happening.
- A test at the closing price assumes you could trade there. The cost in this script is a fixed stand in. Real costs vary and are wider when markets move fast.
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.
- Change FAST and SLOW and run it again. Write down how much the total moves for a small change in either. A result that swings widely with a small change is telling you it is mostly noise.
- Print each trade with the day it opened and closed, so you can see how long the rule held and where the total came from.
- Set COST to zero, then double it. How much of the total was cost?
- Rewrite the loop as a function that takes the two lengths and returns the list of results. You will need that shape for the page on how a backtest lies.
- Run the function on the first half of the prices and then the second half. Do the two halves agree?
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.