brokapi

brokapi: one interface to the APIs of Russian brokers (BCS, T-Bank, Finam, ALOR) for Python.

A thin layer over the Rust core of brokapi. The rules of the core are kept: trading opens after reconcile, the cid of an order is made by the bot, a command whose outcome is Unknown is never repeated, and one rate limit counter serves one broker area. Money, prices and quantities are decimal.Decimal, never float.

Read first, then trade. For example, against the imitations of the brokers (no network, no money):

import asyncio
from decimal import Decimal
import brokapi
from brokapi.testing import mock_bcs

async def main():
    async with await mock_bcs() as mock:
        client = await mock.connect()
        sber = await client.instrument("SBER")
        book = await client.order_book(sber, depth=5)
        print(book.bids[0].price)
        await client.close()

asyncio.run(main())

With a real account, leave out the mock: client = await brokapi.connect("bcs") reads BCS_REFRESH_TOKEN from the environment. Through a gateway (the program brokapi, which holds the connection for several bots) it is the same client: client = await brokapi.connect_gateway("bcs-main", bot="my-bot"). The book at https://brokapi.ru/docs/ has the quick start, the how-to pages and the explanations.

@dataclass(frozen=True, slots=True)
class Backpressure:

What a subscription does when the reader is slow. Build it with the class methods.

Backpressure(kind: str, cap: int = 0)
kind: str
cap: int
@classmethod
def conflate(cls) -> Backpressure:

Keep only the latest value. For books, quotes and candles, where the old value is useless.

@classmethod
def lossless(cls, cap: int) -> Backpressure:

Never drop. At cap events in the buffer an Overflow is sent and trading closes.

@classmethod
def drop_newest(cls, cap: int) -> Backpressure:

Keep up to cap events and drop the newest beyond that (and count them).

@dataclass(frozen=True, slots=True)
class Balance:

Money of one currency.

Balance( currency: str, available: decimal.Decimal, blocked: decimal.Decimal, ts: Timestamps)
currency: str
available: decimal.Decimal
blocked: decimal.Decimal
@dataclass(frozen=True, slots=True)
class BalancesUpdate:

The balances of the account changed.

BalancesUpdate(balances: list[Balance])
balances: list[Balance]
@dataclass(frozen=True, slots=True)
class BookLevel:

One price level of an order book.

BookLevel(price: decimal.Decimal, qty: decimal.Decimal)
price: decimal.Decimal
qty: decimal.Decimal
class BrokapiError(builtins.Exception):

Base class of every error that brokapi raises about a broker or a command.

error_class: ClassVar[ErrorClass] = <ErrorClass.FATAL: 'Fatal'>

May the call be tried again: see ErrorClass.

class CandlePager:

A long candle history, one window per step: async for bars in client.candle_pages(...).

A window is as long as the broker accepts, reads wait out RateLimited and repeat after a network error (reading is safe to repeat). Bars come in time order, each one once. A page can be empty (a weekend): that is not the end. After an error, resume_from is where to start again, and nothing before it is read twice.

CandlePager(native: __main__.NativePager, start: datetime.datetime)
resume_from: datetime.datetime

The start of the next window: save it after each page you wrote.

@dataclass(frozen=True, slots=True)
class Candle:

One bar.

Candle( instrument: InstrumentId, timeframe: Timeframe, open: decimal.Decimal, high: decimal.Decimal, low: decimal.Decimal, close: decimal.Decimal, volume: decimal.Decimal, open_ts: datetime.datetime, close_ts: datetime.datetime, ts: Timestamps)
instrument: InstrumentId
timeframe: Timeframe
open: decimal.Decimal
high: decimal.Decimal
low: decimal.Decimal
close: decimal.Decimal
volume: decimal.Decimal
open_ts: datetime.datetime
close_ts: datetime.datetime
@dataclass(frozen=True, slots=True)
class Caps:

What a connection can do (client.caps).

Caps(md: MarketDataCaps, ud: UserDataCaps)
class Client:

A connection to one broker account: market data, orders, the account, and the stream of events.

Make one with connect() (the connection to the broker is in this process) or with connect_gateway() (the connection is in a gateway, which several bots share). It is the same class with the same methods either way. Every method is a coroutine. A client is an async with context: it is closed when the block ends.

The rules of the core hold here as they do in Rust. Trading starts after reconcile(). The cid of an order is made by the bot (new_cid()). ~brokapi.Unknown is never repeated: ask order_state(). A command that you stop waiting for (a cancelled task, a timeout) still runs to its end in the library, so after that, too, ask the broker what became of it.

Client(native: __main__.NativeClient)
broker: str

bcs, tbank, finam or alor.

name: str

The name given to connect(). Events say <name>-md and <name>-ud in source. For a client of a gateway it is the name of the bot, and the events say <gateway>-md and <gateway>-ud: the gateway named the connection.

gateway: str | None

The name of the gateway this client reaches the broker through, None for a connection made by connect().

limiter: Limiter

The rate limit counter of this client. Pass it to the next connect() to share it.

A client of a gateway has none: the counter is the gateway's, one for all its bots, and it is not in this process. This raises ~brokapi.Unsupported there.

events: EventStream

The stream of events of this client (the same object every time).

async def close( self, deadline: float | None = 5.0) -> list[ShutdownReport]:

Stops the connectors and ends the event stream. Safe to call twice.

deadline is how many seconds a graceful stop may take, None stops at once. Orders at the exchange are not cancelled: cancel them yourself before, if the bot should leave none. Returns the reports of the two connectors (empty if the client was closed already).

A client of a gateway only ends its own connection: the gateway keeps running, and the subscriptions this bot opened are let go by the gateway when the bot has been away for a while. deadline has no meaning there and the list is empty.

async def caps(self) -> Caps:

What this connection can do: streams, replace, stop orders, how a repeated cid is treated.

async def instrument( self, ticker: str, board: str | None = None) -> InstrumentId:

The instrument for a ticker, in the form the broker wants.

board is the class code of BCS, T-Bank and ALOR (default TQBR), the MIC of Finam (default MISX) or the category of Bybit (linear, the only one). "SBER@TQBR" works as the ticker too, and so does "BTCUSDT@linear". BCS, Finam, ALOR and Bybit build the id from the text. T-Bank looks it up in its directory (one request) and raises ~brokapi.Rejected with code not_found if there is none.

