DEV Community

hoon6653
hoon6653

Posted on • Originally published at dev.to

Bybit API Python Tutorial: Authentication, Orders, and Position Management

Bybit API Python Tutorial: Authentication, Orders, and Position Management

The Bybit V5 API is one of the more capable exchange APIs available to retail algo traders — it supports spot, linear and inverse futures, options, and a unified account model, all under a single endpoint hierarchy. The pybit library wraps it cleanly for Python.

This tutorial covers the full workflow for a Python trading bot: authenticating with the Bybit V5 API, placing market and limit orders, monitoring open positions, handling errors and rate limits, and structuring your code for production. No prior exchange API experience required.

By the end you'll have working code for every step. For a production example that uses this exact API under a 3-domain architecture with an AI strategy gate, the full source is at hoon6653/autotrading.


Prerequisites and Setup

Python version: 3.10+ (this tutorial uses 3.11, matching the production server environment)

Bybit account: You'll need API keys from Bybit — start with testnet keys (free, no deposit required).

Install pybit:

pip install pybit
Enter fullscreen mode Exit fullscreen mode

pybit is the official Bybit Python SDK. It handles request signing, timestamp generation, and V5 endpoint routing. Current version: 5.7+.

API Keys:

  1. Log in to testnet.bybit.com (paper money — use this first)
  2. Navigate to Account → API Management → Create New Key
  3. Select "System-generated API Keys"
  4. Permissions needed: Read + Trade (for orders and positions)
  5. Copy the key and secret — you only see the secret once

Testnet vs mainnet: Testnet keys only work with testnet=True. Mainnet keys only work with testnet=False. Mixing them gives retCode: 10003.

Store keys in environment variables, not in source code:

export BYBIT_API_KEY="your_key_here"
export BYBIT_API_SECRET="your_secret_here"
Enter fullscreen mode Exit fullscreen mode

Authenticating with the Bybit V5 API in Python

Initializing the HTTP Session

import os
from pybit.unified_trading import HTTP

session = HTTP(
    testnet=True,  # Switch to False for mainnet
    api_key=os.environ["BYBIT_API_KEY"],
    api_secret=os.environ["BYBIT_API_SECRET"],
)
Enter fullscreen mode Exit fullscreen mode

The HTTP class from pybit.unified_trading targets the Bybit V5 unified endpoint. It handles:

  • HMAC-SHA256 request signing
  • Timestamp injection (avoids clock drift issues)
  • Response parsing to Python dicts

Verifying Authentication

Test with a wallet balance check — this requires a signed request:

try:
    response = session.get_wallet_balance(accountType="UNIFIED")
    print("Auth OK. Balance:", response["result"]["list"][0]["totalEquity"])
except Exception as e:
    print("Auth failed:", e)
Enter fullscreen mode Exit fullscreen mode

Expected success output:

Auth OK. Balance: 10000.0
Enter fullscreen mode Exit fullscreen mode

If you see retCode: 10003, you have a testnet/mainnet key mismatch. See the common Bybit API errors guide for fixes.


Fetching Market Data (No Auth Required)

Some endpoints are public — useful for verifying connectivity before testing authenticated calls.

Current Price

ticker = session.get_tickers(category="linear", symbol="BTCUSDT")
price = float(ticker["result"]["list"][0]["lastPrice"])
print(f"BTC price: ${price:,.2f}")
Enter fullscreen mode Exit fullscreen mode

Instrument Info (for order sizing constraints)

Before placing any order, fetch the instrument's lot size constraints:

info = session.get_instruments_info(category="linear", symbol="BTCUSDT")
lot_filter = info["result"]["list"][0]["lotSizeFilter"]

qty_step = lot_filter["qtyStep"]       # "0.001"
min_qty  = lot_filter["minOrderQty"]   # "0.001"
max_qty  = lot_filter["maxOrderQty"]   # "100"

print(f"Step: {qty_step}, Min: {min_qty}, Max: {max_qty}")
Enter fullscreen mode Exit fullscreen mode

Always round order quantities to qtyStep using Decimal (not round()) to avoid retCode: 10001:

from decimal import Decimal, ROUND_DOWN

def safe_qty(qty: float, step: str) -> str:
    return str(Decimal(str(qty)).quantize(Decimal(step), rounding=ROUND_DOWN))

