Execution·Demo Exchange Trading·Beginner

Bybit Demo Trading

Connect to the Bybit testnet environment for risk-free demo trading with virtual funds, implementing the complete order lifecycle from placement through fill confirmation in a sandbox that exactly mirrors live market data and exchange behavior.

order-executionpaper-tradingtrading-strategies

Bybit Demo Account Trading Integration

Introduction

This notebook demonstrates live order execution using Bybit's Demo Account, a hosted sandbox environment that connects to real market data but operates with virtual funds.

Trading Environment Comparison

ModeReal Market DataReal Exchange ConnectionReal FundsPurpose
Paper TradingOptionalSimulates trading in a closed environment.
Testnet❌ (synthetic)Tests API connectivity and order flow on a separate, non-live exchange.
Demo Account✅ RealTests strategies against live market data without financial risk.
LiveExecutes trades on the live exchange with real capital.

Key Configuration Parameters

  • Client Initialization: pybit.unified_trading.HTTP(demo=True) connects to the demo environment.
  • API Credentials: Same keys as live trading; the demo flag during instantiation makes the difference.
  • Clock Synchronization: recv_window of 30000ms mitigates clock skew issues.

1. Dependency Installation

Required Python packages are installed to ensure notebook functionality.

[ ]
%pip install pybit pandas python-dotenv

# pybit: Python SDK for Bybit API interactions.
# pandas: Data manipulation and analysis library.
# python-dotenv: Manages environment variables for API keys.
Collecting pybit
  Using cached pybit-5.16.0-py2.py3-none-any.whl.metadata (8.3 kB)
Collecting pandas
  Downloading pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl.metadata (79 kB)
Collecting python-dotenv
  Using cached python_dotenv-1.2.2-py3-none-any.whl.metadata (27 kB)
Requirement already satisfied: requests in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from pybit) (2.34.2)
Requirement already satisfied: websocket-client in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from pybit) (1.9.0)
Collecting pycryptodome (from pybit)
  Using cached pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (3.4 kB)
Collecting numpy>=1.26.0 (from pandas)
  Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