async def find( self, query: InstrumentQuery) -> list[Instrument]:

Looks in the directory of the broker.

async def get_instrument( self, instrument: InstrumentId) -> Instrument:

The directory entry of an instrument.

async def trading_params( self, instrument: InstrumentId) -> TradingParams:

The price step, the smallest quantity and the lot of an instrument.

Also teaches the connector the price step, so that a price off the step is refused at once with ~brokapi.Rejected (price_off_step), before anything is sent.

async def trading_status( self, instrument: InstrumentId) -> TradingStatus:

Does the instrument trade now.

async def schedule( self, exchange: str, start: datetime.datetime, end: datetime.datetime) -> Schedule:

Trading sessions of an exchange in a period.

What exchange is depends on the broker. T-Bank: the name of an exchange ("MOEX"). BCS: the instrument, "TICKER@CLASSCODE" ("SBER@TQBR"). Finam: the symbol, "SBER@MISX". ALOR and Bybit have no schedule (~brokapi.Unsupported). A string the broker cannot use is ~brokapi.Config, nothing is sent.

start and end are timezone-aware datetime objects (datetime(..., tzinfo=timezone.utc)): a naive one raises ValueError.

async def order_book( self, instrument: InstrumentId, depth: int = 10) -> OrderBookSnapshot | OrderBookIncrement:

The order book now, depth levels on each side (one request).

async def last_trades( self, instrument: InstrumentId, limit: int = 100) -> list[MarketTrade]:

The last deals of the tape. How deep it goes depends on the broker.

async def candles( self, instrument: InstrumentId, timeframe: Timeframe, start: datetime.datetime, end: datetime.datetime) -> list[Candle]:

Bars of a period in one request. A broker accepts only a limited span per request: for a long history use candle_pages() or load_candles(), which cut the period into windows.

start and end must be timezone-aware datetime objects, for example datetime(2026, 1, 1, tzinfo=timezone.utc): a naive datetime raises ValueError (which zone would it be?). A period longer than the broker allows is ~brokapi.Rejected.

def candle_pages( self, instrument: InstrumentId, timeframe: Timeframe, start: datetime.datetime, end: datetime.datetime, *, window: datetime.timedelta | None = None, max_rate_waits: int | None = None, transport_retries: int | None = None) -> CandlePager:

A long history, one window per step. window overrides the span the broker accepts per request.

async def load_candles( self, instrument: InstrumentId, timeframe: Timeframe, start: datetime.datetime, end: datetime.datetime) -> list[Candle]:

The whole history in memory. Suits a few thousand bars; for more write page by page.

async def subscribe_order_book( self, instrument: InstrumentId, *, depth: int | None = None, backpressure: Backpressure | None = None) -> int:

Order book events for an instrument. Returns the subscription id (Envelope.sub).

By default Backpressure.conflate(): only the latest book matters, a slow reader just misses old ones.

async def subscribe_trades( self, instrument: InstrumentId, *, backpressure: Backpressure) -> int:

Deals of the tape. backpressure has no default: every deal is an event, so conflate() would lose deals. Choose drop_newest(cap) if losing some is acceptable, lossless(cap) if not.

async def subscribe_quotes( self, instrument: InstrumentId, *, backpressure: Backpressure | None = None) -> int:

Best bid and ask events.

async def subscribe_candles( self, instrument: InstrumentId, timeframe: Timeframe, *, backpressure: Backpressure | None = None) -> int:

Candle events.

async def unsubscribe(self, sub: int) -> None:

Ends a market data subscription.

async def portfolio(self) -> Portfolio:

Money and holdings.

async def positions(self) -> list[Position]:

Holdings.

async def balances(self) -> list[Balance]:

Money by currency.

async def executions( self, start: datetime.datetime, end: datetime.datetime) -> list[Execution]:

Your fills in a period.

async def subscribe_order_state(self) -> int:

Events about your orders (OrderSnapshot). Always lossless. Subscribe BEFORE reconcile(), so no event between the snapshot of the reconcile and the subscription is lost.

Returns when the stream is open at the broker, so an order placed right after it cannot be missed. A stream that does not open within three seconds raises ~brokapi.Transport and leaves nothing half open: call again. Finam is the exception: after three seconds the call returns and the stream goes on opening in the background.

async def subscribe_executions(self) -> int:

Events about your fills (Execution). Returns when the stream is open, like subscribe_order_state().

async def subscribe_portfolio(self, *, backpressure: Backpressure | None = None) -> int:

Events about money and holdings.

async def reconcile( self, ledger: Sequence[uuid.UUID | str] = ()) -> ReconcileReport:

Compares your ledger with the account and opens the trading gate.

ledger is the cids your bot believes are alive (an empty list for a new bot). Until a reconcile has succeeded, place raises ~brokapi.NotReconciled. After ~brokapi.Degraded call it again.

async def place( self, *, cid: uuid.UUID, instrument: InstrumentId, side: Side, qty: decimal.Decimal | str | int, price: decimal.Decimal | str | int | None = None, order_type: OrderType = <OrderType.LIMIT: 'Limit'>) -> OrderAck:

Places an order.

cid is made by the bot (new_cid()) and belongs in its journal before this call. qty is in units of the instrument (shares, currency units, contracts), never in lots: a broker that counts in lots gets the conversion from the connector, and a quantity that is not a whole number of lots is refused with ~brokapi.Rejected (invalid_qty) before anything is sent. qty and price are Decimal (a float is refused). A limit order needs a price, else the call raises ~brokapi.Rejected (missing_price).

Raises ~brokapi.NotReconciled before the first reconcile(), ~brokapi.Rejected when the broker or the connector says no (nothing was done), ~brokapi.Unknown when the outcome is not known: do not repeat, call order_state().

async def cancel(self, cid: uuid.UUID) -> None:

Cancels an order. Raises ~brokapi.Unknown when the outcome is not known.

async def replace( self, *, cid: uuid.UUID, original_cid: uuid.UUID, qty: decimal.Decimal | str | int | None = None, price: decimal.Decimal | str | int | None = None) -> OrderAck:

