Broker API HubMenu

Stage 3 of 4, lesson 15 of 31. Beginner

Read your positions and work out exposure by instrument

Fetch the open positions from the practice API, work out how much of each market they hold, and check your own sums against what the API reports. It introduces grouping with a dictionary over live replies.

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 lessons on your first HTTP request and on money with Decimal. An internet connection. No broker account. No key.

A list of positions answers the question what do I hold. Exposure answers a more useful one: how much of each market is that. For one position it is the size times the price it would close at. In the practice API one unit of size is worth one pound for each point of price. A real broker defines size in its own way for each instrument, so read its documents before you trust a figure like this on a real account.

The script counts a BUY as positive and a SELL as negative, then adds the positions up instrument by instrument in a dictionary, as the journal lesson did for markets. Gross exposure ignores direction and says how much is at work. Net exposure lets a BUY and a SELL offset each other.

Prices are read as Decimal, not float, by passing parse_float to json.load. That is the habit from the lesson on money, applied at the moment the numbers arrive.

The check is the part to copy into your own work. The API reports a running profit for each position, and the account summary reports the total. The script works both out again from the levels and stops if they disagree. When your sums and the server's sums match, you have understood the data. When they do not, you have found either a bug or something about the API you had not read yet.

It makes two requests, with a pause between them. Every value is invented, and nothing here is a view on what to hold.

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.

Every address on the practice API, and what it cannot do

The code

# Read the open positions from the practice API and work out the exposure on each instrument.
# 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 cannot place a trade.
import json
import os
import time
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": "positions-and-exposure-lesson"},
    )
    with urllib.request.urlopen(request, timeout=10) as reply:
        data = json.load(reply, parse_float=Decimal)   # prices as exact decimals, not floats
    assert data["practice"] is True, "this reply is not practice data"
    return data


def closing_price(position):
    """A BUY is closed by selling at the bid. A SELL is closed by buying at the offer."""
    return position["bid"] if position["direction"] == "BUY" else position["offer"]


def exposure(position):
    """Size times price: how much of the market the position holds. BUY counts up, SELL counts down."""
    value = position["size"] * closing_price(position)
    return value if position["direction"] == "BUY" else -value


def running_profit(position):
    """Points moved in your favour since the position was opened, times size."""
    if position["direction"] == "BUY":
        return (position["bid"] - position["openLevel"]) * position["size"]
    return (position["openLevel"] - position["offer"]) * position["size"]


positions = get("/positions.json")["positions"]

# Group by instrument with a dictionary: the key is the instrument, the value is its running total.
by_instrument = {}
for position in positions:
    name = position["instrumentId"]
    by_instrument[name] = by_instrument.get(name, Decimal("0")) + exposure(position)
    print(position["positionId"], name, position["direction"], position["size"], "at", closing_price(position))

gross = sum(abs(value) for value in by_instrument.values())
print("Exposure by instrument, in GBP:")
for name in sorted(by_instrument):
    share = round(100 * abs(by_instrument[name]) / gross, 1)
    print(" ", name, by_instrument[name], "which is", share, "percent of the gross")
print("Gross exposure:", gross)
print("Net exposure:", sum(by_instrument.values()))

# The check. What we work out from the levels must agree with what the API reports,
# position by position and then against the account summary, which is a second call.
for position in positions:
    assert running_profit(position) == position["profitLoss"], position["positionId"]
time.sleep(PAUSE)
account = get("/account.json")["account"]
total = sum(running_profit(position) for position in positions)
assert total == account["profitLoss"], "positions and account disagree"
print("Check passed: running profit of", total, "GBP agrees with the account summary.")

What you should see

P-1001 HBR100 BUY 2 at 5057
P-1002 LNTN SELL 50 at 248.33
Exposure by instrument, in GBP:
  HBR100 10114 which is 44.9 percent of the gross
  LNTN -12416.50 which is 55.1 percent of the gross
Gross exposure: 22530.50
Net exposure: -2302.50
Check passed: running profit of 67.50 GBP agrees with the account summary.

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 is the exposure for LNTN a negative number?

The position is a SELL. exposure works out size times the closing price, 50 times the offer of 248.33, and returns it with a minus sign for a SELL so that it counts down when positions are added together.