Requirement already satisfied: python-dateutil>=2.8.2 in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from requests->pybit) (3.4.7)
Requirement already satisfied: idna<4,>=2.5 in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from requests->pybit) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from requests->pybit) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/neurog/miniforge3/envs/jupyter_env/lib/python3.11/site-packages (from requests->pybit) (2026.5.20)
Using cached pybit-5.16.0-py2.py3-none-any.whl (59 kB)
Downloading pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl (11.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.3/11.3 MB 7.3 MB/s  0:00:01 6.6 MB/s eta 0:00:01
[?25hUsing cached python_dotenv-1.2.2-py3-none-any.whl (22 kB)
Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.9 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 11.1 MB/s  0:00:011.5 MB/s eta 0:00:01:01
[?25hUsing cached pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.3 MB)
Installing collected packages: python-dotenv, pycryptodome, numpy, pybit, pandas
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5/5 [pandas]━━━━ 4/5 [pandas]odome]
Successfully installed numpy-2.4.6 pandas-3.0.3 pybit-5.16.0 pycryptodome-3.23.0 python-dotenv-1.2.2
Note: you may need to restart the kernel to use updated packages.

2. Module Imports

Essential libraries and modules are imported to support API communication, data handling, and utility functions.

[ ]
import os # Operating system interactions, e.g., environment variables
import time # Time-related functions, e.g., sleep
import datetime # Date and time manipulation
import pandas as pd # Data manipulation and analysis
from pybit.unified_trading import HTTP # Bybit Unified Trading API client
from pybit.exceptions import FailedRequestError # Import for Bybit API specific error handling
from requests.exceptions import ReadTimeout, ConnectionError, HTTPError # Exception handling for network errors
from dotenv import load_dotenv # Loads environment variables from a .env file

# Load environment variables from a .env file if present. This is used for API keys.
load_dotenv()
False

3. Trading Parameters Configuration

This section defines the core parameters for the trading strategy and API interaction. These variables control asset selection, order sizing, risk management, and client communication settings.

[ ]
# SYMBOL: Trading pair for linear perpetual contracts.
SYMBOL          = "BTCUSDT"

# CATEGORY: Bybit product category (e.g., 'linear' for USDT perpetuals).
CATEGORY        = "linear"

# SIZE_PERCENT: Percentage of the available balance to allocate per trade order.
SIZE_PERCENT    = 10

# TP_PERCENT: Take-profit percentage relative to the entry price. Determines the target profit level.
TP_PERCENT      = 1.5

# SL_PERCENT: Stop-loss percentage relative to the entry price. Determines the maximum acceptable loss.
SL_PERCENT      = 0.8

# RECV_WINDOW: Request validity window in milliseconds. A higher value (e.g., 30000ms) mitigates clock skew issues, preventing API request rejection.
RECV_WINDOW     = 30000

4. API Client Initialization

This section outlines the functions responsible for initializing the Bybit API client and ensuring robust communication through retry mechanisms.

build_demo_client

[ ]
def build_demo_client(recv_window: int = 30000) -> HTTP:
    """
    Create a Bybit HTTP client connected to the Demo Account environment.

    Parameters
    ----------
    recv_window : Request validity window in milliseconds.
                  Increase above default (5000) if clock sync issues occur.

    Returns
    -------
    pybit HTTP client instance in demo mode.
    """
    return HTTP(
        api_key    = "YOUR-API_KEY",
        api_secret = "YOUR-API-SECRET",
        demo       = True,
        recv_window= recv_window,
    )

safe_request

[ ]
def safe_request(fn, retries: int = 5, delay: float = 5.0):
    """
    Execute an API call with retry on transient network/API errors.

    Parameters
    ----------
    fn      : Zero-argument callable wrapping the API call.
    retries : Maximum retry attempts.
    delay   : Seconds to wait between retries.

    Notes
    -----
    Bybit returns the SAME 403 FailedRequestError for both a transient
    IP rate-limit hit and a permanent geo-compliance block (non-US IP
    required for demo/derivatives access). Retrying the latter is
    pointless and just burns more requests against the rate limit, so
    we inspect the error message and fail fast with a clear explanation
    when it's the geo-block case.
    """
    for attempt in range(retries):
        try:
            return fn()
        except (ReadTimeout, ConnectionError, HTTPError) as e:
            if attempt < retries - 1:
                print(f"[RETRY {attempt+1}] Network error: {e}. Retrying in {delay}s.")
                time.sleep(delay)
            else:
                raise e
        except FailedRequestError as e:
            message = str(getattr(e, "message", e)).lower()
            if "usa" in message:
                # Permanent compliance block — not fixable by retrying.
                raise RuntimeError(
                    "Bybit rejected this request because it believes it originated "
                    "from a US IP address. This is a regulatory block on Bybit's "
                    "side (demo/derivatives trading isn't available to US-located "
                    "accounts), not a bug in this notebook. Retrying will not help — "
                    "verify your network's egress location, or contact Bybit support "
                    "if you believe this is a false positive."
                ) from e
            if attempt < retries - 1:
                print(f"[RETRY {attempt+1}] API error: {e}. Retrying in {delay}s.")
                time.sleep(delay)
            else:
                raise e

5. Account Information Retrieval

Functions in this section are designed to query and retrieve critical account-specific data, including current balance and instrument trading parameters.

get_demo_balance

[ ]
def get_demo_balance(client: HTTP) -> float:
    """
    Retrieve the USDT equity balance from the Unified Demo Account.

    Returns
    -------
    Total account equity in USDT.
    """
    resp    = safe_request(lambda: client.get_wallet_balance(accountType="UNIFIED"))
    equity  = resp["result"]["list"][0]["totalEquity"]
    print(f"Demo Account Equity: {equity} USDT")
    return float(equity)

get_instrument_info

[ ]
def get_instrument_info(client: HTTP, symbol: str) -> dict:
    """
    Retrieve tick size, step size, and order quantity limits for a symbol.

    Returns
    -------
    dict with keys: tick_size, step_size, min_qty, max_mkt_qty
    """
    resp = safe_request(lambda: client.get_instruments_info(category="linear", symbol=symbol))
    item = resp["result"]["list"][0]
    return {
        "tick_size":   float(item["priceFilter"]["tickSize"]),
        "step_size":   float(item["lotSizeFilter"]["qtyStep"]),
        "min_qty":     float(item["lotSizeFilter"]["minOrderQty"]),
        "max_mkt_qty": float(item["lotSizeFilter"]["maxMktOrderQty"]),
    }

6. Order Placement and Management Functions

This section provides utilities for executing various order types, monitoring open positions, and reviewing past trading activity on the demo account.

place_demo_market_order

[ ]
def place_demo_market_order(client: HTTP, symbol: str,
                             side: str, qty: float) -> str:
    """
    Place a market order on the Bybit Demo Account.

    Parameters
    ----------
    client : Authenticated demo HTTP client.
    symbol : Ticker, e.g. 'BTCUSDT'.
    side   : 'Buy' | 'Sell'.
    qty    : Order quantity in base asset units.

    Returns
    -------
    Order ID string.
    """
    resp = safe_request(lambda: client.place_order(
        category="linear",
        symbol=symbol,
        side=side,
        orderType="Market",
        qty=str(qty),
    ))
    order_id = resp["result"]["orderId"]
    print(f"[ORDER] Market {side} | {qty} {symbol} | ID: {order_id}")
    return order_id

place_demo_limit_order

[ ]
def place_demo_limit_order(client: HTTP, symbol: str,
                            side: str, qty: float, price: float) -> str:
    """
    Place a limit order on the Bybit Demo Account.

    Parameters
    ----------
    client : Authenticated demo HTTP client.
    symbol : Ticker.
    side   : 'Buy' | 'Sell'.
    qty    : Order quantity.
    price  : Limit price.

    Returns
    -------
    Order ID string.
    """
    resp = safe_request(lambda: client.place_order(
        category="linear",
        symbol=symbol,
        side=side,
        orderType="Limit",
        qty=str(qty),
        price=str(price),
        timeInForce="GTC",
    ))
    order_id = resp["result"]["orderId"]
    print(f"[ORDER] Limit {side} | {qty} {symbol} @ {price} | ID: {order_id}")
    return order_id

get_open_demo_positions

[ ]
def get_open_demo_positions(client: HTTP, symbol: str) -> list:
    """Return active positions for the symbol on the demo account."""
    resp = safe_request(lambda: client.get_positions(category="linear", symbol=symbol))
    positions = resp["result"]["list"]
    return [p for p in positions if p.get("avgPrice", "0") not in ("", "0")]

get_demo_order_history

[ ]
def fetch_demo_transactions(client: HTTP, symbol: str,
                              start_date: str = None,
                              end_date:   str = None) -> pd.DataFrame:
    """
    Fetch transaction log and closed PnL data from the Bybit Demo Account.

    Implements date-range pagination in 7-day windows as required by the API.

    Parameters
    ----------
    client     : Authenticated demo HTTP client.
    symbol     : Asset symbol (without USDT suffix), e.g. 'BTC'.
    start_date : 'YYYY-MM-DD' string. Defaults to 2025-01-01.
    end_date   : 'YYYY-MM-DD' string. Defaults to now.

    Returns
    -------
    DataFrame of transactions with columns:
    datetime, type, symbol, qty, cashFlow, fee, side
    """
    from datetime import datetime, timedelta, timezone

    def to_ms(dt): return int(dt.timestamp() * 1000)

    start = datetime.strptime(start_date, "%Y-%m-%d") if start_date else datetime(2025, 1, 1)
    end   = datetime.strptime(end_date,   "%Y-%m-%d") if end_date   else datetime.now(timezone.utc).replace(tzinfo=None)

    transactions = []
    tx_types     = ["TRADE", "TRANSFER_IN", "TRANSFER_OUT"]

    current = start
    while current < end:
        next_window = min(current + timedelta(days=7), end)
        for tx_type in tx_types:
            cursor = None
            while True:
                params = {
                    "accountType": "UNIFIED",
                    "category":    "linear",
                    "currency":    "USDT",
                    "limit":       100,
                    "startTime":   to_ms(current),
                    "endTime":     to_ms(next_window),
                    "type":        tx_type,
                }
                if cursor:
                    params["cursor"] = cursor

                result  = safe_request(lambda: client.get_transaction_log(**params))
                tx_list = result.get("result", {}).get("list", [])

                if not tx_list:
                    break
                for tx in tx_list:
                    tx["transactionType"] = tx_type
                transactions.extend(tx_list)

                next_cursor = result.get("result", {}).get("nextPageCursor")
                if not next_cursor or next_cursor == cursor:
                    break
                cursor = next_cursor

        current = next_window

    df = pd.DataFrame(transactions)
    if not df.empty and "transactionTime" in df.columns:
        df["datetime"] = pd.to_datetime(df["transactionTime"], unit="ms")
    return df

7. Transaction and PnL History Retrieval

This section provides a function to fetch detailed transaction logs and closed Profit and Loss (PnL) data from the Bybit Demo Account. The implementation includes pagination handling for extensive historical data retrieval.

fetch_demo_transactions

[ ]
def fetch_demo_transactions(client: HTTP, symbol: str,
                              start_date: str = None,
                              end_date:   str = None) -> pd.DataFrame:
    """
    Fetch transaction log and closed PnL data from the Bybit Demo Account.

    Implements date-range pagination in 7-day windows as required by the API.

    Parameters
    ----------jupyter notebook

    client     : Authenticated demo HTTP client.
    symbol     : Asset symbol (without USDT suffix), e.g. 'BTC'.
    start_date : 'YYYY-MM-DD' string. Defaults to 2025-01-01.
    end_date   : 'YYYY-MM-DD' string. Defaults to now.

    Returns
    -------
    DataFrame of transactions with columns:
    datetime, type, symbol, qty, cashFlow, fee, side
    """
    from datetime import datetime, timedelta, timezone

    def to_ms(dt): return int(dt.timestamp() * 1000)

    start = datetime.strptime(start_date, "%Y-%m-%d") if start_date else datetime(2025, 1, 1)
    end   = datetime.strptime(end_date,   "%Y-%m-%d") if end_date   else datetime.now(timezone.utc).replace(tzinfo=None)

    transactions = []
    tx_types     = ["TRADE", "TRANSFER_IN", "TRANSFER_OUT"]

    current = start
    while current < end:
        next_window = min(current + timedelta(days=7), end)
        for tx_type in tx_types:
            cursor = None
            while True:
                params = {
                    "accountType": "UNIFIED",
                    "category":    "linear",
                    "currency":    "USDT",
                    "limit":       100,
                    "startTime":   to_ms(current),
                    "endTime":     to_ms(next_window),
                    "type":        tx_type,
                }
                if cursor:
                    params["cursor"] = cursor

                result  = safe_request(lambda: client.get_transaction_log(**params))
                tx_list = result.get("result", {}).get("list", [])

                if not tx_list:
                    break
                for tx in tx_list:
                    tx["transactionType"] = tx_type
                transactions.extend(tx_list)

                next_cursor = result.get("result", {}).get("nextPageCursor")
                if not next_cursor or next_cursor == cursor:
                    break
                cursor = next_cursor

        current = next_window

    df = pd.DataFrame(transactions)
    if not df.empty and "transactionTime" in df.columns:
        df["datetime"] = pd.to_datetime(df["transactionTime"], unit="ms")
    return df
[ ]

8. Algorithmic Trading Strategy Example

This section presents an example of an automated trading workflow, demonstrating the integration of the previously defined functions to execute a simple market order strategy on the Bybit Demo Account. The strategy involves the following sequential steps:

  1. Client Initialization: Establish an authenticated connection to the Bybit Demo Account API.
  2. Balance Inquiry: Retrieve the current USDT equity balance to determine available trading capital.
  3. Instrument Metadata Retrieval: Obtain specific trading rules for the selected SYMBOL, including tick_size (minimum price increment) and step_size (minimum quantity increment). This information is crucial for calculating valid order parameters.
  4. Order Size Calculation: Based on the available balance, a predefined SIZE_PERCENT, and the current market price, compute the order quantity. The quantity is rounded to align with the instrument's step_size.
  5. Market Order Placement: Execute a market buy order for the calculated quantity of the SYMBOL.
  6. Position Verification: After a brief delay, query the account for any newly opened positions to confirm order execution.
  7. Position Closure (if open): If an open position is detected, initiate a market sell order to close the position entirely, thereby neutralizing market exposure.

This sequence illustrates a basic cycle of capital allocation, trade execution, and position management within the demo environment.

[ ]
# Initialize the HTTP client for the demo trading environment.
client = build_demo_client()

# 1. Retrieve the current USDT balance from the demo account.
balance = get_demo_balance(client)

# 2. Obtain trading instrument metadata (e.g., tick size, step size, quantity limits).
info = get_instrument_info(client, SYMBOL)

# 3. Calculate the order quantity based on a percentage of the balance and instrument rules.
current_price  = float(safe_request(lambda: client.get_tickers(category="linear", symbol=SYMBOL))["result"]["list"][0]["markPrice"])
raw_qty        = (balance * SIZE_PERCENT / 100) / current_price
step_size_decimals = len(str(info["step_size"]).split(".")[1]) if '.' in str(info["step_size"]) else 0
qty            = round(raw_qty, step_size_decimals)

# 4. Place a market buy order for the calculated quantity.
order_id = place_demo_market_order(client, SYMBOL, "Buy", qty)
time.sleep(2)

# 5. Check for currently open positions for the specified symbol.
positions = get_open_demo_positions(client, SYMBOL)

# 6. If an open position exists, close it with a market sell order.
if positions:
    close_qty = float(positions[0]["size"])
    place_demo_market_order(client, SYMBOL, "Sell", close_qty)
Demo Account Equity: 19496.21230036 USDT
[ORDER] Market Buy | 0.03 BTCUSDT | ID: dd35e59f-f247-4c99-a809-7654f6b21d72
[ORDER] Market Sell | 0.03 BTCUSDT | ID: ac064844-9221-40b7-886a-83167f594364
[ ]
[ ]
Bybit Demo Trading · BitPredict