Skip to Content
SDK reference

Python SDK

The client, the stream, discovery and errors of the spectre-terminal package.

Install

pip install spectre-terminal

Python client for the Local API of Spectre Terminal.

from spectre_terminal import Terminal

terminal = Terminal.discover()
for connection in terminal.connections():
    print(connection.id, connection.exchange, connection.market, connection.status.state)

The public surface is Terminal (HTTP: market data, the account, trading commands and the workspace), Stream (WebSocket), the models of models, the errors of errors and the discovery helpers of discovery. Importing the package touches no network.

Terminal

A running Spectre Terminal, reached over its Local API on the loopback interface.

All methods are synchronous and return the typed models of models. Errors are subclasses of SpectreTerminalError: the terminal's own refusals as ApiError, an absent terminal as TerminalUnreachable.

Args

  • token — the token copied from Settings App -> General -> Local API.
  • port — the listener port, 48180 unless the terminal was started with SPECTRE_LOCAL_API_PORT; discover reads it from instance.json.
  • host — 127.0.0.1 or localhost; anything else is refused.
  • timeout — seconds to wait for a response. Cold market operations can take up to 3 s while the terminal opens a stream at the exchange.

Terminal()

Terminal(
    token: str,
    *,
    port: int = discovery.DEFAULT_PORT,
    host: str = discovery.DEFAULT_HOST,
    timeout: float = 10.0,
)

discover()

Terminal.discover(token: str | None = None, *, port: int | None = None) -> Terminal

Build a client from the environment and verify the terminal speaks API v1.

The token is token or SPECTRE_LOCAL_API_TOKEN; without either TokenMissing is raised. The port is port, else local_api_port from the terminal's instance.json (see discovery), else 48180. Then /v1/meta is read once: a terminal serving another API version raises IncompatibleTerminal; a stale token or an API that is switched off raises ApiError with code unauthorized.

host

host: str

The loopback host the client talks to.

port

port: int

The listener port.

base_url

base_url: str

http://<host>:<port>/v1.

stream_url

stream_url: str

ws://<host>:<port>/v1/stream.

meta()

meta() -> Meta

GET /v1/meta: API version, build, token scopes, limits, stream topics.

HTTP operation: GET /v1/meta

health()

health() -> Health

GET /v1/health: listener uptime and the market-data hub counters.

HTTP operation: GET /v1/health

connections()

connections() -> list[Connection]

GET /v1/connections: every connection of the terminal (connections:read).

HTTP operation: GET /v1/connections

connection()

connection(id: int) -> Connection

GET /v1/connections/{id}: one connection; not_found if there is no such id.

HTTP operation: GET /v1/connections/{id}

instruments()

instruments(
    *,
    connection_id: int | None = None,
    exchange: str | None = None,
    market: str | None = None,
    q: str | None = None,
    limit: int | None = None,
    cursor: str | None = None,
) -> Page[Instrument]

GET /v1/instruments: one page of the catalogue of an exchange and market.

Name the pair by connection_id or by exchange and market (one of the two is required). q filters by a case-insensitive substring of the symbol or the base coin; limit is 1-2000 (default 500); cursor is the next_cursor of the previous page.

HTTP operation: GET /v1/instruments

iter_instruments()

iter_instruments(
    *,
    connection_id: int | None = None,
    exchange: str | None = None,
    market: str | None = None,
    q: str | None = None,
    limit: int | None = None,
    cursor: str | None = None,
) -> Iterator[Instrument]

Every instrument of the catalogue, following next_cursor page by page.

Same parameters as instruments; cursor is where to start.

intervals()

intervals() -> Intervals

GET /v1/market/intervals: the candle and cluster intervals the terminal knows.

HTTP operation: GET /v1/market/intervals

candles()

candles(
    instrument: str,
    interval: str,
    *,
    limit: int | None = None,
    end: int | None = None,
    connection_id: int | None = None,
) -> Candles

GET /v1/market/candles: bar history by the path the chart in the window uses.

interval is a code from intervals (1m, 5m, ...); limit is up to 2000 (default 500); end is the right boundary in UTC milliseconds (default now). The exchange not answering is ApiError unavailable.

HTTP operation: GET /v1/market/candles

trades()

trades(
    instrument: str,
    *,
    limit: int | None = None,
    since_ts: int | None = None,
    connection_id: int | None = None,
) -> Trades

GET /v1/market/trades: the latest trades of the instrument, oldest first.

limit is 1-1000 (default 100); since_ts drops trades received before that UTC millisecond. A cold instrument waits for data up to 3 s; 3 s without a trade is an honestly empty list.