Changes the price or quantity of an order. The new order has its own cid.

Only where the broker can: (await client.caps()).ud.supports_replace. Elsewhere it raises ~brokapi.Unsupported and the order is not touched.

async def place_stop( self, *, cid: uuid.UUID, instrument: InstrumentId, side: Side, kind: StopOrderKind, qty: decimal.Decimal | str | int, stop_price: decimal.Decimal | str | int, limit_price: decimal.Decimal | str | int | None = None, expiry: StopExpiry | None = None) -> OrderAck:

Puts a stop order: it waits at the broker until the price reaches stop_price, then the broker puts an exchange order (a market order, or a limit order at limit_price). Until then it is not on the exchange.

The rules are those of place(): cid is made by the bot (new_cid()) and written to its journal first, it is sent at most once, qty is in units of the instrument and the gate opens after reconcile(). expiry defaults to StopExpiry.good_till_cancel(). A STOP_LIMIT needs a limit_price, else the call raises ~brokapi.Rejected before anything is sent.

Raises ~brokapi.Unsupported where the broker has no stop orders (caps().ud.supports_stop_orders), ~brokapi.Rejected when the request or the broker says no, ~brokapi.Unknown when the outcome is not known: do not repeat, look in active_stop_orders(). A stop order that has triggered is a plain order of the broker, and the library does not link it to the stop.

async def cancel_stop(self, stop: StopOrderRef | uuid.UUID) -> None:

Cancels a stop order that has not triggered.

Name it with StopOrderRef.cid(cid) (a uuid.UUID alone means the same) or StopOrderRef.broker(id). A cid is known from what this run has seen: after a restart T-Bank cannot find it, so use the id of the broker (OrderAck.broker_id, StopOrderSnapshot.broker_id). A repeated cancel creates nothing. Raises ~brokapi.Rejected when the stop is unknown or has triggered, ~brokapi.Unknown when the answer is missing: look in active_stop_orders() before you decide.

async def active_stop_orders(self) -> list[StopOrderSnapshot]:

The stop orders that wait for their trigger, with their parameters and StopOrderSnapshot.state.

Not in active_orders() and not in reconcile(). This list is the only way to learn that a stop triggered: there is no event for it.

async def subscribe_stop_orders(self) -> int:

A stream of stop order statuses. No broker has one (BCS does not document it, T-Bank's API has none, Finam has none, ALOR's stream has no event of the common interface): this raises ~brokapi.Unsupported for all of them. Poll active_stop_orders(); the library does not poll for you.

async def raw( self, method: str, args: Mapping[str, Any] | None = None, *, command: bool = False) -> Any:

Calls any method of the broker's API, outside the common interface. Returns the reply as dict.

The call goes through the same token, address and rate limit counter as the client: the quota is taken by the method name, and a call that finds none left is ~brokapi.RateLimited and is not sent. Errors of the broker are the usual ones. What raw does not do: it does not know the orders of the client. An order placed through it is not in the events, not in reconcile() and not in the order gate, and the library checks nothing in it (a price step, a lot, a stop-limit price). Place orders with place(). Use raw for what the common interface has no word for: reports, coupons and dividends, favourites, new methods of the broker.

command=False is for reading: a network failure is ~brokapi.Transport, and the call may be repeated. command=True is for anything that changes the account: a timeout or a lost reply is ~brokapi.Unknown and the call is never repeated, and it runs to its end even if you stop waiting. When in doubt whether a method changes the account, pass command=True.

  • T-Bank, Finam (gRPC): method is the name of the method in the description of the API ("GetLastPrices", "Clock"), or "Service/Method" where two services have the same name. args is the request message as JSON with the field names of the .proto, 64 bit integers and money as text. The reply is the message as a dict in the same form. The account is not added: put it in the request. Streaming methods raise ~brokapi.Unsupported (raw_streaming).
  • BCS (REST): method is a name from the rate limit file of the library ("GetQuotes", "GetPortfolio", "GetOperationHistory", ...) or "GET /path" / "POST /path" for any other method. args has query (URL parameters), body (the JSON of a POST) and, for GetOrderStatus, path ({"id": ...}). A name without a route in the rate limit file spends the default quota.
  • ALOR (REST): method is "VERB /path" with the verb GET, POST, PUT or DELETE and the path of the API description ("GET /md/v2/time", "GET /md/v2/Clients/MOEX/D12345/summary"). args has query (URL parameters), body (the JSON of a POST or PUT) and request_id (the X-REQID of an order command: the broker answers a repeated id with a copy of its first answer; a new one is made if you give none). Every call spends the default quota.

Raises ~brokapi.Config for a method the broker does not have or arguments that do not fit it (nothing was sent and no quota was spent).

async def order_state(self, cid: uuid.UUID) -> OrderSnapshot:

What the broker says about an order. The way out of ~brokapi.Unknown.

async def active_orders(self) -> list[OrderSnapshot]:

Orders that are alive at the broker.

class Config(brokapi.BrokapiError):

The setup is wrong: a missing token, a bad value, an input that cannot work with this broker.

Nothing was sent. The call fails the same way until the setting is fixed.

Config(message: str)
message
Inherited Members
BrokapiError
error_class
class Degraded(brokapi.BrokapiError):

The trading gate is closed because the connector lost events it could not afford to lose.

A lossless subscription overflowed (an ~brokapi.Overflow event was sent). Read the event stream to the end, then call reconcile(): a successful one opens the gate again. Nothing was sent.

Inherited Members
BrokapiError
error_class
@dataclass(frozen=True, slots=True)
class Envelope:

One event with its address.

Envelope( source: str, sub: int | None, ts: Timestamps, payload: OrderBookSnapshot | OrderBookIncrement | MarketTrade | Quote | Candle | OrderSnapshot | Execution | Portfolio | PositionsUpdate | BalancesUpdate | Resync | Overflow | Reconnected | Gap | UnknownEvent, schema: int = 1, _text: str | None = None)
source: str

Which connector sent it: <name>-md for market data, <name>-ud for the account.

sub: int | None

The subscription it belongs to, if any (what subscribe_* returned).

schema: int
class ErrorClass(enum.Enum):

