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
pybit is the official Bybit Python SDK. It handles request signing, timestamp generation, and V5 endpoint routing. Current version: 5.7+.
API Keys:
- Log in to testnet.bybit.com (paper money — use this first)
- Navigate to Account → API Management → Create New Key
- Select "System-generated API Keys"
- Permissions needed: Read + Trade (for orders and positions)
- 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 withtestnet=False. Mixing them givesretCode: 10003.
Store keys in environment variables, not in source code:
export BYBIT_API_KEY="your_key_here"
export BYBIT_API_SECRET="your_secret_here"
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"],
)
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)
Expected success output:
Auth OK. Balance: 10000.0
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}")
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}")
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"
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']}")
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
)
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
)
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",
)
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}")
Cancelling an Order
cancel_result = session.cancel_order(
category="linear",
symbol="BTCUSDT",
orderId=order_id,
)
if cancel_result["retCode"] == 0:
print("Order cancelled")
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']}"
)
Sample output:
BTCUSDT Buy: size=0.001, entry=64250.5, unrealizedPnl=3.25
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"])
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
)
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"
)
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",
)
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
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"],
)
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")
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())
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."}]}
Image Suggestions
- Hero: Diagram of the Python → pybit → Bybit V5 API call chain with arrows showing auth → order → position flow
- Auth section: Screenshot of Bybit API Management page (testnet vs mainnet toggle visible)
- Order section: Side-by-side comparison of market vs limit order book placement
- Position section: Screenshot of a parsed position dict in Python terminal output with unrealizedPnl highlighted