HTTP operation: GET /v1/market/trades

snapshot()

snapshot(instrument: str, *, connection_id: int | None = None) -> Snapshot

GET /v1/market/snapshot: best bid and ask, last trade, mark price and ticker.

HTTP operation: GET /v1/market/snapshot

orderbook()

orderbook(
    instrument: str,
    *,
    depth: int | None = None,
    connection_id: int | None = None,
) -> OrderBook

GET /v1/market/orderbook: the top depth levels (1-200, default 50) of the book.

No full book within 3 s is ApiError unavailable: an empty book would look like an empty market.

HTTP operation: GET /v1/market/orderbook

clusters()

clusters(instrument: str, *, connection_id: int | None = None) -> Clusters

GET /v1/market/clusters: the latest cluster buckets at the terminal's interval.

An instrument not open in the window is computed on request and its history is seeded from the backend; if the seed takes longer than 3 s the answer is ApiError not_active and a retry a few seconds later finds the clusters.

HTTP operation: GET /v1/market/clusters

account_orders()

account_orders(connection_id: int) -> AccountOrders

GET /v1/account/orders: open orders and trigger orders of one connection.

connection_id is required: there is no default connection on the account operations. An unknown id is ApiError not_found.

HTTP operation: GET /v1/account/orders

account_positions()

account_positions(connection_id: int) -> AccountPositions

GET /v1/account/positions: open positions of one connection.

HTTP operation: GET /v1/account/positions

account_balances()

account_balances(connection_id: int) -> AccountBalances

GET /v1/account/balances: per currency, the latest balance the exchange sent.

Balances are accumulated from the moment the API was switched on: a currency nothing happened to since then is absent from the list.

HTTP operation: GET /v1/account/balances

account_fills()

account_fills(
    connection_id: int,
    *,
    limit: int | None = None,
    since_ts: int | None = None,
) -> AccountFills

GET /v1/account/fills: the connection's latest fills, oldest first.

limit is 1-1000 (default 100); since_ts keeps fills not earlier than that exchange time in UTC milliseconds. The buffer is the last thousand fills since the API was switched on.

HTTP operation: GET /v1/account/fills

place_order()

place_order(
    connection_id: int,
    instrument: str,
    *,
    side: str,
    order_type: str,
    price: Money | None = None,
    size: Money | None = None,
    notional: Money | None = None,
    reduce_only: bool = False,
    post_only: bool = False,
    time_in_force: str | None = None,
    client_key: str | None = None,
) -> Accepted

POST /v1/orders: place an order the way the window does.

side is buy or sell; order_type is limit (needs price) or market (no price); exactly one of size (base coin) and notional (quote amount) is given. Money is Decimal, str or int and travels as a string; a float is refused. time_in_force is gtc, ioc, fok or post_only; absent means the venue's default. client_key is an idempotency key: a repeat with the same key returns the previous answer and places nothing.

The answer is Accepted — accepted by the terminal, not by the exchange; the outcome arrives on account.orders or in account_orders. A connection that does not trade is ApiError not_tradable; a refusal by the venue's minimums and steps is invalid_request; faster than limits.commands_per_second is rate_limited.

HTTP operation: POST /v1/orders

cancel_order()

cancel_order(connection_id: int, instrument: str, order_id: str) -> Accepted

DELETE /v1/orders/{id}: cancel one order by terminal key (t-...) or exchange id.

The cancellation goes to the exchange directly; the confirmation arrives through the stream. Accepted.key is None.

HTTP operation: DELETE /v1/orders/{id}

cancel_all()

cancel_all(connection_id: int, instrument: str) -> Accepted

POST /v1/orders/cancel-all: cancel every order of the instrument (the space bar).

HTTP operation: POST /v1/orders/cancel-all

close_position()

close_position(connection_id: int, instrument: str) -> Accepted

POST /v1/positions/close: close the instrument's position the way the window does.

On futures a market order for the position size with close_position; on spot "cancel all and close" as one atomic command. Without an open position — ApiError invalid_request.

HTTP operation: POST /v1/positions/close

place_trigger()

place_trigger(
    connection_id: int,
    instrument: str,
    *,
    kind: str,
    trigger_price: Money,
    side: str | None = None,
    notional: Money | None = None,
    rule: str | None = None,
    client_key: str | None = None,
) -> Accepted

POST /v1/orders/trigger: a stop loss, take profit or stop, as the window places them.