The coarse answer to "may I try this call again" (Error::class in Rust).

RETRYABLE = <ErrorClass.RETRYABLE: 'Retryable'>

The call failed for a reason that can pass: the network, a rate limit.

FATAL = <ErrorClass.FATAL: 'Fatal'>

The same call fails again until something else changes: a setup, a refused request, a closed gate.

UNKNOWN = <ErrorClass.UNKNOWN: 'Unknown'>

The outcome of a command is not known. Repeating it could create a second order.

class EventStream:

The events of one client, in order: async for envelope in client.events.

One stream per client, for market data and for the account together. `Envelope.source` says which
connector sent an event, `Envelope.sub` which subscription. The stream ends when the client is closed:

the events sent before Client.close() are still delivered, then the iteration stops.

Read it as fast as you can. Do slow work (a database, a model) in another task and hand it the events through
your own queue: when this stream is not read, the connector applies the rule of each subscription, and a lossless
one closes trading after it overflows (see ``Overflow``).
EventStream(native: __main__.NativeClient)
async def get(self, timeout: float | None = None) -> Envelope:

The next event. Raises asyncio.TimeoutError after timeout seconds, EOFError at the end.

def drain(self, limit: int = 10000) -> list[Envelope]:

The events that are already waiting, without waiting for more.

@dataclass(frozen=True, slots=True)
class Execution:

A fill of one of your orders.

Execution( broker_order_id: str, instrument: InstrumentId, side: Side, price: decimal.Decimal, qty: decimal.Decimal, commission: decimal.Decimal, trade_id: str, ts: Timestamps, cid: uuid.UUID | None = None)
broker_order_id: str
instrument: InstrumentId
side: Side
price: decimal.Decimal
qty: decimal.Decimal
commission: decimal.Decimal
trade_id: str
cid: uuid.UUID | None
@dataclass(frozen=True, slots=True)
class Gap:

Events were lost between the gateway and this client: the gateway no longer has what came after the last event the client was given. Only a client made by ~brokapi.connect_gateway() gets it (source is <gateway>-gateway).

It is the gateway's counterpart of Overflow and Reconnected, and the rule is the same: read the state from the broker (active_orders, order_state, executions) and call reconcile. Trading is closed (~brokapi.Degraded) until you have. The stream goes on from the present moment, nothing needs to be reopened.

