brokapi.models

The data that brokapi returns and takes: instruments, order books, orders, events.

Every type is a frozen dataclass, so == and hash work and match can take it apart. Prices, quantities and money are decimal.Decimal, never float. Moments are timezone-aware datetime in UTC with microsecond precision. The exact nanoseconds are on Timestamps.

The fields have the names of the Rust types in brokapi_core, so the book and the Rust reference describe them both.

@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
@dataclass(frozen=True, slots=True)
class Caps:

What a connection can do (client.caps).

Caps(md: MarketDataCaps, ud: UserDataCaps)
@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 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

What an Envelope carries.

@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).

@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], ...]
@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 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
@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
@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 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'>
@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 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
@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
@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