kind is stop_loss or take_profit — placed on the open position (side, size and rule come from it; without a position ApiError invalid_request) — or stop, which needs side (buy/sell) and notional (the quote amount) and takes an optional rule (price_above_or_equal or price_below_or_equal; absent means above the market for a buy, below for a sell). client_key is an idempotency key as in place_order.

Accepted.key is the trigger's key (sl_…, tp_…, stop_…), which shows up as client_id of the trigger in account_orders; cancelling and amending use the numeric id from there. A venue without trigger orders in the terminal is ApiError unsupported.

HTTP operation: POST /v1/orders/trigger

cancel_trigger()

cancel_trigger(connection_id: int, instrument: str, trigger_id: int | str) -> Accepted

DELETE /v1/orders/trigger/{id}: cancel one trigger by its numeric id.

The id is the one from account_orders. Accepted.key is None.

HTTP operation: DELETE /v1/orders/trigger/{id}

cancel_all_triggers()

cancel_all_triggers(connection_id: int, instrument: str) -> Accepted

POST /v1/orders/trigger/cancel-all: cancel every trigger of the instrument.

Plain orders stay; they go through cancel_all.

HTTP operation: POST /v1/orders/trigger/cancel-all

amend_trigger()

amend_trigger(
    connection_id: int,
    instrument: str,
    trigger_id: int | str,
    *,
    kind: str,
    trigger_price: Money,
    side: str | None = None,
    rule: str | None = None,
) -> Accepted

PATCH /v1/orders/trigger/{id}: move a trigger to a new price.

The window's sequential replacement: the old trigger is cancelled, a new one placed. kind is required (stop_loss, take_profit or stop); a stop also needs side and takes an optional rule. Accepted.key is None.

HTTP operation: PATCH /v1/orders/trigger/{id}

leverage()

leverage(connection_id: int, instrument: str) -> Leverage

GET /v1/account/leverage (account:read): leverage and margin mode.

Read from the terminal's cache: an instrument the terminal has not touched yet is ApiError not_active — open it in the window or call set_leverage, which is how it enters the cache.

HTTP operation: GET /v1/account/leverage

set_leverage()

set_leverage(
    connection_id: int,
    instrument: str,
    *,
    leverage: int | None = None,
    mode: str | None = None,
) -> Accepted

POST /v1/account/leverage (trading:write): set the leverage as the window does.

Exactly one of leverage (an integer of at least 1) or mode (max or smart). The answer is Accepted with key None; the applied value shows in leverage after the exchange answered.

HTTP operation: POST /v1/account/leverage

set_margin_mode()

set_margin_mode(connection_id: int, instrument: str, margin_mode: str) -> Accepted

POST /v1/account/margin-mode (trading:write): cross or isolated.

Changing it with an open position is refused by the exchange, as in the window; the Accepted answer does not know that yet.

HTTP operation: POST /v1/account/margin-mode

workspace()

workspace() -> Workspace

GET /v1/workspace (workspace:read): every window, its tabs, stacks and charts.

Read from the snapshot the window publishes at every structural change, so the call never waits on the window. The ids are internal to this run of the terminal.

HTTP operation: GET /v1/workspace

workspace_window()

workspace_window(window_id: int) -> Window

GET /v1/workspace/windows/{id}: one window; an unknown id is not_found.

HTTP operation: GET /v1/workspace/windows/{id}

open_stack()

open_stack(
    instrument: str,
    *,
    connection_id: int | None = None,
    chart: bool = False,
    target: str = 'new_tab',
    window_id: int | None = None,
    activate: bool = True,
) -> WorkspaceOutcome

POST /v1/workspace/stacks (workspace:write): a stack in a new tab or window.

target is new_tab (in window_id, or the focused window without it) or new_window. chart opens a chart of the same instrument over the stack. activate=False opens the tab in the background; a new window always takes focus. The answer names the window, the tab and the new widgets. An instrument the terminal's catalogue does not know is invalid_request.

HTTP operation: POST /v1/workspace/stacks

open_tab()

open_tab(
    items: Sequence[TabItem | str],
    *,
    name: str | None = None,
    target: str = 'new_tab',
    window_id: int | None = None,
    activate: bool = True,
) -> WorkspaceOutcome

POST /v1/workspace/tabs (workspace:write): a tab with a column per instrument.

Each item of items — a TabItem or a bare instrument address — becomes a column in list order: a stack and, with chart, a chart over it. One to fifty items. name is the tab name (up to 64 characters); without it the terminal names the tab after the only instrument or the first one plus a count (BTCUSDT +1). target, window_id and activate are as in open_stack. A refusal names the item (items[1]: …) and nothing reaches the window.