reason is "too_old" (the gateway keeps a limited number of recent events and the client was away longer), "new_epoch" (the gateway was restarted: it is a new process, and the numbers of events begin again) or "ahead" (the client's number is ahead of the gateway's, which does not happen unless the gateway's data was replaced). requested_after is the number the client asked to continue from, first_available the oldest event the gateway still has.

Gap(reason: str, requested_after: int, first_available: int)
reason: str
requested_after: int
first_available: int
subscriptions_lost: bool

True after "new_epoch": the new gateway holds none of the subscriptions this client made. Subscribe again (the ids are new).

@dataclass(frozen=True, slots=True)
class Instrument:

A directory entry.

Instrument( id: InstrumentId, name: str, kind: InstrumentKind, currency: str, exchange: str, isin: str | None = None)
name: str
currency: str
exchange: str
isin: str | None
@dataclass(frozen=True, slots=True)
class InstrumentId:

An instrument: the common key and the broker's own id.

InstrumentId(key: InstrumentKey, native: NativeId)
native: NativeId
@dataclass(frozen=True, slots=True)
class InstrumentKey:

The same for every broker: a symbol on a venue.

InstrumentKey(symbol: str, mic: str)
symbol: str
mic: str

Market identifier. For BCS it is the board (TQBR), for Finam the MIC (MISX).

class InstrumentKind(enum.Enum):

Kind of an instrument.

STOCK = <InstrumentKind.STOCK: 'Stock'>
BOND = <InstrumentKind.BOND: 'Bond'>
OPTION = <InstrumentKind.OPTION: 'Option'>
FUTURE = <InstrumentKind.FUTURE: 'Future'>
PERPETUAL = <InstrumentKind.PERPETUAL: 'Perpetual'>
ETF = <InstrumentKind.ETF: 'Etf'>
MUTUAL_FUND = <InstrumentKind.MUTUAL_FUND: 'MutualFund'>
CURRENCY = <InstrumentKind.CURRENCY: 'Currency'>
OTHER = <InstrumentKind.OTHER: 'Other'>
@dataclass(frozen=True, slots=True)
class InstrumentQuery:

What to look for in the directory of the broker. Build it with the class methods.

InstrumentQuery(_json: Any)
@classmethod
def by_ticker( cls, ticker: str, exchange: str | None = None) -> InstrumentQuery:

A ticker, optionally on one exchange or board.

@classmethod
def by_isin(cls, isin: str) -> InstrumentQuery:

An ISIN.

@classmethod
def by_kind( cls, kind: InstrumentKind) -> InstrumentQuery:

All instruments of one kind (can be a long list).

class Limiter:

The rate limit counter of one broker area.

A broker counts its limits per token or per user, so everything that talks to the broker with that token must share one counter: two counters together exceed the quota and the broker starts to refuse. Every connect() makes its own. To put several clients on one counter, pass limiter=first.limiter to the next ones. A limiter is shared only inside one broker.

Limiter(native: __main__.NativeLimiter)
broker: str

The broker whose limits it counts: bcs, tbank, finam or alor.

class LogError(brokapi.BrokapiError):

The event log could not be written or read (LogError of the Rust library).

kind says why: io (the file or the disk failed: check the path and the space), format (a line is not what the format says, the file is damaged or is not a log: line is its number, the header is line 1) or schema (the file was written by a newer version: update the package to read it). A reader never skips a bad line silently: it stops with this error.

LogError( kind: str, message: str, line: int | None = None, found: int | None = None, supported: int | None = None)
kind: str

io, format or schema.

line: int | None

The number of the bad line (from 1), for format.

found: int | None

The format or schema the file declares, for schema.

supported: int | None

The newest this build reads, for schema.

Inherited Members
BrokapiError
error_class
@dataclass(frozen=True, slots=True)
class MarketDataCaps:

What the market data side can do.

MarketDataCaps( max_order_book_depth: int, incremental_order_book: bool, streaming: StreamingCaps)
max_order_book_depth: int
incremental_order_book: bool
streaming: StreamingCaps
@dataclass(frozen=True, slots=True)
class MarketTrade:

A deal on the exchange tape.

MarketTrade( instrument: InstrumentId, price: decimal.Decimal, qty: decimal.Decimal, side: Side, ts: Timestamps, trade_id: str | None = None)
instrument: InstrumentId
price: decimal.Decimal
qty: decimal.Decimal
side: Side
trade_id: str | None
@dataclass(frozen=True, slots=True)
class NativeId:

How the broker itself names an instrument. Opaque: take it from client.instrument and pass it back.

NativeId(kind: str, values: tuple[tuple[str, str], ...])
kind: str

SymbolMic, TickerClass, Figi or Uid.

values: tuple[tuple[str, str], ...]
class NotReconciled(brokapi.BrokapiError):

The trading gate is closed: reconcile() has not run successfully yet.

Call reconcile with your ledger (an empty list if you have none), then repeat the command. Nothing was sent.

Inherited Members
BrokapiError
error_class
@dataclass(frozen=True, slots=True)
class OrderAck:

The broker accepted an order.

OrderAck(cid: uuid.UUID, broker_id: str, ts: Timestamps)
cid: uuid.UUID
broker_id: str
@dataclass(frozen=True, slots=True)
class OrderBookIncrement:

A change to apply to the book. A level with a zero qty is removed. seq grows by one.

OrderBookIncrement( instrument: InstrumentId, seq: int, bids: list[BookLevel], asks: list[BookLevel], ts: Timestamps)
instrument: InstrumentId
seq: int
bids: list[BookLevel]
asks: list[BookLevel]
@dataclass(frozen=True, slots=True)
class OrderBookSnapshot:

The whole book (the levels asked for). Replaces what you had.

OrderBookSnapshot( instrument: InstrumentId, bids: list[BookLevel], asks: list[BookLevel], ts: Timestamps)
instrument: InstrumentId
bids: list[BookLevel]

Buy side, best price first.

asks: list[BookLevel]

Sell side, best price first.

@dataclass(frozen=True, slots=True)
class OrderSnapshot:

An order as the broker has it. Also the event for a change of an order.

OrderSnapshot( cid: uuid.UUID, instrument: InstrumentId, state: OrderState, side: Side, order_type: OrderType, qty: decimal.Decimal, filled: decimal.Decimal, ts: Timestamps, broker_id: str | None = None, price: decimal.Decimal | None = None)
cid: uuid.UUID
instrument: InstrumentId
state: OrderState
side: Side
order_type: OrderType
qty: decimal.Decimal

In units of the instrument (shares, currency units, contracts), not lots.

filled: decimal.Decimal
broker_id: str | None
price: decimal.Decimal | None
@dataclass(frozen=True, slots=True)
class OrderState:

Where an order is. The fields that do not apply to kind are None.

OrderState( kind: OrderStateKind, cid: uuid.UUID, broker_id: str | None = None, filled: decimal.Decimal | None = None, left: decimal.Decimal | None = None, reason: str | None = None)
cid: uuid.UUID
broker_id: str | None

Set when kind is ACKED.

filled: decimal.Decimal | None

Set when kind is PARTIALLY_FILLED: how much is done.

left: decimal.Decimal | None

Set when kind is PARTIALLY_FILLED: how much is left.

reason: str | None

Set when kind is REJECTED.

is_final: bool

The order cannot change any more: filled, cancelled or rejected.

class OrderStateKind(enum.Enum):

Where an order is in its life. See the orders page of the book for the diagram.

IN_FLIGHT = <OrderStateKind.IN_FLIGHT: 'InFlight'>

Sent, no answer yet.

UNKNOWN = <OrderStateKind.UNKNOWN: 'Unknown'>

The outcome is not known. Ask the broker with order_state(cid).

ACKED = <OrderStateKind.ACKED: 'Acked'>

The broker has it and it works.

PARTIALLY_FILLED = <OrderStateKind.PARTIALLY_FILLED: 'PartiallyFilled'>
FILLED = <OrderStateKind.FILLED: 'Filled'>
CANCELLED = <OrderStateKind.CANCELLED: 'Cancelled'>
REJECTED = <OrderStateKind.REJECTED: 'Rejected'>
class OrderType(enum.Enum):

Limit order (needs a price) or market order.

LIMIT = <OrderType.LIMIT: 'Limit'>
MARKET = <OrderType.MARKET: 'Market'>
@dataclass(frozen=True, slots=True)
class Overflow:

The connector lost events of a lossless subscription since lost_from.

Trading is closed (~brokapi.Degraded) until you read the stream to the end and call reconcile.

Overflow(sub: int, lost_from: datetime.datetime)
sub: int
lost_from: datetime.datetime
@dataclass(frozen=True, slots=True)
class PlaceIdempotency:

What the broker does with a second place that has the same cid.

PlaceIdempotency(kind: str, window: datetime.timedelta | None = None)
kind: str

LabelOnly (the broker does not look), RejectDuplicate or Idempotent.

window: datetime.timedelta | None
@dataclass(frozen=True, slots=True)
class Portfolio:

Money and holdings of the account.

Portfolio( balances: list[Balance], positions: list[Position], ts: Timestamps)
balances: list[Balance]
positions: list[Position]
@dataclass(frozen=True, slots=True)
class Position:

A holding.

Position( instrument: InstrumentId, qty: decimal.Decimal, ts: Timestamps, avg_price: decimal.Decimal | None = None, current_price: decimal.Decimal | None = None)
instrument: InstrumentId
qty: decimal.Decimal
avg_price: decimal.Decimal | None
current_price: decimal.Decimal | None
@dataclass(frozen=True, slots=True)
class PositionsUpdate:

The positions of the account changed.

PositionsUpdate(positions: list[Position])
positions: list[Position]
@dataclass(frozen=True, slots=True)
class Quote:

Best bid and best ask.

Quote( instrument: InstrumentId, ts: Timestamps, bid: BookLevel | None = None, ask: BookLevel | None = None)
instrument: InstrumentId
bid: BookLevel | None
ask: BookLevel | None
class RateLimited(brokapi.BrokapiError):

A rate limit stopped the call: the local counter of the connector or the broker's own answer.

Wait retry_after, then repeat. A command refused by the local counter was not sent. The library does not wait and repeat for you.

RateLimited(retry_after: datetime.timedelta | float)
error_class = <ErrorClass.RETRYABLE: 'Retryable'>

May the call be tried again: see ErrorClass.

retry_after: datetime.timedelta

How long to wait before the next try. A hint, not a promise that the next call passes.

@dataclass(frozen=True, slots=True)
class ReconcileReport:

What reconcile found.

ReconcileReport( confirmed: list[OrderSnapshot], closed_offline: list[uuid.UUID], foreign: list[OrderSnapshot])
confirmed: list[OrderSnapshot]

Orders of your ledger that the broker has.

closed_offline: list[uuid.UUID]

Orders of your ledger that ended while you were away.

foreign: list[OrderSnapshot]

Active orders of the account that are not in your ledger.

@dataclass(frozen=True, slots=True)
class Reconnected:

The stream of your orders or of your fills was cut and opened again: events of the gap never reached you.

sub is the subscription whose stream was cut (one event for the order state subscription and one for the executions subscription). gap_from is when the connector noticed the cut: the hole starts at or before it.

Trading is closed (~brokapi.Degraded) until you read the stream to the end, read the state from the broker (active_orders, order_state, executions from a little before gap_from) and call reconcile.

Reconnected(sub: int, gap_from: datetime.datetime)
sub: int
gap_from: datetime.datetime
class Rejected(brokapi.BrokapiError):

The broker (or the connector, before sending) said no to this request, and nothing was done.

The request is wrong or not allowed: a price outside the band, no money, an unknown order. Change the request. Do not repeat it as it is. code is a short label set by the connector (validation, missing_price, not_found, price_off_step, ...), safe to compare with ==. Do not parse message.

Rejected(code: str, message: str)
code: str

Short label of the reason, set by the connector.

message: str

Explanation for a human, usually the broker's own text.

Inherited Members
BrokapiError
error_class
@dataclass(frozen=True, slots=True)
class Resync:

The order book of this instrument must be taken again (take a snapshot, then follow increments).

Resync( sub: int, instrument: InstrumentId, reason: ResyncReason)
sub: int
instrument: InstrumentId
reason: ResyncReason
class ResyncReason(enum.Enum):

Why an order book must be taken again.

SEQUENCE_GAP = <ResyncReason.SEQUENCE_GAP: 'SequenceGap'>
SUBSCRIPTION_RESET = <ResyncReason.SUBSCRIPTION_RESET: 'SubscriptionReset'>
MANUAL = <ResyncReason.MANUAL: 'Manual'>
class RoundMode(enum.Enum):

Where to go when the value is between two steps.

DOWN = <RoundMode.DOWN: 'Down'>

To the lower step (a buy price that must not exceed the plan).

UP = <RoundMode.UP: 'Up'>

To the higher step.

NEAREST = <RoundMode.NEAREST: 'Nearest'>

To the nearer step. In the middle, away from zero for positive values.

@dataclass(frozen=True, slots=True)
class Schedule:

Trading sessions of an exchange.

Schedule( exchange: str, sessions: list[SessionInterval], ts: Timestamps)
exchange: str
sessions: list[SessionInterval]
@dataclass(frozen=True, slots=True)
class SessionInterval:

One trading session.

SessionInterval( start: datetime.datetime, end: datetime.datetime, kind: SessionKind)
start: datetime.datetime
end: datetime.datetime
kind: SessionKind
class SessionKind(enum.Enum):

Kind of a trading session.

PRE_MARKET = <SessionKind.PRE_MARKET: 'PreMarket'>
MAIN = <SessionKind.MAIN: 'Main'>
POST_MARKET = <SessionKind.POST_MARKET: 'PostMarket'>
AUCTION = <SessionKind.AUCTION: 'Auction'>
OTHER = <SessionKind.OTHER: 'Other'>
@dataclass(frozen=True, slots=True)
class ShutdownReport:

What a stop of one connector did.

ShutdownReport( unsubscribed: int, unsubscribe_failed: int, inflight_abandoned: list[uuid.UUID])
unsubscribed: int
unsubscribe_failed: int
inflight_abandoned: list[uuid.UUID]
class ShuttingDown(brokapi.BrokapiError):

The connector is shutting down and takes no new trading commands.

Final for this client. Orders already at the exchange are not touched by a shutdown. Connect again to trade.

Inherited Members
BrokapiError
error_class
class Side(enum.Enum):

Buy or sell.

BUY = <Side.BUY: 'Buy'>
SELL = <Side.SELL: 'Sell'>
@dataclass(frozen=True, slots=True)
class StopExpiry:

How long a stop order lives. Build it with the class methods.

StopExpiry(kind: str, until: datetime.datetime | None = None)
kind: str

GoodTillCancel, EndOfDay or GoodTillDate.

until: datetime.datetime | None

Set when kind is GoodTillDate.

@classmethod
def good_till_cancel(cls) -> StopExpiry:

Until cancelled.

@classmethod
def end_of_day(cls) -> StopExpiry:

Until the end of the trading day (not every broker has it).

@classmethod
def good_till_date(cls, moment: datetime.datetime) -> StopExpiry:

Until the given moment (an aware datetime). Not every broker has it for every kind.

class StopOrderKind(enum.Enum):

What a stop order is for. With the side it fixes the direction of the trigger.

STOP_LOSS = <StopOrderKind.STOP_LOSS: 'StopLoss'>

Protects a position: a sell triggers when the price falls to stop_price, a buy when it rises.

TAKE_PROFIT = <StopOrderKind.TAKE_PROFIT: 'TakeProfit'>

Takes a profit: a sell triggers when the price rises to stop_price, a buy when it falls.

STOP_LIMIT = <StopOrderKind.STOP_LIMIT: 'StopLimit'>

Triggers like STOP_LOSS and always puts a limit order, so limit_price is required.

def triggers_on_rise(self, side: Side) -> bool:

True when the stop triggers on a RISING price for this side. Falling otherwise.

@dataclass(frozen=True, slots=True)
class StopOrderRef:

Which stop order a command is about. Build it with cid() or broker().

StopOrderRef(kind: str, value: str)
kind: str

Cid or Broker.

value: str
@classmethod
def cid(cls, cid: uuid.UUID | str) -> StopOrderRef:

The cid given to place_stop. The connector knows it only for stops it saw in this run: after a restart T-Bank refuses it with Rejected (unknown_stop_order), use broker() then.

@classmethod
def broker(cls, broker_id: str) -> StopOrderRef:

The id of the broker, from OrderAck.broker_id or StopOrderSnapshot.broker_id.

@dataclass(frozen=True, slots=True)
class StopOrderSnapshot:

A stop order as the broker shows it (active_stop_orders).

StopOrderSnapshot( broker_id: str, instrument: InstrumentId, side: Side, kind: StopOrderKind, qty: decimal.Decimal, stop_price: decimal.Decimal, expiry: StopExpiry, state: StopOrderState, ts: Timestamps, cid: uuid.UUID | None = None, limit_price: decimal.Decimal | None = None)
broker_id: str

The id of the broker. Use it with StopOrderRef.broker.

instrument: InstrumentId
side: Side
qty: decimal.Decimal

In units of the instrument, not lots.

stop_price: decimal.Decimal

The trigger price.

expiry: StopExpiry
cid: uuid.UUID | None

None when the connector cannot tie the stop order to a cid (placed by someone else, or by an earlier run of the program, at a broker that does not echo it, or a lost answer that fits this stop and another one equally well). Next to a lost answer, a stop without a cid may be yours.

limit_price: decimal.Decimal | None

The price of the exchange order put at the trigger. None is a market order.

@dataclass(frozen=True, slots=True)
class StopOrderState:

Where a stop order is: kind, and the broker's own text when it is OTHER.

StopOrderState(kind: StopOrderStateKind, text: str | None = None)
text: str | None
is_final: bool

The stop order cannot change any more: triggered, cancelled or expired.

class StopOrderStateKind(enum.Enum):

Where a stop order is in its life.

ACTIVE = <StopOrderStateKind.ACTIVE: 'Active'>

Waiting for the trigger.

TRIGGERED = <StopOrderStateKind.TRIGGERED: 'Triggered'>

Triggered: the exchange order was put.

CANCELLED = <StopOrderStateKind.CANCELLED: 'Cancelled'>

Cancelled before it triggered, by you or by the broker.

EXPIRED = <StopOrderStateKind.EXPIRED: 'Expired'>

The lifetime ended without a trigger.

OTHER = <StopOrderStateKind.OTHER: 'Other'>

The status of the broker does not fit the others: its text is on StopOrderState.text.

@dataclass(frozen=True, slots=True)
class StreamingCaps:

Which market data streams the broker has.

StreamingCaps(order_book: bool, market_trades: bool, quotes: bool, candles: bool)
order_book: bool
market_trades: bool
quotes: bool
candles: bool
class Timeframe(enum.Enum):

Candle length.

M1 = <Timeframe.M1: 'M1'>
M5 = <Timeframe.M5: 'M5'>
M10 = <Timeframe.M10: 'M10'>
M15 = <Timeframe.M15: 'M15'>
M30 = <Timeframe.M30: 'M30'>
H1 = <Timeframe.H1: 'H1'>
H4 = <Timeframe.H4: 'H4'>
D1 = <Timeframe.D1: 'D1'>
W1 = <Timeframe.W1: 'W1'>
MN = <Timeframe.MN: 'MN'>
seconds: int

Length of one candle in seconds (a month is counted as 30 days).

@dataclass(frozen=True, slots=True)
class Timestamps:

When something happened, in nanoseconds since the Unix epoch (exact).

Timestamps(rx_ns: int, broker_ns: int | None = None)
rx_ns: int

When the connector received it.

broker_ns: int | None

The time the broker put on it, if it did.

rx_ts: datetime.datetime

Receive time as an aware UTC datetime.

broker_ts: datetime.datetime | None

Broker time as an aware UTC datetime, if the broker gave one.

class TradingAvailability(enum.Enum):

Whether an instrument trades right now.

OPEN = <TradingAvailability.OPEN: 'Open'>
CLOSED = <TradingAvailability.CLOSED: 'Closed'>
BREAK = <TradingAvailability.BREAK: 'Break'>
UNKNOWN = <TradingAvailability.UNKNOWN: 'Unknown'>
@dataclass(frozen=True, slots=True)
class TradingParams:

What an order must respect: the step of the price, the smallest quantity, the lot.

TradingParams( instrument: InstrumentId, lot_size: decimal.Decimal, min_qty: decimal.Decimal, price_step: decimal.Decimal, price_step_value: decimal.Decimal, currency: str)
instrument: InstrumentId
lot_size: decimal.Decimal
min_qty: decimal.Decimal
price_step: decimal.Decimal
price_step_value: decimal.Decimal
currency: str
@dataclass(frozen=True, slots=True)
class TradingStatus:

Whether an instrument trades now.

TradingStatus( instrument: InstrumentId, availability: TradingAvailability, reason: str | None = None, ts: datetime.datetime | None = None)
instrument: InstrumentId
availability: TradingAvailability
reason: str | None
ts: datetime.datetime | None
class Transport(brokapi.BrokapiError):

The network or the broker's endpoint failed in a way that is not an answer (connect, TLS, timeout, a cut stream).

For a reading call, repeat it later. A command that might have reached the broker raises Unknown instead, so this error on a command means the connector has no sign that it did.

Transport(message: str)
error_class = <ErrorClass.RETRYABLE: 'Retryable'>

May the call be tried again: see ErrorClass.

message
class Unknown(brokapi.BrokapiError):

A command was sent and nothing says whether the broker did it: a timeout, a cut connection, an HTTP 5xx.

The order may exist, may be cancelled, or may never have arrived. The library never repeats the command and neither should you:

  1. Do not send the command again.
  2. Ask the broker: order_state(cid), active_orders() or the executions.
  3. Only when the broker has no such order, place a new one under a NEW cid.
error_class = <ErrorClass.UNKNOWN: 'Unknown'>

May the call be tried again: see ErrorClass.

@dataclass(frozen=True, slots=True)
class UnknownEvent:

An event of a kind this version of the Python package does not know (the core is newer than the package).

The stream does not break on it: kind is the name the core gave it and data its content as it came (JSON values, money as text). Update the package to get a typed class. An event of a kind you do not know may be about the loss of data (the way Overflow and Reconnected are): when in doubt, reconcile before trading further.

UnknownEvent(kind: str, data: Any)
kind: str
data: Any
class Unsupported(brokapi.BrokapiError):

This connector or this broker does not have the capability, so the call was not made.

Raised at once, without a network call. capability is a short name of what is missing. The library does not emulate a missing stream by polling. client.caps says in advance what a connector can do.

Unsupported(capability: str)
capability: str

Short name of the missing capability, safe to compare with ==.

Inherited Members
BrokapiError
error_class
@dataclass(frozen=True, slots=True)
class UserDataCaps:

What the account side can do.

UserDataCaps( place_idempotency: PlaceIdempotency, ws_rpc: bool, ordered_command_channel: bool, supports_replace: bool, supports_stop_orders: bool, streams_executions: bool, sandbox: bool)
place_idempotency: PlaceIdempotency
ws_rpc: bool
ordered_command_channel: bool
supports_replace: bool

replace works. Without it replace raises ~brokapi.Unsupported.

supports_stop_orders: bool
streams_executions: bool
sandbox: bool
async def connect( broker: str, *, name: str | None = None, limiter: Limiter | None = None, event_buffer: int = 8192, **settings: Any) -> Client:

Connects to a broker account.

What you do not pass is read from the environment (and a .env file next to the program), as in the Rust library:

  • bcs: refresh_token (BCS_REFRESH_TOKEN, required), write (BCS_IS_WRITE, true: the token can trade).
  • tbank: token (TBANK_TOKEN, required), sandbox (TBANK_SANDBOX, without it the address is the real one), account_id (TBANK_ACCOUNT_ID, needed when the token has several), endpoint and app_name (TBANK_ENDPOINT, TBANK_APP_NAME).
  • finam: secret (FINAM_SECRET, required), account_id (FINAM_ACCOUNT_ID, required), endpoint (FINAM_API_URL).
  • alor: refresh_token (ALOR_REFRESH_TOKEN, required), portfolio (ALOR_PORTFOLIO, required when the account has several portfolios), exchange (ALOR_EXCHANGE, MOEX or SPBX, default MOEX), test (ALOR_TEST, true: the test contour of the broker, with a test account).
  • bybit: api_key (BYBIT_API_KEY, required) and api_secret (BYBIT_API_SECRET, required), env (BYBIT_ENV: "mainnet" is the default, "demo" is the demo account with virtual money, "testnet"). The demo account has a key pair of its own, read from BYBIT_DEMO_API_KEY and BYBIT_DEMO_API_SECRET. Also category (BYBIT_CATEGORY, only "linear": USDT perpetuals and futures), settle_coin (BYBIT_SETTLE_COIN, "USDT"), recv_window_ms and endpoint.

name labels the events (<name>-md, <name>-ud) and defaults to the broker. Give each client of a process its own name. limiter puts this client on the rate limit counter of another one (see Limiter). event_buffer is how many events wait for the reader before the connector applies the rule of each subscription.

Raises ~brokapi.Config for a missing or wrong setting, ~brokapi.Transport when the broker cannot be reached or refuses the token. BCS has no sandbox: the account is real. ALOR has a test contour (test=True), Bybit a demo account (env="demo").

async def connect_gateway( gateway: str | None = None, *, bot: str, state: str | os.PathLike[str] | None = None, dir: str | os.PathLike[str] | None = None, ready_timeout: float = 30.0, event_buffer: int = 8192, own_orders_only: bool = False) -> Client:

Connects to a broker account through a gateway: the same Client that connect() makes.

A gateway is a process of the program brokapi that holds the connection to one broker account for several bots (brokapi up, see the Gateway section of the book). A bot that moves from the broker to a gateway changes this one line and keeps every call it makes.

The gateway is named in one of two ways:

  • gateway: its name in brokapi.toml. Its directory is <state>/gateways/<gateway>, where state is the state_dir of the service: the argument state, else the environment variable BROKAPI_STATE, else brokapi.state in the working directory (what brokapi init makes next to brokapi.toml).
  • dir: the directory of the gateway itself. It holds gateway.addr and access.key, which the client reads.

bot is the name of this bot, 1 to 64 letters, digits, ., _ or -. The gateway shows it in brokapi status and uses it for own_orders_only. Give each bot its own: two bots with one name are one bot to the gateway.

ready_timeout is how many seconds to wait for a gateway that is still comparing its journal with the broker (it takes commands after that). event_buffer is how many events wait for the reader before the gateway cuts the bot off and the client mends the stream from the last event it handled. own_orders_only leaves out the order and fill events of the orders other bots placed.

What is the same as with the broker: every method of Client, the errors, async for over Client.events, the rule that trading starts after ~Client.reconcile(), and ~brokapi.Degraded after events were lost. What differs, and why: Gap is a new event, Client.limiter is not there, and ~Client.close() does not stop the gateway. The page "Подключить бота" of the Gateway section of the book lists them.

Raises ~brokapi.Config when the directory has no gateway in it (it is not running, or the name is wrong) or the key is refused, ~brokapi.Transport when the gateway does not become ready in time.

async def merge( *sources: EventStream | Client) -> AsyncIterator[Envelope]:

One stream out of the streams of several clients: async for envelope in brokapi.merge(a, b).

Envelope.source still says whose each event is. It ends when all the clients are closed. Stop reading it only when you are done with those clients: an event that was just fetched when you stop is lost.

def new_cid() -> uuid.UUID:

A new client order id. The bot makes it, not the library, and writes it to its journal BEFORE place.

def round_to_step( value: decimal.Decimal, step: decimal.Decimal, mode: RoundMode = <RoundMode.DOWN: 'Down'>) -> decimal.Decimal:

Rounds value to a multiple of step. Exact: a value that already is a multiple is returned as it is.