Broker API HubMenu

Stage 4 of 4, lesson 27 of 31. Improver

Backtest a trading rule in plain Python

Turn a rule into code and run it over past prices, with costs, before it goes anywhere near an account. It introduces functions, state and the most common way a backtest lies.

Independent and unofficialCode 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.

You need: The lesson on reading prices from a file. No broker account.

A backtest asks one question: if I had followed this rule over these prices, what would have happened? It is the cheapest test there is, and it is also easy to fool yourself with.

The script follows a moving average crossover over the sample file of invented prices. The rule is only an illustration, and the result says nothing about any market. Read it for the method.

Two details carry the lesson. The rule decides using prices up to yesterday only, because a backtest that peeks at today's close to trade at today's close is measuring something you could never have done. And every trade pays a cost, because a rule that only works for free does not work.

The script also prints what simply holding would have made over the same period. Always compare against doing nothing. When you outgrow a hand written loop, the frameworks below do the bookkeeping for you.

Files for this lesson

The code

# Test a trading rule on past prices before risking anything.
# Uses sample-candles.csv from the lesson on reading prices from a file. The prices are invented, so the result means nothing
# about any real market. The point is the method.
import csv

closes = []
with open("sample-candles.csv", newline="") as f:
    for row in csv.DictReader(f):
        closes.append(float(row["close"]))


def average(values):
    return sum(values) / len(values)


FAST, SLOW = 5, 20
COST = 0.10          # points paid each time we trade, a stand in for the spread

holding = False
entry = 0.0
total = 0.0
trades = 0

for day in range(SLOW, len(closes)):
    # Only use prices up to yesterday to decide. Using today's close to trade at today's close is cheating.
    fast = average(closes[day - FAST:day])
    slow = average(closes[day - SLOW:day])
    price = closes[day]

    if not holding and fast > slow:
        holding, entry = True, price
    elif holding and fast < slow:
        total += price - entry - COST
        trades += 1
        holding = False

if holding:
    total += closes[-1] - entry - COST
    trades += 1

print("Trades:", trades)
print("Total points after costs:", round(total, 2))
print("Buy and hold over the same period:", round(closes[-1] - closes[SLOW], 2))

What you should see

Trades: 3
Total points after costs: 6.68
Buy and hold over the same period: 7.1

Run with Python 3.12 on Windows against the sample files on this page. No network and no account.

Check yourself

Answer each question from the code above before you open it. The answers were written from the script on this page and what it printed.

How does the slice closes[day - FAST:day] keep today's price out of the decision?

A slice stops just before its end position. Ending at day means the last close used is the one from the day before.

What does COST do to the result?

0.10 points is taken off every completed trade as a stand in for the spread. With three trades, the total is 0.30 points lower than it would be for free.

The rule made 6.68 points and holding made 7.1. What does that tell you?

On these invented prices, after costs, the rule made less than doing nothing. It tells you nothing about any real market. It shows why the comparison belongs in every backtest.

Exercise

Raise the cost of each trade from 0.10 to 0.50 points and run it again. Which lines change?

What the changed script prints
Trades: 3
Total points after costs: 5.48
Buy and hold over the same period: 7.1

Captured from a run of the solution on . Run with Python 3.12 on Windows against the sample files on this page. No network and no account.

One solution
# Exercise solution: the lesson script with the one change the exercise asks for.
# Test a trading rule on past prices before risking anything.
# Uses sample-candles.csv from the lesson on reading prices from a file. The prices are invented, so the result means nothing
# about any real market. The point is the method.
import csv

closes = []
with open("sample-candles.csv", newline="") as f:
    for row in csv.DictReader(f):
        closes.append(float(row["close"]))


def average(values):
    return sum(values) / len(values)


FAST, SLOW = 5, 20
COST = 0.50          # points paid each time we trade, a stand in for the spread

holding = False
entry = 0.0
total = 0.0
trades = 0

for day in range(SLOW, len(closes)):
    # Only use prices up to yesterday to decide. Using today's close to trade at today's close is cheating.
    fast = average(closes[day - FAST:day])
    slow = average(closes[day - SLOW:day])
    price = closes[day]

    if not holding and fast > slow:
        holding, entry = True, price
    elif holding and fast < slow:
        total += price - entry - COST
        trades += 1
        holding = False

if holding:
    total += closes[-1] - entry - COST
    trades += 1

print("Trades:", trades)
print("Total points after costs:", round(total, 2))
print("Buy and hold over the same period:", round(closes[-1] - closes[SLOW], 2))

Your progress

Next: The look ahead mistake

Your marks are kept in this browser only, in its local storage. They are not sent anywhere, there is no account, and they will not follow you to another browser or device. To remove them, use Clear my progress on the learning track page, or clear this site's data in your browser's settings. Marking needs JavaScript. With scripts off the track reads the same, without the ticks.

Where this sits in the path

Stage 4 of 4: Building things that survive. Failure, limits, repeated orders, logs, settings, tests and backtests that do not fool you: what separates a script from a tool.

  1. Retry and backoff
  2. Stay under a rate limit
  3. Handle an error reply
  4. Idempotent orders
  5. Log what happened
  6. Log your API calls
  7. Backtest an ideaYou are here
  8. The look ahead mistake
  9. Settings from environment
  10. Test with saved replies
  11. A command line tool
  12. The capstone project

The whole path, every stage

Repositories to read next

The full repository list

Back: Log your API callsNext: The look ahead mistake