HTTP operation: POST /v1/workspace/tabs

add_stacks()

add_stacks(
    window_id: int,
    tab_id: int,
    items: Sequence[TabItem | str],
) -> WorkspaceOutcome

POST /v1/workspace/windows/{id}/tabs/{tab}/stacks: columns after the tab's last one.

The same items as open_tab; the tab keeps its name and its column widths. No such window or tab is not_found. The answer lists only the added stacks and charts.

HTTP operation: POST /v1/workspace/windows/{id}/tabs/{tab}/stacks

open_multiview()

open_multiview(
    base: str,
    *,
    target: str | None = None,
    window_id: int | None = None,
    activate: bool = True,
) -> WorkspaceOutcome

POST /v1/workspace/multiview (workspace:write): a coin on every venue.

The same plan as the window's Multiview button. base is a coin (BTC) or a pair (BTC/USDT); target left None follows the Multiview «open as» setting. No cell for the coin is not_found.

HTTP operation: POST /v1/workspace/multiview

set_stack_instrument()

set_stack_instrument(
    widget_id: int,
    instrument: str,
    *,
    connection_id: int | None = None,
) -> WorkspaceOutcome

PATCH /v1/workspace/stacks/{widget_id}: another instrument in the stack's group.

HTTP operation: PATCH /v1/workspace/stacks/{widget_id}

close_stack()

close_stack(widget_id: int) -> WorkspaceOutcome

DELETE /v1/workspace/stacks/{widget_id}: close one stack, as its close button.

HTTP operation: DELETE /v1/workspace/stacks/{widget_id}

close_tab()

close_tab(window_id: int, tab_id: int) -> WorkspaceOutcome

DELETE /v1/workspace/windows/{id}/tabs/{tab}: close a tab.

The only tab of a window cannot be closed (invalid_request).

HTTP operation: DELETE /v1/workspace/windows/{id}/tabs/{tab}

activate_tab()

activate_tab(window_id: int, tab_id: int) -> WorkspaceOutcome

POST /v1/workspace/windows/{id}/tabs/{tab}/activate: select the tab.

The window's focus is untouched; see activate_window.

HTTP operation: POST /v1/workspace/windows/{id}/tabs/{tab}/activate

close_window()

close_window(window_id: int) -> WorkspaceOutcome

DELETE /v1/workspace/windows/{id}: the main window is invalid_request.

HTTP operation: DELETE /v1/workspace/windows/{id}

activate_window()

activate_window(window_id: int) -> WorkspaceOutcome

POST /v1/workspace/windows/{id}/activate: restore and bring to the front.

HTTP operation: POST /v1/workspace/windows/{id}/activate

settings()

settings(
    *,
    scope: str | None = None,
    category: str | None = None,
    key: str | None = None,
    connection_id: int | None = None,
    instrument: str | None = None,
) -> Settings

GET /v1/settings (settings:read): rows as stored, no default ladder.

Without parameters: the global rows without a connection. instrument lists the pair's rows (scope order_book); connection_id the connection's rows. Each row says whether it is writable through update_settings.

HTTP operation: GET /v1/settings

orderbook_settings()

orderbook_settings(connection_id: int, instrument: str) -> OrderbookSettings

GET /v1/settings/orderbook (settings:read): stack settings, resolved.

The ladder the window uses — the pair's row on the connection, the connection's default, the pair's row without a connection, the global default — with resolved_from on every key.

HTTP operation: GET /v1/settings/orderbook

update_settings()

update_settings(changes: Sequence[SettingChange]) -> SettingsApplied

PATCH /v1/settings (settings:write): write settings as the Settings App does.

Every change is checked against the terminal's catalogue first (unknown key, wrong type, out of range, wrong shape — invalid_request naming changes[i], nothing written); leverage is unsupported here (use set_leverage). Then the changes are applied in order: the same row, window event and service caches as the Settings App. The first failed change stops the list and raises; earlier ones stay applied.

HTTP operation: PATCH /v1/settings

stream()

stream() -> Stream

A Stream for /v1/stream, for async with.

Stream

One connection to /v1/stream.

Use it as an async context manager (or connect / close): subscribe and unsubscribe send requests and wait for their replies while async for frame in stream yields Event, Gap and SubscriptionError in arrival order. Iteration ends when the connection closes; a server close with a code in CLOSE_RAISES raises StreamClosed so the caller notices, and close_code / close_reason keep what the server said.