# Examples:
safe_qty(0.0037, "0.001")  # → "0.003"
safe_qty(0.005,  "0.001")  # → "0.005"
Enter fullscreen mode Exit fullscreen mode

Placing Orders with pybit

Market Order (Linear Futures)

A market order executes immediately at the best available price:

order = session.place_order(
    category="linear",
    symbol="BTCUSDT",
    side="Buy",          # "Buy" or "Sell"
    orderType="Market",
    qty="0.001",         # BTC quantity — must match qtyStep
    timeInForce="IOC",   # Immediate-or-cancel for market orders
)

if order["retCode"] == 0:
    order_id = order["result"]["orderId"]
    print(f"Market order placed: {order_id}")
else:
    print(f"Order failed: {order['retMsg']}")
Enter fullscreen mode Exit fullscreen mode

Market Order (Spot)

Spot orders use category="spot" and can specify quoteOrderQty (spend X USDT) instead of qty:

# Buy with a fixed USDT amount
order = session.place_order(
    category="spot",
    symbol="BTCUSDT",
    side="Buy",
    orderType="Market",
    quoteOrderQty="50",  # Spend 50 USDT
)
Enter fullscreen mode Exit fullscreen mode

Limit Order

A limit order sits in the order book until filled at your specified price or better:

current_price = 65000.0
limit_price = current_price * 0.99  # 1% below current price

order = session.place_order(
    category="linear",
    symbol="BTCUSDT",
    side="Buy",
    orderType="Limit",
    qty="0.001",
    price=str(round(limit_price, 2)),  # Price as string
    timeInForce="GTC",                 # Good-till-cancelled
)
Enter fullscreen mode Exit fullscreen mode

Limit Order with Stop-Loss and Take-Profit

order = session.place_order(
    category="linear",
    symbol="BTCUSDT",
    side="Buy",
    orderType="Limit",
    qty="0.001",
    price="64000",
    timeInForce="GTC",
    stopLoss="62000",          # Auto-close if price drops to 62k
    takeProfit="68000",        # Auto-close if price rises to 68k
    slTriggerBy="LastPrice",   # Trigger condition
    tpTriggerBy="LastPrice",
)
Enter fullscreen mode Exit fullscreen mode

Checking Order Status

order_detail = session.get_order_history(
    category="linear",
    orderId=order_id,
)
status = order_detail["result"]["list"][0]["orderStatus"]
# "New" → open, "Filled" → executed, "Cancelled" → cancelled
print(f"Order status: {status}")
Enter fullscreen mode Exit fullscreen mode

Cancelling an Order

cancel_result = session.cancel_order(
    category="linear",
    symbol="BTCUSDT",
    orderId=order_id,
)
if cancel_result["retCode"] == 0:
    print("Order cancelled")
Enter fullscreen mode Exit fullscreen mode

Managing Open Positions

Fetching All Open Positions

positions = session.get_positions(
    category="linear",
    settleCoin="USDT",  # or specific symbol: symbol="BTCUSDT"
)

for pos in positions["result"]["list"]:
    if float(pos["size"]) > 0:
        print(
            f"{pos['symbol']} {pos['side']}: "
            f"size={pos['size']}, "
            f"entry={pos['avgPrice']}, "
            f"unrealizedPnl={pos['unrealisedPnl']}"
        )
Enter fullscreen mode Exit fullscreen mode

Sample output:

BTCUSDT Buy: size=0.001, entry=64250.5, unrealizedPnl=3.25
Enter fullscreen mode Exit fullscreen mode

Closing a Position

Close by placing a market order on the opposite side with reduceOnly=True:

def close_position(session, symbol: str, size: str, side: str) -> dict:
    """Close an open position at market price."""
    close_side = "Sell" if side == "Buy" else "Buy"
    return session.place_order(
        category="linear",
        symbol=symbol,
        side=close_side,
        orderType="Market",
        qty=size,
        reduceOnly=True,   # Prevents accidentally flipping the position
        timeInForce="IOC",
    )

# Usage
pos = positions["result"]["list"][0]
result = close_position(session, pos["symbol"], pos["size"], pos["side"])
Enter fullscreen mode Exit fullscreen mode

Setting Stop-Loss on an Existing Position

session.set_trading_stop(
    category="linear",
    symbol="BTCUSDT",
    stopLoss="62000",
    slTriggerBy="LastPrice",
    positionIdx=0,  # 0=one-way mode, 1=hedge long, 2=hedge short
)
Enter fullscreen mode Exit fullscreen mode

