Python SDK
The client, the stream, discovery and errors of the spectre-terminal package.
Install
pip install spectre-terminalPython 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,48180unless the terminal was started withSPECTRE_LOCAL_API_PORT;discoverreads it frominstance.json.host—127.0.0.1orlocalhost; 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) -> TerminalBuild 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: strThe loopback host the client talks to.
port
port: intThe listener port.
base_url
base_url: strhttp://<host>:<port>/v1.
stream_url
stream_url: strws://<host>:<port>/v1/stream.
meta()
meta() -> MetaGET /v1/meta: API version, build, token scopes, limits, stream topics.
HTTP operation: GET /v1/meta
health()
health() -> HealthGET /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) -> ConnectionGET /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() -> IntervalsGET /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,
) -> CandlesGET /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,
) -> TradesGET /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) -> SnapshotGET /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,
) -> OrderBookGET /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) -> ClustersGET /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) -> AccountOrdersGET /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) -> AccountPositionsGET /v1/account/positions: open positions of one connection.
HTTP operation: GET /v1/account/positions
account_balances()
account_balances(connection_id: int) -> AccountBalancesGET /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,
) -> AccountFillsGET /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,
) -> AcceptedPOST /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) -> AcceptedDELETE /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) -> AcceptedPOST /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) -> AcceptedPOST /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,
) -> AcceptedPOST /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) -> AcceptedDELETE /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) -> AcceptedPOST /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,
) -> AcceptedPATCH /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) -> LeverageGET /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,
) -> AcceptedPOST /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) -> AcceptedPOST /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() -> WorkspaceGET /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) -> WindowGET /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,
) -> WorkspaceOutcomePOST /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,
) -> WorkspaceOutcomePOST /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],
) -> WorkspaceOutcomePOST /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,
) -> WorkspaceOutcomePOST /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,
) -> WorkspaceOutcomePATCH /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) -> WorkspaceOutcomeDELETE /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) -> WorkspaceOutcomeDELETE /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) -> WorkspaceOutcomePOST /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) -> WorkspaceOutcomeDELETE /v1/workspace/windows/{id}: the main window is invalid_request.
HTTP operation: DELETE /v1/workspace/windows/{id}
activate_window()
activate_window(window_id: int) -> WorkspaceOutcomePOST /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,
) -> SettingsGET /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) -> OrderbookSettingsGET /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]) -> SettingsAppliedPATCH /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() -> StreamA 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;streambuilds 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 tosubscribe/unsubscribe;Nonewaits 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: strThe stream URL.
connected
connected: boolTrue between a successful handshake and the close.
close_code
close_code: int | NoneThe WebSocket close code once the connection has closed, else None.
close_reason
close_reason: str | NoneThe close reason once the connection has closed, else None.
connect()
async connect() -> StreamOpen the connection. Called by async with; a no-op when already open.
close()
async close(code: int = 1000, reason: str = '') -> NoneClose 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,
) -> SubscribedSubscribe 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) -> UnsubscribedRemove a subscription by its Subscribed or its id.
Unknown subscription ids are answered with not_found and raise
StreamRequestError.
Discovery
instance_path()
instance_path() -> PathThe 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 | NoneThe 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.