Stage 4 of 4, lesson 31 of 31. Improver
Build a command line tool that summarises one instrument
Turn a script into a small tool: name an instrument when you run it and it prints a summary from the practice API, says plainly when it cannot, and tells the terminal whether it worked.
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 handling an error reply and on settings. An internet connection. No broker account. No key.
So far, changing what a script does meant editing it. A tool takes its instructions on the command line. Save this one as summary.py and run python summary.py TIDE, and the word TIDE reaches the program as an argument.
argparse, which comes with Python, reads the arguments. You describe what you expect, here one instrument that may be left out, and it gives you the value, a help page when you run the tool with -h, and a clear message when the arguments make no sense.
The tool asks the API which instruments exist before it asks for a price. If you name one that is not in the list, it prints the list and stops. If the instrument is listed as closed, it says so and stops. Checking against the API's own list beats a guess, and it turns a puzzling failure into a sentence.
The work is inside try and except, so that the failures a network can produce end in one plain line and not a page of traceback: the server refused, the server could not be reached, or the reply was not what was expected.
A tool also answers the terminal. main returns a number and sys.exit hands it over: 0 for success, 2 for an instrument it cannot summarise, 1 for a failure. Other programs, and you in a script of your own, can act on that number without reading the words.
The asserts near the top test the two small sums on made up values before any request is made. The functions are separate from the printing so that they can be tested this way.
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
# A small command line tool: give it an instrument and it prints a summary from the practice API.
# Run it as: python summary.py TIDE With no instrument it summarises HBR100.
# 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, and it only reads.
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
from decimal import Decimal
BASE_URL = os.environ.get("PRACTICE_API", "https://openbrokerapi.com/mock/v1")
PAUSE = 1 # seconds to leave between calls
def get(path):
request = urllib.request.Request(
BASE_URL + path,
headers={"Accept": "application/json", "User-Agent": "command-line-summary-lesson"},
)
with urllib.request.urlopen(request, timeout=10) as reply:
data = json.load(reply, parse_float=Decimal)
if data.get("practice") is not True:
raise ValueError("the reply to " + path + " is not practice data")
return data
def spread(price):
"""The gap between the price you can buy at and the price you can sell at."""
return price["offer"] - price["bid"]
def closing_range(candles):
"""The lowest and the highest close in a list of candles."""
closes = [candle["close"] for candle in candles]
return min(closes), max(closes)
# The check, on made up values, before any request is made.
assert spread({"bid": Decimal("99.5"), "offer": Decimal("100.0")}) == Decimal("0.5")
assert closing_range([{"close": 3}, {"close": 1}, {"close": 2}]) == (1, 3)
def summarise(wanted):
"""Print the summary. Returns 0 if it could, and 2 if the instrument cannot be summarised."""
listed = {item["id"]: item for item in get("/instruments.json")["instruments"]}
if wanted not in listed:
print(wanted, "is not an instrument here. Choose from:", ", ".join(sorted(listed)))
return 2
instrument = listed[wanted]
print(instrument["id"] + ":", instrument["name"])
print("Type:", instrument["type"] + ",", "priced in", instrument["currency"] + ",", "smallest size", instrument["minSize"])
if instrument["status"] != "OPEN":
print("Status:", instrument["status"] + ".", "No price to show.")
return 2
time.sleep(PAUSE)
price = get("/prices/" + wanted + ".json")["price"]
print("Bid:", price["bid"], "Offer:", price["offer"], "Spread:", spread(price))
print("Today: low", price["low"], "high", price["high"], "change", price["change"], "(" + str(price["changePercent"]) + " percent)")
time.sleep(PAUSE)
history = get("/history/" + wanted + ".json")
low, high = closing_range(history["candles"])
print("Closes over", history["count"], "days: lowest", low, "highest", high)
print("Check passed: every reply said it was practice data.")
return 0
def main(arguments):
parser = argparse.ArgumentParser(description="Summarise one instrument from the practice API. Invented data.")
parser.add_argument("instrument", nargs="?", default="HBR100", help="an instrument id, for example TIDE")
options = parser.parse_args(arguments)
try:
return summarise(options.instrument.upper())
except urllib.error.HTTPError as refusal:
print("The server refused: status", refusal.code, "for", refusal.url)
except (urllib.error.URLError, TimeoutError) as problem:
print("Could not reach the practice API:", problem)
except ValueError as problem:
print("Could not read the reply:", problem)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))What you should see
HBR100: Harbour 100 (invented index) Type: index, priced in GBP, smallest size 0.5 Bid: 5057 Offer: 5058 Spread: 1 Today: low 5040.5 high 5062 change 11.5 (0.23 percent) Closes over 10 days: lowest 5008.5 highest 5060.5 Check passed: every reply said it was practice data.
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.
The output above came from running the tool with no instrument. Why did it summarise HBR100?
The argument is declared with nargs set to a question mark and a default of HBR100. That makes it optional, and the default is used when it is left out.
You run python summary.py tide, in lower case. Does it work?
Yes. main passes options.instrument.upper() to summarise, so tide becomes TIDE before it is looked up in the list of instruments.
QUILL is in the instrument list with a status of CLOSED. How many requests does the tool make for it?
One, for /instruments.json. summarise prints the name and the status, then returns 2 before the lines that ask for a price and a history.
Exercise
Add a second optional argument, days, a whole number with a default of 5, and make the closing range cover only that many of the most recent days. Running the tool with no arguments should now describe the last 5 days, not all 10.
What the changed script prints
HBR100: Harbour 100 (invented index) Type: index, priced in GBP, smallest size 0.5 Bid: 5057 Offer: 5058 Spread: 1 Today: low 5040.5 high 5062 change 11.5 (0.23 percent) Closes over the last 5 days: lowest 5033.5 highest 5060.5 Check passed: every reply said it was practice data.
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.
# A small command line tool: give it an instrument and it prints a summary from the practice API.
# Run it as: python summary.py TIDE With no instrument it summarises HBR100.
# 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, and it only reads.
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
from decimal import Decimal
BASE_URL = os.environ.get("PRACTICE_API", "https://openbrokerapi.com/mock/v1")
PAUSE = 1 # seconds to leave between calls
def get(path):
request = urllib.request.Request(
BASE_URL + path,
headers={"Accept": "application/json", "User-Agent": "command-line-summary-lesson"},
)
with urllib.request.urlopen(request, timeout=10) as reply:
data = json.load(reply, parse_float=Decimal)
if data.get("practice") is not True:
raise ValueError("the reply to " + path + " is not practice data")
return data
def spread(price):
"""The gap between the price you can buy at and the price you can sell at."""
return price["offer"] - price["bid"]
def closing_range(candles):
"""The lowest and the highest close in a list of candles."""
closes = [candle["close"] for candle in candles]
return min(closes), max(closes)
# The check, on made up values, before any request is made.
assert spread({"bid": Decimal("99.5"), "offer": Decimal("100.0")}) == Decimal("0.5")
assert closing_range([{"close": 3}, {"close": 1}, {"close": 2}]) == (1, 3)
def summarise(wanted, days):
"""Print the summary. Returns 0 if it could, and 2 if the instrument cannot be summarised."""
listed = {item["id"]: item for item in get("/instruments.json")["instruments"]}
if wanted not in listed:
print(wanted, "is not an instrument here. Choose from:", ", ".join(sorted(listed)))
return 2
instrument = listed[wanted]
print(instrument["id"] + ":", instrument["name"])
print("Type:", instrument["type"] + ",", "priced in", instrument["currency"] + ",", "smallest size", instrument["minSize"])
if instrument["status"] != "OPEN":
print("Status:", instrument["status"] + ".", "No price to show.")
return 2
time.sleep(PAUSE)
price = get("/prices/" + wanted + ".json")["price"]
print("Bid:", price["bid"], "Offer:", price["offer"], "Spread:", spread(price))
print("Today: low", price["low"], "high", price["high"], "change", price["change"], "(" + str(price["changePercent"]) + " percent)")
time.sleep(PAUSE)
history = get("/history/" + wanted + ".json")
recent = history["candles"][-days:]
low, high = closing_range(recent)
print("Closes over the last", len(recent), "days: lowest", low, "highest", high)
print("Check passed: every reply said it was practice data.")
return 0
def main(arguments):
parser = argparse.ArgumentParser(description="Summarise one instrument from the practice API. Invented data.")
parser.add_argument("instrument", nargs="?", default="HBR100", help="an instrument id, for example TIDE")
parser.add_argument("days", nargs="?", type=int, default=5, help="how many recent days to cover")
options = parser.parse_args(arguments)
try:
return summarise(options.instrument.upper(), options.days)
except urllib.error.HTTPError as refusal:
print("The server refused: status", refusal.code, "for", refusal.url)
except (urllib.error.URLError, TimeoutError) as problem:
print("Could not reach the practice API:", problem)
except ValueError as problem:
print("Could not read the reply:", problem)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))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 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.
- Retry and backoffDone
- Stay under a rate limitDone
- Handle an error replyDone
- Idempotent ordersDone
- Log what happenedDone
- Log your API callsDone
- Backtest an ideaDone
- The look ahead mistakeDone
- Settings from environmentDone
- Test with saved repliesDone
- A command line toolDoneYou are here
- The capstone projectDone