Error Handling and Rate Limits

Checking retCode on Every Response

Never assume a request succeeded. Always check retCode:

def checked(response: dict, context: str = "") -> dict:
    """Raise on non-zero retCode."""
    if response.get("retCode") != 0:
        raise RuntimeError(
            f"{context} failed: [{response['retCode']}] {response['retMsg']}"
        )
    return response

# Usage
response = checked(
    session.place_order(category="linear", symbol="BTCUSDT", ...),
    context="BTCUSDT market buy"
)
Enter fullscreen mode Exit fullscreen mode

Rate Limit Headers

Bybit returns rate limit info in HTTP response headers:

Header Meaning
X-Bapi-Limit Total allowed requests per window
X-Bapi-Limit-Status Remaining requests
X-Bapi-Limit-Reset-Timestamp Window reset time (ms)

pybit exposes the raw response via .headers if you make raw calls. For most bots, the practical limit is: don't make more than 10 order-related requests per second on a single symbol.

Retry Pattern for Rate Limits (retCode 10006)

import time
import random

def call_with_retry(fn, *args, max_retries=4, **kwargs) -> dict:
    for attempt in range(max_retries):
        result = fn(*args, **kwargs)
        if result.get("retCode") == 10006:
            wait = (2 ** attempt) + random.uniform(0, 0.5)
            time.sleep(wait)
            continue
        return result
    raise RuntimeError("Rate limit retry exhausted")

# Usage
order = call_with_retry(
    session.place_order,
    category="linear",
    symbol="BTCUSDT",
    side="Buy",
    orderType="Market",
    qty="0.001",
)
Enter fullscreen mode Exit fullscreen mode

Common retCode Reference

retCode Meaning Fix
0 Success
10001 Parameter error Check qty step size, price format
10003 Invalid API key Testnet/mainnet key mismatch
10004 Signature error Sync system clock with NTP
10006 Rate limit Exponential backoff
110001 Instrument not found Check symbol spelling
110007 Insufficient balance Check wallet balance
110043 reduceOnly rejected No matching open position to reduce

Structuring for a Real Bot

A one-off script is fine for testing. A production bot needs a few extra patterns.

Config Dataclass

from dataclasses import dataclass

@dataclass
class BotConfig:
    symbol: str = "BTCUSDT"
    category: str = "linear"
    max_position_usdt: float = 100.0   # Max notional per trade
    min_confidence: float = 0.70       # For AI gate integration
    testnet: bool = True               # Flip for mainnet
Enter fullscreen mode Exit fullscreen mode

Session Factory

def make_session(config: BotConfig) -> HTTP:
    return HTTP(
        testnet=config.testnet,
        api_key=os.environ["BYBIT_API_KEY"],
        api_secret=os.environ["BYBIT_API_SECRET"],
    )
Enter fullscreen mode Exit fullscreen mode

Position Size Calculator

Size to notional value — respects max position limit and step size:

def calc_qty(usdt_amount: float, price: float, qty_step: str) -> str:
    raw = usdt_amount / price
    return safe_qty(raw, qty_step)  # safe_qty defined earlier

# Example: $100 at $65,000/BTC with 0.001 step → "0.001"
qty = calc_qty(100.0, 65000.0, "0.001")
Enter fullscreen mode Exit fullscreen mode

Next Steps: From One-Off Script to a Running Bot

This bybit api python tutorial covers the core operations: auth, orders, positions, error handling. The natural next step is to build the decision layer — what tells the bot when to buy or sell.

One approach is rule-based (RSI crossovers, moving average signals). Another is the pattern used in AutoTrading: a fail-closed AI gate that uses Claude Haiku at runtime to select from a set of pre-defined strategies — or explicitly refuse to trade when market conditions are ambiguous.

The fail-closed gate is covered in detail here: Claude Haiku as a Runtime Trading Gate, Not a Bot Builder.

For a deeper look at the pybit library itself — WebSocket, async, session config options — see the pybit Library: Complete Python Developer Guide.

Before going to mainnet, validate your logic on Bybit testnet — paper money, same V5 API.

For a comparison of different bot architectures (AutoTrading vs freqtrade vs Jesse vs Hummingbot), see docs/comparison.md.

The full AutoTrading source — Python 3.11, FastAPI backend, React 19 dashboard, 927 pytest tests, Oracle Cloud deployment — is open source:

