Skip to content

The UBI client

UnifiedBrokerInterface is the thin layer that actually speaks HTTP to UBI. It logs in with UBI's api key and secret, sends every request with the access token, logs in again once when the token is refused, and turns every failed answer into a typed exception. It knows nothing about instruments or orders: callers give it a route such as /api/instruments/ltp and get the parsed JSON back. Configuration sits underneath it and says where UBI and MongoDB are.

You rarely use either class directly, because every instrument and the account already share one client. The table below lists what the two classes offer, for the times you do.

Kind Member Description
class UnifiedBrokerInterface A connection to UBI's REST API
method connect Exchanges the api key and secret for an access token
method disconnect Revokes the access token for every client
method status Reports whether the session is connected and when it expires
method get, post, put, patch, delete Send one request to any UBI route
attribute token_expires_at When the current token expires, as UBI reported it
class Configuration The settings for one process, read from the environment on first use

UnifiedBrokerInterface

class UnifiedBrokerInterface(base_url=None, timeout_seconds=30, project_configuration=None)

The constructor finds UBI's address and reads UBI's api key and secret from MongoDB. It does not connect: the first request connects on its own. It lives in tradingmachine.unified_broker_interface.client.

Parameters

Name Type Required Default Description
base_url str or None No None UBI's address, such as http://127.0.0.1:8080. None reads TRADINGMACHINE_UBI_BASE_URL.
timeout_seconds float No 30 How long to wait for each response
project_configuration Configuration or None No None Where to read the base url and the MongoDB settings from. None builds a plain Configuration.

Example

This example builds a client of its own and reads the session status. It sends a real connect, so it would log out any client holding a token from before the latest 07:00; see One token for everyone. Its output was not captured.

from tradingmachine.unified_broker_interface import client

unified_broker_interface = client.UnifiedBrokerInterface()
print(unified_broker_interface.status())

Returns

The constructor returns the client, with token_expires_at set to None.

Raises

Exception When
ValueError No base url is configured, or MongoDB has no settings document for UBI, or the document has no api_key or no api_secret

Where the key and secret come from

The client does not read UBI's credentials from the environment. It reads them once, in the constructor, from this project's own MongoDB, from the settings collection document whose broker_name is unified_broker_interface. The document looks like the one below; the values are placeholders, and they must match the same document in UBI's own MongoDB.

{
  "broker_name": "unified_broker_interface",
  "api_key": "<the key UBI has in its own settings>",
  "api_secret": "<the secret UBI has in its own settings>"
}

The MongoDB connection is opened in a with block and closed straight away, so no database connection stays open for the life of the client. No code in this library creates the document; Configuration shows how to seed it.

One token for everyone

UBI holds a single access token for the whole application, not one per client. The rules that govern it come from UBI, and How the token lives and dies on the UBI site has them in full. The two that matter here are listed below.

  1. A connect returns the token already in force when it was issued at or after the most recent 07:00 and has not expired. Otherwise UBI mints a new token, and every other client still holding the old one starts getting HTTP 401, including UBI's own REST API test page.
  2. A token is accepted for one day by default, set by UBI's UNIFIED_BROKER_INTERFACE_API_TOKEN_TTL_SECONDS.

So a token held by this library can be refused at any moment, either because it expired or because someone else's connect replaced it. The client does not check token_expires_at before a request. It sends the request, and on a 401 it throws the token away, connects again and retries exactly once.

That is also why every instrument shares one client through Instrument.shared_unified_broker_interface. One shared client holds one copy of the token, so after a renewal or a disconnect only one request fails and only one reconnect is made.

The retry on 401

The sequence below shows a get whose token has gone stale. The retry happens once only: a second 401 is raised as AuthenticationError, so a wrong key or secret cannot cause a loop, and connect itself never retries.

sequenceDiagram
    autonumber
    participant You as Your code
    participant Client as UBI client
    participant UBI as UBI
    You->>Client: get("/api/instruments/ltp", params)
    Client->>UBI: GET with the stale access-token
    UBI-->>Client: 401 Invalid access token
    Client->>Client: forget the token
    Client->>UBI: POST /api/session/connect<br/>api-key and api-secret headers
    UBI-->>Client: 200 with access-token and expires_at
    Client->>UBI: GET again with the new access-token
    UBI-->>Client: 200 with the quote
    Client-->>You: the parsed JSON

If the second attempt also fails, the client raises the exception that matches its status code, as described under How failures become exceptions.

connect

method connect()POST /api/session/connect

This method sends the api key and secret as the api-key and api-secret headers and keeps the access token UBI returns, with its expiry in token_expires_at. You do not need to call it: the first request connects by itself, and a refused token reconnects by itself.

Parameters

This method takes no parameters.

Returns

The access token as a str.

Raises

Exception When
AuthenticationError UBI refused the key or secret
UnreachableError UBI could not be reached
UnifiedBrokerInterfaceError Any other failure

disconnect

method disconnect()DELETE /api/session/disconnect

This method revokes the access token on UBI, which ends the session for every client at once, not only this one, and clears the client's own copy. The next request made through any client connects again.

Parameters

This method takes no parameters.

Returns

UBI's answer as a dict, such as {"status": "disconnected"}.

Raises

Exception When
UnifiedBrokerInterfaceError UBI reported a failure or could not be reached

status

method status()GET /api/session/status

This method asks UBI whether the token is valid and when it expires. Like every request it connects first if the client has no token yet.

Parameters

This method takes no parameters.

Returns

UBI's answer as a dict, such as {"status": "connected", "expires_at": "..."}.

Raises

