Broker API HubMenu

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.

Independent and unofficialEducational onlyInvented data onlyCode run

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

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.

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.