github.com/hoon6653/autotrading

Ready to try it? Create a free Bybit account and generate testnet API keys to run the code in this tutorial. Want to compare exchanges? OKX also offers a comprehensive trading API.


FAQ

pybit vs ccxt for Bybit?

Both work. ccxt is a multi-exchange library — good if you want to support Binance, Kraken, and Bybit under one interface. pybit is Bybit-specific and exposes more V5 API features directly (hedge mode, unified account, advanced order types). For a Bybit-only bot, pybit is the better choice.

How do I switch from testnet to mainnet?

Change testnet=True to testnet=False in your HTTP() call and swap your API keys (BYBIT_API_KEY / BYBIT_API_SECRET) for mainnet keys. Nothing else changes.

Can I use async/await with pybit?

Yes. Use pybit.unified_trading.AsyncHTTP instead of HTTP. The method signatures are identical — prefix calls with await. For WebSocket, use pybit.unified_trading.WebSocket with asyncio.

from pybit.unified_trading import AsyncHTTP
import asyncio

async def get_balance():
    session = AsyncHTTP(testnet=True, api_key=..., api_secret=...)
    return await session.get_wallet_balance(accountType="UNIFIED")

asyncio.run(get_balance())
Enter fullscreen mode Exit fullscreen mode

Do I need a VPN or proxy to use the Bybit API?

Bybit blocks access from certain jurisdictions (US, UK, Ontario). If you're in a restricted region, a VPS in a permitted jurisdiction is the standard workaround. The Oracle Cloud server used in AutoTrading runs in Japan.

What's the difference between one-way mode and hedge mode?

One-way mode (default) means you can only hold one direction per symbol at a time. Hedge mode lets you hold a long and short simultaneously on the same symbol. Set via set_leverage with buyLeverage and sellLeverage, or change in Bybit's UI under Derivatives → Position Mode.


SEO Metadata

Title Tag: Bybit API Python Tutorial: Authentication, Orders, and Position Management (79 chars — truncate to: Bybit API Python Tutorial: Auth, Orders, Positions)
Meta Description: Complete Bybit API Python tutorial: authenticate with pybit, place market and limit orders, manage open positions, handle errors and rate limits. Bybit V5 API guide. (167 chars)
Primary Keyword: bybit api python tutorial (used 5×)
Secondary Keywords: bybit v5 api python, pybit library tutorial, bybit python bot, pybit place order

Schema Markup

{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "Bybit API Python Tutorial: Authentication, Orders, and Position Management",
  "description": "Complete guide to using the Bybit V5 API in Python: authenticate with pybit, place market and limit orders, manage positions, and handle errors.",
  "step": [
    {
      "@type": "HowToStep",
      "name": "Install pybit and generate API keys",
      "text": "Run pip install pybit. Generate API keys at testnet.bybit.com. Store keys in environment variables."
    },
    {
      "@type": "HowToStep",
      "name": "Authenticate with the Bybit V5 API",
      "text": "Initialize pybit.unified_trading.HTTP with testnet=True/False and your api_key/api_secret. Verify with get_wallet_balance()."
    },
    {
      "@type": "HowToStep",
      "name": "Fetch instrument constraints",
      "text": "Call get_instruments_info() to get qtyStep and minOrderQty. Round all order quantities down to qtyStep using Decimal.quantize."
    },
    {
      "@type": "HowToStep",
      "name": "Place market and limit orders",
      "text": "Use session.place_order() with category, symbol, side, orderType, qty. Always check retCode == 0 in the response."
    },
    {
      "@type": "HowToStep",
      "name": "Manage open positions",
      "text": "Use get_positions() to list open positions. Close with a reduceOnly market order on the opposite side."
    },
    {
      "@type": "HowToStep",
      "name": "Handle errors and rate limits",
      "text": "Check retCode on every response. Implement exponential backoff for retCode 10006. Use checked() wrapper to raise on failure."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Image Suggestions

  1. Hero: Diagram of the Python → pybit → Bybit V5 API call chain with arrows showing auth → order → position flow
  2. Auth section: Screenshot of Bybit API Management page (testnet vs mainnet toggle visible)
  3. Order section: Side-by-side comparison of market vs limit order book placement
  4. Position section: Screenshot of a parsed position dict in Python terminal output with unrealizedPnl highlighted

Top comments (0)