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.
What a subscription does when the reader is slow. Build it with the class methods.
Keep up to cap events and drop the newest beyond that (and count them).
Money of one currency.
The balances of the account changed.
One price level of an order book.
Base class of every error that brokapi raises about a broker or a command.
May the call be tried again: see ErrorClass.
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.
One bar.
What a connection can do (client.caps).
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.
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.
The name of the gateway this client reaches the broker through, None for a connection made by connect().
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.
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.
What this connection can do: streams, replace, stop orders, how a repeated cid is treated.
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.
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.
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.
The order book now, depth levels on each side (one request).
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.
A long history, one window per step. window overrides the span the broker accepts per request.
The whole history in memory. Suits a few thousand bars; for more write page by page.
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.
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.
Best bid and ask events.
Candle events.
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.
Events about your fills (Execution). Returns when the stream is open, like subscribe_order_state().
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.
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().
Cancels an order. Raises ~brokapi.Unknown when the outcome is not known.
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.
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.
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.
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.
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.
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):
methodis the name of the method in the description of the API ("GetLastPrices","Clock"), or"Service/Method"where two services have the same name.argsis 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 adictin the same form. The account is not added: put it in the request. Streaming methods raise~brokapi.Unsupported(raw_streaming). - BCS (REST):
methodis a name from the rate limit file of the library ("GetQuotes","GetPortfolio","GetOperationHistory", ...) or"GET /path"/"POST /path"for any other method.argshasquery(URL parameters),body(the JSON of aPOST) and, forGetOrderStatus,path({"id": ...}). A name without a route in the rate limit file spends the default quota. - ALOR (REST):
methodis"VERB /path"with the verbGET,POST,PUTorDELETEand the path of the API description ("GET /md/v2/time","GET /md/v2/Clients/MOEX/D12345/summary").argshasquery(URL parameters),body(the JSON of aPOSTorPUT) andrequest_id(theX-REQIDof 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).
What the broker says about an order. The way out of ~brokapi.Unknown.
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.
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.
One event with its address.
The coarse answer to "may I try this call again" (Error::class in Rust).
The call failed for a reason that can pass: the network, a rate limit.
The same call fails again until something else changes: a setup, a refused request, a closed gate.
The outcome of a command is not known. Repeating it could create a second order.
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``).
A fill of one of your orders.
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.
A directory entry.
An instrument: the common key and the broker's own id.
The same for every broker: a symbol on a venue.
Kind of an instrument.
What to look for in the directory of the broker. Build it with the class methods.
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.
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.
What the market data side can do.
A deal on the exchange tape.
How the broker itself names an instrument. Opaque: take it from client.instrument and pass it back.
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.
The broker accepted an order.
A change to apply to the book. A level with a zero qty is removed. seq grows by one.
The whole book (the levels asked for). Replaces what you had.
An order as the broker has it. Also the event for a change of an order.
Where an order is. The fields that do not apply to kind are None.
Where an order is in its life. See the orders page of the book for the diagram.
The outcome is not known. Ask the broker with order_state(cid).
Limit order (needs a price) or market order.
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.
What the broker does with a second place that has the same cid.
Money and holdings of the account.
A holding.
The positions of the account changed.
Best bid and best ask.
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.
What reconcile found.
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.
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.
The order book of this instrument must be taken again (take a snapshot, then follow increments).
Why an order book must be taken again.
Where to go when the value is between two steps.
To the nearer step. In the middle, away from zero for positive values.
Trading sessions of an exchange.
One trading session.
Kind of a trading session.
What a stop of one connector did.
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.
Buy or sell.
How long a stop order lives. Build it with the class methods.
What a stop order is for. With the side it fixes the direction of the trigger.
Protects a position: a sell triggers when the price falls to stop_price, a buy when it rises.
Takes a profit: a sell triggers when the price rises to stop_price, a buy when it falls.
Triggers like STOP_LOSS and always puts a limit order, so limit_price is required.
The id of the broker, from OrderAck.broker_id or StopOrderSnapshot.broker_id.
A stop order as the broker shows it (active_stop_orders).
Where a stop order is: kind, and the broker's own text when it is OTHER.
Where a stop order is in its life.
Cancelled before it triggered, by you or by the broker.
The status of the broker does not fit the others: its text is on StopOrderState.text.
Which market data streams the broker has.
Candle length.
When something happened, in nanoseconds since the Unix epoch (exact).
Whether an instrument trades right now.
What an order must respect: the step of the price, the smallest quantity, the lot.
Whether an instrument trades now.
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.
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:
- Do not send the command again.
- Ask the broker:
order_state(cid),active_orders()or the executions. - Only when the broker has no such order, place a new one under a NEW
cid.
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.
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.
What the account side can do.
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),endpointandapp_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,MOEXorSPBX, defaultMOEX),test(ALOR_TEST,true: the test contour of the broker, with a test account).bybit:api_key(BYBIT_API_KEY, required) andapi_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 fromBYBIT_DEMO_API_KEYandBYBIT_DEMO_API_SECRET. Alsocategory(BYBIT_CATEGORY, only"linear": USDT perpetuals and futures),settle_coin(BYBIT_SETTLE_COIN,"USDT"),recv_window_msandendpoint.
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").
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 inbrokapi.toml. Its directory is<state>/gateways/<gateway>, wherestateis thestate_dirof the service: the argumentstate, else the environment variableBROKAPI_STATE, elsebrokapi.statein the working directory (whatbrokapi initmakes next tobrokapi.toml).dir: the directory of the gateway itself. It holdsgateway.addrandaccess.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.
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.
A new client order id. The bot makes it, not the library, and writes it to its journal BEFORE place.
Rounds value to a multiple of step. Exact: a value that already is a multiple is returned as it is.