Exception When
UnifiedBrokerInterfaceError UBI reported a failure or could not be reached

Sending requests

The five methods in this section send one authenticated request to any route and return the parsed JSON. They are what every other class in the library is built on, and they all behave the same way: connect first if there is no token, send the request with the access-token header, retry once on a 401, and raise the exception that matches any other failure status.

The table below lists their parameters together, because they share them.

Name Type Required Default Description
path str Yes The route, starting with /api/
params dict or None No None Query string parameters
body any JSON-serialisable value, or None No None The request body, sent as JSON. get takes none.
timeout_seconds float or None No None post only. The seconds to wait for this one response, or None for the client's own timeout.

Each method returns the parsed JSON body, of any JSON type, or None when the body is empty or not JSON. Each raises a subclass of UnifiedBrokerInterfaceError chosen by the status code when UBI answers with a failure, and UnreachableError when no answer arrives.

get

method get(path, params=None)GET any route

This method sends a GET request. Every read in the library, from an instrument lookup to a quote, goes through it.

Example

This example reads a last price through the shared client. Its output was not captured, but the same route answered 1226.0 for RELIANCE through last_price on 2026-09-26.

from tradingmachine.assets import instruments

shared_client = instruments.Instrument.shared_unified_broker_interface()
answer = shared_client.get(
    "/api/instruments/ltp",
    params={
        "exchange": "nse",
        "segment": "equities",
        "symbol": "RELIANCE",
    },
)
print(answer["last_price"])

post

method post(path, body=None, params=None, timeout_seconds=None)POST any route

This method sends a POST request with a JSON body. post alone takes a timeout_seconds for one slow request, which exists for Account.flatten, because closing several positions one after another can take well over thirty seconds. Placing an order goes through post, so a POST to /api/orders/place without "dry_run": true in the body is a real order.

put

method put(path, body=None, params=None)PUT any route

This method sends a PUT request with a JSON body.

patch

method patch(path, body=None, params=None)PATCH any route

This method sends a PATCH request with a JSON body. UBI has no PATCH route today, so a PATCH gets HTTP 405 from UBI's web framework, which the client raises as ServerError. The method is there so that a future route needs no change here.

delete

method delete(path, body=None, params=None)DELETE any route

This method sends a DELETE request. It accepts a body as well as query parameters, because UBI's order routes read both.

How failures become exceptions

UBI reports a failure as a status code and a JSON body, and has no error-type field to switch on, so the status code alone chooses the exception class. The message is taken from the body's error field, or from its status_message when there is no error, which is how UBI's order engine explains a 504, or else it reads UBI returned HTTP <status>. The whole body is kept in the exception's detail. Errors has the full table of status codes.

token_expires_at

token_expires_at is a public attribute holding the str time the current token expires, exactly as UBI reported it, or None before the first connect and after a disconnect. The client never reads it; it is there for code that wants to show the expiry or plan around it.

Configuration

class Configuration(environment_file=None, load_environment_file=True)

Configuration, in tradingmachine.utilities.configuration, is the one place that knows which environment variable holds which setting. It reads nothing when it is created or when the library is imported. The first time one of its properties is read, it loads the .env file once, if asked to, and then every value is read from the process environment at that moment.

Parameters

Name Type Required Default Description
environment_file str or None No None The path of a .env file to load, or None to search the working directory and its parents for a file named .env
load_environment_file bool No True False to load no file at all and rely only on variables already exported

Properties

The table below lists what the object reports and which variable each value comes from. Every value is a str, or None when the variable is not set.

Property Environment variable
ubi_base_url TRADINGMACHINE_UBI_BASE_URL
mongodb_host TRADINGMACHINE_MONGODB_HOST
mongodb_port TRADINGMACHINE_MONGODB_PORT
mongodb_database_name TRADINGMACHINE_MONGODB_DB
mongodb_username TRADINGMACHINE_MONGODB_USERNAME
mongodb_password TRADINGMACHINE_MONGODB_PASSWORD
mongodb_connection_string Built from the four MongoDB connection values above, as mongodb://<username>:<password>@<host>:<port>/?authSource=admin, with the username and password escaped

reload() forgets that the file was loaded, so the next read loads it again. Note that loading a .env file never overrides a variable that is already exported, so an exported value always wins over the file.

Example

The three ways to point the library at its settings are shown below. The last one hands a custom client to an instrument, which is how one process can talk to a different UBI from another.

from tradingmachine.assets import equities
from tradingmachine.unified_broker_interface import client
from tradingmachine.utilities import configuration

from_working_directory = configuration.Configuration()
from_elsewhere = configuration.Configuration(
    environment_file="/etc/tradingmachine.env",
)
exported_only = configuration.Configuration(load_environment_file=False)

custom_client = client.UnifiedBrokerInterface(
    project_configuration=from_elsewhere,
)
reliance = equities.Equity(
    "nse",
    "RELIANCE",
    unified_broker_interface=custom_client,
)

A second client means a second login

A custom client holds its own token. If it connects after the shared client did and UBI mints a new token, the shared client's next request gets a 401 and reconnects, and the two keep trading places. That costs an extra round trip each time but never fails, because each side retries once.

Raises

Nothing. A missing variable is reported as None, and it is the client that raises ValueError when the base url or the credentials are missing.

Under the hood

Every request goes through one private method that adds the access-token header, sends it with requests.request, and on a first 401 clears the token and calls itself once more. A requests.RequestException such as a refused connection or a timeout becomes UnreachableError, chained so the original error is kept. See Session on the UBI site for the three session routes, and .claude/notes/src/tradingmachine/unified_broker_interface/client.py.md for the reasoning behind the client.