Frames are buffered up to max_pending while the caller is not iterating; past that the client stops reading the socket, which is how the terminal learns a client is slow (gap frames, then close 4008).

Args

  • url — ws://127.0.0.1:<port>/v1/stream; stream builds it.
  • token — the bearer token for the handshake header.
  • open_timeout — seconds to wait for the handshake.
  • request_timeout — seconds to wait for a reply to subscribe / unsubscribe; None waits until the reply or the close.
  • max_pending — frames buffered ahead of the iterator.
  • max_size — largest frame accepted, in bytes.

Stream()

Stream(
    url: str,
    token: str,
    *,
    open_timeout: float = 10.0,
    request_timeout: float | None = None,
    max_pending: int = 1000,
    max_size: int | None = 16 * 1024 * 1024,
)

url

url: str

The stream URL.

connected

connected: bool

True between a successful handshake and the close.

close_code

close_code: int | None

The WebSocket close code once the connection has closed, else None.

close_reason

close_reason: str | None

The close reason once the connection has closed, else None.

connect()

async connect() -> Stream

Open the connection. Called by async with; a no-op when already open.

close()

async close(code: int = 1000, reason: str = '') -> None

Close the connection and wait for the server to complete the handshake.

subscribe()

async subscribe(
    topic: str,
    instrument: str | None = None,
    *,
    connection_id: int | None = None,
    **params: Any,
) -> Subscribed

Subscribe to topic and wait for the server's reply.

topic is one of /v1/meta's topics. Market topics (trades, ticker, mark_price, book.top, clusters) need instrument; connection_id is optional and chosen by the general rule without it. Account topics (account.orders, account.positions, account.balances, account.fills) need connection_id and take no instrument: the request carries none. workspace and settings take neither: they are one per terminal; workspace sends the current snapshot first, settings a frame per written row. params matter only for book.top (depth 1-200, interval_ms >= 50); other topics ignore them. The reply names the subscription id and the connection (None for the terminal topics); an error reply raises StreamRequestError.

unsubscribe()

async unsubscribe(sub: Subscribed | str) -> Unsubscribed

Remove a subscription by its Subscribed or its id.

Unknown subscription ids are answered with not_found and raise StreamRequestError.

Discovery

instance_path()

instance_path() -> Path

The path of the terminal's instance.json on this platform.

Linux: $XDG_DATA_HOME or ~/.local/share; macOS: ~/Library/Application Support; Windows: %LOCALAPPDATA%, then %APPDATA% — each followed by spectre-terminal/instance.json. The file is not required to exist.

read_port()

read_port(path: Path | None = None) -> int | None

The Local API port published in instance.json, or None when unknown.

None means the file is missing or unreadable — the caller falls back to DEFAULT_PORT. A file whose local_api_port is null or absent means the terminal is running with the API switched off, which raises LocalApiDisabled.

Errors

SpectreTerminalError

class SpectreTerminalError(Exception)

Base class of every error raised by spectre_terminal.

ApiError

class ApiError(SpectreTerminalError)

ApiError(code: str, message: str, status: int)

The terminal answered a request with an error envelope.

code is the stable code from the contract (unauthorized, not_found, invalid_request, unsupported, not_active, rate_limited, unavailable, ...); message is the human-readable text that came with it; status is the HTTP status. A non-JSON error body yields code="unknown" with the body text as the message.

TerminalUnreachable

class TerminalUnreachable(SpectreTerminalError)

No terminal answered on the loopback port: connection refused or timed out.

LocalApiDisabled

class LocalApiDisabled(SpectreTerminalError)

instance.json says the terminal is running but its Local API is off.

TokenMissing

class TokenMissing(SpectreTerminalError)

No token was given and SPECTRE_LOCAL_API_TOKEN is not set.

IncompatibleTerminal

class IncompatibleTerminal(SpectreTerminalError)

IncompatibleTerminal(api_version: str, supported: str)

The terminal serves an API version this client does not support.

StreamRequestError

class StreamRequestError(SpectreTerminalError)

StreamRequestError(code: str, message: str)

The stream answered a subscribe or unsubscribe request with an error frame.

StreamClosed

class StreamClosed(SpectreTerminalError)

StreamClosed(code: int | None, reason: str | None)

The server closed the stream with a code the caller must notice.

1001 — the API was switched off or the token was reissued; 4008 — this client was too slow to read; 4029 — the per-token connection limit; 1006 — the connection dropped without a close frame.

Generated from the code of spectre-terminal at commit 2d1c324.

On this page