Gross exposure is 22530.50 and net exposure is -2302.50. Why are they so different?

Gross adds the size of each figure and ignores its sign: 10114 plus 12416.50. Net adds them as they are, so the BUY and the SELL offset each other and what is left is the difference.

How many requests does the script make, and what happens between them?

Two: one for /positions.json and one for /account.json. The line time.sleep(PAUSE) waits one second before the second request.

Exercise

The practice API also lists working orders, which are not positions yet. After the net exposure, pause, fetch /orders.json and print what the exposure on each instrument would be if every working order were filled at its level.

What the changed script prints
P-1001 HBR100 BUY 2 at 5057
P-1002 LNTN SELL 50 at 248.33
Exposure by instrument, in GBP:
  HBR100 10114 which is 44.9 percent of the gross
  LNTN -12416.50 which is 55.1 percent of the gross
Gross exposure: 22530.50
Net exposure: -2302.50
If every working order were filled at its level:
  HBR100 5124
  LNTN -12416.50
  TIDE 184.5
Check passed: running profit of 67.50 GBP agrees with the account summary.

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.
# Read the open positions from the practice API and work out the exposure on each instrument.
# 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 cannot place a trade.
import json
import os
import time
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": "positions-and-exposure-lesson"},
    )
    with urllib.request.urlopen(request, timeout=10) as reply:
        data = json.load(reply, parse_float=Decimal)   # prices as exact decimals, not floats
    assert data["practice"] is True, "this reply is not practice data"
    return data


def closing_price(position):
    """A BUY is closed by selling at the bid. A SELL is closed by buying at the offer."""
    return position["bid"] if position["direction"] == "BUY" else position["offer"]


def exposure(position):
    """Size times price: how much of the market the position holds. BUY counts up, SELL counts down."""
    value = position["size"] * closing_price(position)
    return value if position["direction"] == "BUY" else -value


def running_profit(position):
    """Points moved in your favour since the position was opened, times size."""
    if position["direction"] == "BUY":
        return (position["bid"] - position["openLevel"]) * position["size"]
    return (position["openLevel"] - position["offer"]) * position["size"]


positions = get("/positions.json")["positions"]

# Group by instrument with a dictionary: the key is the instrument, the value is its running total.
by_instrument = {}
for position in positions:
    name = position["instrumentId"]
    by_instrument[name] = by_instrument.get(name, Decimal("0")) + exposure(position)
    print(position["positionId"], name, position["direction"], position["size"], "at", closing_price(position))

gross = sum(abs(value) for value in by_instrument.values())
print("Exposure by instrument, in GBP:")
for name in sorted(by_instrument):
    share = round(100 * abs(by_instrument[name]) / gross, 1)
    print(" ", name, by_instrument[name], "which is", share, "percent of the gross")
print("Gross exposure:", gross)
print("Net exposure:", sum(by_instrument.values()))

time.sleep(PAUSE)
if_filled = dict(by_instrument)
for order in get("/orders.json")["orders"]:
    value = order["size"] * order["level"]
    signed = value if order["direction"] == "BUY" else -value
    if_filled[order["instrumentId"]] = if_filled.get(order["instrumentId"], Decimal("0")) + signed
print("If every working order were filled at its level:")
for name in sorted(if_filled):
    print(" ", name, if_filled[name])

# The check. What we work out from the levels must agree with what the API reports,
# position by position and then against the account summary, which is a second call.
for position in positions:
    assert running_profit(position) == position["profitLoss"], position["positionId"]
time.sleep(PAUSE)
account = get("/account.json")["account"]
total = sum(running_profit(position) for position in positions)
assert total == account["profitLoss"], "positions and account disagree"
print("Check passed: running profit of", total, "GBP agrees with the account summary.")

Your progress

Next: Keys and demo accounts

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 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.

  1. Read an API response
  2. First HTTP request
  3. Positions and exposureYou are here
  4. Keys and demo accounts
  5. First API call
  6. Paged results
  7. Follow the next page
  8. Price history to CSV

The whole path, every stage

Back: First HTTP requestNext: Keys and demo accounts