Skip to content
DOCUMENTATION / Trading API
OPBT SDK · 04

Trading API

On this page
04

Trading API

Backtest market orders execute after the strategy decision against the current real completed-bar close logical BBO, plus configured slippage and fees; an unseen next open is never used. Paper uses completed bars as decision triggers but prices a submitted market order from a fresh OnePort BBO. If that quote is missing, stale or invalid, the Paper order is rejected without changing the account.

Order methods#

ctx.place_order(instrument, side, order_type, qty, *, account=None, price=None, reduce_only=False) → OrderSubmitResult

Submit an order in Backtest, Paper or Live. qty is normally a positive coin quantity; side carries direction. A finite exact zero returns a terminal Rejected/PRECISION result with no provider call or position change. Limit requires price, while Market does not accept one. The framework requires exact quantity and price-step alignment and never silently resizes or reprices an order. Minimum quantity, minimum notional and per-order maximums are reference fields rather than OPR admission rules; a Live venue remains authoritative. reduce_only applies only to UPERP: a valid request that is flat, same-direction or crosses zero returns terminal Rejected/REDUCE_ONLY and the strategy continues. These local rejections consume one decision intent but no active-order slot. With multiple accounts, pass the configured account key.

ctx.quantize_qty(instrument, qty) → float

Round a calculated non-negative coin quantity down to the active execution context's qty_step. Backtest uses the run-frozen step; Paper/Live use one cached current OnePort snapshot bound before the callback. A value below one step returns 0; normally skip it. If submitted, zero is a local terminal Rejected/PRECISION audit result and does not abort the callback.

ctx.quantize_price(instrument, price, *, side) → float

Round a calculated Limit price to the active execution context's price_step: down for Buy and up for Sell. Backtest uses frozen precision; Paper/Live use the callback-bound current snapshot. The gateway never requantizes or retries.

ctx.cancel_order(order_id) → bool

Return whether the execution source confirmed the cancel request. This does not prove that the order is cancelled or terminal; query order_status later.

ctx.order_status(order_id) → OrderView | None

Read only the latest durable local view. Known orders, including completed ones, remain queryable throughout the generation; this form never reads a venue.

ctx.order_status(*, native_order_id, instrument, account) → OrderView | None

Explicitly query one known, exactly owned native order identity. Live performs exactly one provider read per call, with no scan, retry or background polling; Backtest and Paper use native ID = local ID as a string.

ctx.open_orders(instrument=None, *, account=None) → tuple[OrderView, ...]

Return local orders not explicitly terminal=true. It reads only the local ledger; Live terminal=null means the last observation did not provide that field, not that OPR is confirming it in the background.

ctx.execution_receipts(after_cursor=0, limit=100) → tuple[ExecutionReceipt, ...]

Page immutable PLACE/CANCEL command receipts from the generation-local cursor. Reading does not query, consume or acknowledge a receipt; persist the processed cursor in ctx.state.

Order status is exactly New, PartiallyFilled, Filled, Cancelled, Rejected, Expired or Unknown. Live transport ambiguity is returned without retry or a global freeze. The scheduler never polls, resubmits, cancels or flattens an order; the SDK has no order-event DTO or callback.

python
instrument = "binance:UPERP:BTCUSDT"
side = "Buy"
market_price = float(bars[instrument].close)
equity = ctx.accounts["alpha"].equity
if equity is None or equity <= 0 or market_price <= 0:
    return
limit_price = ctx.quantize_price(
    instrument, market_price * 0.995, side=side,
)
if limit_price <= 0:
    return
raw_qty = 0.10 * equity / limit_price
qty = ctx.quantize_qty(instrument, raw_qty)
if qty <= 0:
    return

result = ctx.place_order(
    instrument, side, "Limit", qty, account="alpha", price=limit_price,
)
ctx.state.pending_order_id = result.local_order_id
ctx.state.submit_success = result.success
ctx.state.submit_status = result.status

view = ctx.order_status(ctx.state.pending_order_id)
if view is not None and view.terminal is True:
    ctx.state.pending_order_id = None

Account & positions#

python
ctx.portfolio.nav              # unitless capital-flow-adjusted NAV | None
account = ctx.accounts["main"]    # account-scoped facts and multi-account orders use an explicit key
account.base_coin                  # USD/BTC/ETH/SOL/XRP denomination
account.cash                       # float | None; missing wallet fact stays None
account.equity                     # float | None; base-coin amount for sizing/risk
account.wallet_balance             # float | None; never inferred from equity
account.available_to_trade         # float | None
account.initial_margin             # float | None
account.maintenance_margin         # float | None
account.uni_mmr                    # float | None
account.as_of_ms / as_of_semantics
account.complete / availability / completeness
account.balances[asset].balance    # asset quantity used consistently in every mode
account.leverage                  # (debt value + derivative gross notional) / equity
account.realized_pnl              # float | None; may be unavailable in Live
account.funding_pnl               # float | None; may be unavailable in Live
account.fees_paid                 # float | None; unavailable values stay None
account.nav                       # Live: None/unavailable; deployment NAV is ctx.portfolio.nav
account.positions                 # dict[instrument, Position]

pos = account.positions.get("binance:UPERP:BTCUSDT")
if pos:
    pos.qty                 # signed quantity (negative = short)
    pos.avg_price           # float | None; average entry price when available
    pos.last_price          # float | None; latest mark when available
    pos.unrealized_pnl      # float | None; requires avg + mark
    pos.notional            # float | None; requires mark
            pos.side                # "LONG" / "SHORT"
ONEPORT RESEARCH · DOCUMENTATION