Stage 3 of 4, lesson 20 of 31. Improver
Turn a price history into moving averages and a CSV file
Fetch ten daily candles from the practice API, work out a three day and a five day simple moving average, write them to a CSV file a spreadsheet can open, and read the file back to prove it is right.
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 lessons on reading prices from a file, on the trade journal and on your first HTTP request. An internet connection. No broker account. No key.
Stage 2 worked out a moving average from a file you downloaded, and the journal lesson wrote a CSV. This lesson joins the two to a request: the prices arrive from an API, and the result leaves as a file.
A simple moving average is the mean of the last few closes. The function takes the list of closes, how many to average and which day it is working on. It slices out the closes that end on that day. Until there are enough closes it returns None, and the CSV writer turns None into an empty cell, which is the honest way to write a figure that does not exist yet.
Look at what the slice reads: that day and the days before it, never a later one. An average that includes a price from the future is the most common way a backtest fools its author. A later lesson in stage 4 is given over to that mistake.
The check reads the file back, which tests the thing you will use and not the list in memory. It counts the lines against the count in the reply, and it compares the last average in the file with one worked out a second way.
The averages are arithmetic to practise on. The prices are invented, and nothing in the output is a signal to act on.
The practice API this lesson calls
Practice data, not any broker's API: fixed files of invented values on this site. Each opens in a browser tab, so you can read what the script will be sent.
The code
# Fetch a price history from the practice API, work out two simple moving averages and write a CSV.
# The practice API is a set of fixed files of invented values. It is not any broker's API.
# It needs no account and no key. A moving average here is arithmetic to practise on, not a signal.
import csv
import json
import os
import urllib.request
from decimal import Decimal
BASE_URL = os.environ.get("PRACTICE_API", "https://openbrokerapi.com/mock/v1")
INSTRUMENT = "HBR100"
SHORT, LONG = 3, 5 # how many closes go into each average
PENNY = Decimal("0.01")
def get(path):
request = urllib.request.Request(
BASE_URL + path,
headers={"Accept": "application/json", "User-Agent": "price-history-to-csv-lesson"},
)
with urllib.request.urlopen(request, timeout=10) as reply:
data = json.load(reply, parse_float=Decimal)
assert data["practice"] is True, "this reply is not practice data"
return data
def moving_average(values, window, day):
"""The average of the `window` values ending on `day`. None until there are enough of them.
It reads today and earlier, never a later day."""
if day + 1 < window:
return None
chosen = values[day + 1 - window:day + 1]
return (sum(chosen) / window).quantize(PENNY)
history = get("/history/" + INSTRUMENT + ".json")
candles = history["candles"]
closes = [Decimal(candle["close"]).quantize(PENNY) for candle in candles]
rows = []
for day, candle in enumerate(candles):
rows.append({
"date": candle["date"],
"close": closes[day],
"sma" + str(SHORT): moving_average(closes, SHORT, day),
"sma" + str(LONG): moving_average(closes, LONG, day),
})
# Write the file. A cell with no average yet is left empty, which is what None becomes in a CSV.
file_name = INSTRUMENT.lower() + "-averages.csv"
with open(file_name, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
# The check. Read the file back: one line for each candle, and the last short average
# equal to one worked out the long way, straight from the last closes in the reply.
with open(file_name, newline="") as f:
saved = list(csv.DictReader(f))
assert len(saved) == history["count"], "the file does not hold one line for each candle"
by_hand = (sum(closes[-SHORT:]) / SHORT).quantize(PENNY)
assert Decimal(saved[-1]["sma" + str(SHORT)]) == by_hand, "the last average in the file is wrong"
print(history["instrumentName"] + ",", len(saved), "daily closes")
for row in saved:
print(row["date"], row["close"].rjust(8), row["sma" + str(SHORT)].rjust(8), row["sma" + str(LONG)].rjust(8))
print("Wrote", file_name, "with a header and", len(saved), "lines")
print("Check passed: the last", SHORT, "day average in the file is", by_hand)What you should see
Harbour 100 (invented index), 10 daily closes 2025-12-19 5021.50 2025-12-22 5036.00 2025-12-23 5024.00 5027.17 2025-12-24 5008.50 5022.83 2025-12-29 5041.00 5024.50 5026.20 2025-12-30 5060.50 5036.67 5034.00 2025-12-31 5052.00 5051.17 5037.20 2026-01-01 5033.50 5048.67 5039.10 2026-01-02 5046.00 5043.83 5046.60 2026-01-05 5057.50 5045.67 5049.90 Wrote hbr100-averages.csv with a header and 10 lines Check passed: the last 3 day average in the file is 5045.67
Run with Python 3.12 on Windows against a copy of the practice API files served over HTTP on the same machine, with PRACTICE_API set to the address of the copy. They are the files this site serves. No account and no key.
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.
Why do the first two lines have no three day average?
moving_average returns None when day + 1 is less than the window. On the first two days there are one and two closes, which is fewer than three. The CSV writer writes None as an empty cell.
The last three day average is 5045.67. Which closes is it made from?
The last three: 5033.50, 5046.00 and 5057.50. They add up to 15137, and a third of that, rounded to two places, is 5045.67. The check works it out the same way from closes[-SHORT:].
Does the average for a day ever use a close from a later day?
No. The slice runs from day + 1 - window up to, but not including, day + 1, so the last close it takes is that day's own.
Exercise
Change one line so that the script reads the history of LNTN, the invented share, and writes lntn-averages.csv. Nothing else needs to change, because the file name and the address are both built from INSTRUMENT.
What the changed script prints
Lantern Metals (invented share), 10 daily closes 2025-12-19 251.20 2025-12-22 250.10 2025-12-23 250.80 250.70 2025-12-24 249.60 250.17 2025-12-29 248.30 249.57 250.00 2025-12-30 249.40 249.10 249.64 2025-12-31 249.00 248.90 249.42 2026-01-01 247.40 248.60 248.74 2026-01-02 248.40 248.27 248.50 2026-01-05 248.20 248.00 248.48 Wrote lntn-averages.csv with a header and 10 lines Check passed: the last 3 day average in the file is 248.00
Captured from a run of the solution on . Run with Python 3.12 on Windows against a copy of the practice API files served over HTTP on the same machine, with PRACTICE_API set to the address of the copy. They are the files this site serves. No account and no key.
One solution
# Exercise solution: the lesson script with the one change the exercise asks for.
# Fetch a price history from the practice API, work out two simple moving averages and write a CSV.
# The practice API is a set of fixed files of invented values. It is not any broker's API.
# It needs no account and no key. A moving average here is arithmetic to practise on, not a signal.
import csv
import json
import os
import urllib.request
from decimal import Decimal
BASE_URL = os.environ.get("PRACTICE_API", "https://openbrokerapi.com/mock/v1")
INSTRUMENT = "LNTN"
SHORT, LONG = 3, 5 # how many closes go into each average
PENNY = Decimal("0.01")
def get(path):
request = urllib.request.Request(
BASE_URL + path,
headers={"Accept": "application/json", "User-Agent": "price-history-to-csv-lesson"},
)
with urllib.request.urlopen(request, timeout=10) as reply:
data = json.load(reply, parse_float=Decimal)
assert data["practice"] is True, "this reply is not practice data"
return data
def moving_average(values, window, day):
"""The average of the `window` values ending on `day`. None until there are enough of them.
It reads today and earlier, never a later day."""
if day + 1 < window:
return None
chosen = values[day + 1 - window:day + 1]
return (sum(chosen) / window).quantize(PENNY)
history = get("/history/" + INSTRUMENT + ".json")
candles = history["candles"]
closes = [Decimal(candle["close"]).quantize(PENNY) for candle in candles]
rows = []
for day, candle in enumerate(candles):
rows.append({
"date": candle["date"],
"close": closes[day],
"sma" + str(SHORT): moving_average(closes, SHORT, day),
"sma" + str(LONG): moving_average(closes, LONG, day),
})
# Write the file. A cell with no average yet is left empty, which is what None becomes in a CSV.
file_name = INSTRUMENT.lower() + "-averages.csv"
with open(file_name, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
# The check. Read the file back: one line for each candle, and the last short average
# equal to one worked out the long way, straight from the last closes in the reply.
with open(file_name, newline="") as f:
saved = list(csv.DictReader(f))
assert len(saved) == history["count"], "the file does not hold one line for each candle"
by_hand = (sum(closes[-SHORT:]) / SHORT).quantize(PENNY)
assert Decimal(saved[-1]["sma" + str(SHORT)]) == by_hand, "the last average in the file is wrong"
print(history["instrumentName"] + ",", len(saved), "daily closes")
for row in saved:
print(row["date"], row["close"].rjust(8), row["sma" + str(SHORT)].rjust(8), row["sma" + str(LONG)].rjust(8))
print("Wrote", file_name, "with a header and", len(saved), "lines")
print("Check passed: the last", SHORT, "day average in the file is", by_hand)Your progress
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.
This browser refused to store anything for this page, so marks cannot be kept here.
Where this sits in the path
Stage 3 of 4: Talking to a broker. What an API sends back, real requests to a practice API that needs no account, how to keep a key safe, a first read only call on a demo account, and histories that arrive in pages.
- Read an API responseDone
- First HTTP requestDone
- Positions and exposureDone
- Keys and demo accountsDone
- First API callDone
- Paged resultsDone
- Follow the next pageDone
- Price history to CSVDoneYou are here