Limit Bid Order Tutorial
This guide shows you how to place a limit bid order for a digital asset using the Upbit API.
The full code examples can be found in the Recipes menu.
Click the button above to navigate to the full code Recipe page of this tutorial.
Get Started
In this tutorial, you’ll build a simple automated flow that places a limit buy order at 3% below the current best bid using the Upbit API.
A typical sequence for buying a digital asset on Upbit is:
- Deposit Fiat
- Select the asset to buy
- Check the current orderbook (best bid/ask)
- Enter the price and quantity to create a limit buy order
- Check the order status
In this guide, we’ll follow the same steps: read the current best bid for a target market, calculate a price 3% lower, and place a limit buy order. Python code examples are included.
- Singapore (sg): https://sg-api.upbit.com
- Indonesia (id): https://id-api.upbit.com
- Thailand (th): https://th-api.upbit.com
Authentication Guide
Some APIs require generating a JWT for authentication and including it in the request header when making calls. Please refer to the Authentication documentation and the recipe below to add the authentication header for all Exchange API calls.
Limit Bid Order Tutorial
Check Order Availability
Implement a function that takes a trading pair as input and checks whether Upbit supports trading for it. Use the List Trading Pairs API to verify that the market exists.
def get_trading_pair(trading_pair: str) -> str:
url = "https://sg-api.upbit.com/v1/market/all"
headers = {
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers).json()
trading_pair_list = [
item for item in response if item.get('market') == trading_pair]
if len(trading_pair_list) == 0:
raise ValueError("The trading pair list is empty.")
return trading_pair_list[0].get('market')
When this function runs, it retrieves an array of trading pair objects supported by Upbit. It then compares the value of the market field in each object with the pair entered by the user. If a match is found, the function returns the corresponding market value. If no matching pair exists, the function raises an error.
[
{
"market": "SGD-ETH",
"english_name": "Ethereum",
"market_warning": "NONE"
},
{
"market": "SGD-XRP",
"english_name": "XRP",
"market_warning": "NONE"
},
{
"market": "SGD-BTC",
"english_name": "Bitcoin",
"market_warning": "NONE"
},
{
"market": "SGD-USDT",
"english_name": "Tether",
"market_warning": "NONE"
},
{
"market": "SGD-SOL",
"english_name": "Solana",
"market_warning": "NONE"
}
]
Get Current Orderbook
Implement a function that calls the Orderbook API to fetch the current orderbook for the selected trading pair. In this tutorial, the function extracts and returns the highest bid price from the orderbook data.
def get_best_bid_price(trading_pair: str) -> Decimal:
params = {
"markets": trading_pair
}
url = "https://sg-api.upbit.com/v1/orderbook"
headers = {
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers, params=params).json()
orderbook_units = response[0].get('orderbook_units')
highest_bid_price = Decimal(str(orderbook_units[0].get('bid_price')))
if highest_bid_price is None:
raise ValueError(
"Please check the orderbook. {response}".format(response=response))
else:
return highest_bid_price
When this function runs, it returns the highest bid price for the specified trading pair, as shown below.
144882
Tick Size & Rounding Price
Upbit provides market-specific price tick sizes (SGD, IDR, THB markets). Tick size varies by price range. This guide uses SGD tick sizes only (The tick sizes for IDR and THB require a different implementation. Please refer to the link below to check the tick size rules for each market and implement accordingly).
Implement a function that returns the correct tick size for a given price:
def get_tick_size(price: Decimal) -> Decimal:
if price <= 0:
raise ValueError("price must be > 0")
if price >= Decimal("2000"):
return Decimal("1")
if price < Decimal("0.01"):
return Decimal("0.00001")
decade = int(price.log10().to_integral_value(rounding=ROUND_DOWN))
if decade == 3:
leading = price / (Decimal(10) ** decade)
return Decimal("1") if leading >= Decimal("2") else Decimal("0.5")
elif decade == 2:
base = Decimal("0.1")
leading = price / Decimal("100")
return base if leading >= Decimal("5") else base / Decimal("2")
elif decade == 1:
return Decimal("0.01")
elif decade == 0:
return Decimal("0.005")
elif decade == -1:
return Decimal("0.001")
else:
return Decimal("0.0001")
Using the previously implemented function for retrieving the price unit, implement a function to set the order price. The function rounds down the user-entered digital asset price to the nearest tick size. This ensures that the order price conforms to the tick size rules and helps prevent exceeding the intended order amount.
def round_price_by_tick_size(price: Decimal) -> Decimal:
tick = get_tick_size(price)
return (price // tick) * tick
Create Order
To place a limit buy order, you’ll need the following:
- The trading pair of the asset to buy (e.g., SGD-BTC)
- Order side (buy or sell)
- Order type (limit, market, etc.)
- Purchase price per unit of the digital asset
- Quantity of the digital asset to buy
In this tutorial, we place a limit buy order 3% below the current best bid price.
The order side and order type are therefore set to bid
and limit
.
Since we already implemented a function to calculate valid prices based on tick size rules, you only need to provide the trading pair and quantity. The order price will be adjusted automatically to comply with the required tick size.
• Buy orders placed at ±300% or more from the current market price
• Buy/sell orders exceeding 30 ticks from the market price
• Other internal policies
This function creates a limit buy order using the trading pair, the unit buy price, and the total purchase amount. It then returns the UUID of the created order.
To successfully execute a buy order, your account must have enough balance to cover both the order amount and the corresponding fees. If your available balance matches the order amount exactly but does not include the fee, the order will not be created.
def create_order(
market: str,
price: str,
volume: str
) -> str:
body = {
"market": market,
"side": "bid",
"ord_type": "limit",
"price": price,
"volume": volume,
}
query_string = _build_query_string(body)
jwt_token = _create_jwt(access_key, secret_key, query_string)
url = "https://sg-api.upbit.com/v1/orders"
headers = {
"Authorization": "Bearer {jwt_token}".format(jwt_token=jwt_token),
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=body).json()
uuid = response.get('uuid')
if uuid is None:
raise ValueError("Please check the response. {response}".format(response=response))
else:
return uuid
Check Order Status
Implement a function that takes an order UUID as input and retrieves the current status of the order. In this tutorial, you’ll use it to check the status of the limit bid order.
def get_order(uuid: str) -> Mapping:
params = {
"uuid": uuid
}
query_string = _build_query_string(params)
jwt_token = _create_jwt(access_key, secret_key, query_string)
url = "https://sg-api.upbit.com/v1/order"
headers = {
"Authorization": "Bearer {jwt_token}".format(jwt_token=jwt_token),
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers, params=params).json()
return response
When the function is executed, it returns the order information as shown below.
You can check the order status, such as pending or completed, through the state
field.
{
"uuid": "<your_order_uuid>",
"side": "bid",
"ord_type": "limit",
"price": "153559.00",
"state": "done",
"market": "SGD-BTC",
"created_at": "2025-07-10T13:15:08+09:00",
"volume": "1.0",
"remaining_volume": "0.0",
"prevented_volume": "0",
"reserved_fee": "0.0",
"remaining_fee": "0.0",
"paid_fee": "383.8975",
"locked": "0.0",
"prevented_locked": "0",
"executed_volume": "1.0",
"trades_count": 0,
"trades": []
}
Full Code Example
The following complete example shows how to deposit Fiat and place a limit buy order for a digital asset at a price 3% lower than the current best ask.
Limit Bid Order Tutorial
from urllib.parse import unquote, urlencode
from typing import Any, Union
from collections.abc import Mapping
import hashlib
import uuid
import jwt # PyJWT
import requests
from decimal import Decimal, getcontext, ROUND_FLOOR
access_key = "<YOUR_ACCESS_KEY>"
secret_key = "<YOUR_SECRET_KEY>"
# Please add the logic here to generate the JWT authentication token.
def get_trading_pair(trading_pair: str) -> str:
url = "https://sg-api.upbit.com/v1/market/all"
headers = {
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers).json()
trading_pair_list = [
item for item in response if item.get('market') == trading_pair]
if len(trading_pair_list) == 0:
raise ValueError("The trading pair list is empty.")
return trading_pair_list[0].get('market')
getcontext().prec = 16
def get_best_bid_price(trading_pair: str) -> Decimal:
params = {
"markets": trading_pair
}
url = "https://sg-api.upbit.com/v1/orderbook"
headers = {
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers, params=params).json()
orderbook_units = response[0].get('orderbook_units')
highest_bid_price = Decimal(str(orderbook_units[0].get('bid_price')))
if highest_bid_price is None:
raise ValueError(
"Please check the orderbook. {response}".format(response=response))
else:
return highest_bid_price
def get_tick_size(price: Decimal) -> Decimal:
if price <= 0:
raise ValueError("price must be > 0")
if price >= Decimal("2000"):
return Decimal("1")
if price < Decimal("0.01"):
return Decimal("0.00001")
decade = int(price.log10().to_integral_value(rounding=ROUND_DOWN))
if decade == 3:
leading = price / (Decimal(10) ** decade)
return Decimal("1") if leading >= Decimal("2") else Decimal("0.5")
elif decade == 2:
base = Decimal("0.1")
leading = price / Decimal("100")
return base if leading >= Decimal("5") else base / Decimal("2")
elif decade == 1:
return Decimal("0.01")
elif decade == 0:
return Decimal("0.005")
elif decade == -1:
return Decimal("0.001")
else:
return Decimal("0.0001")
def round_price_by_tick_size(price: Decimal) -> Decimal:
tick = get_tick_size(price)
return (price // tick) * tick
def create_order(
trading_pair: str,
price: str,
volume: str
) -> str:
body = {
"market": trading_pair,
"side": "bid",
"ord_type": "limit",
"price": price,
"volume": volume,
}
query_string = _build_query_string(body)
jwt_token = _create_jwt(access_key, secret_key, query_string)
url = "https://sg-api.upbit.com/v1/orders"
headers = {
"Authorization": "Bearer {jwt_token}".format(jwt_token=jwt_token),
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=body).json()
uuid = response.get('uuid')
if uuid is None:
raise ValueError(
"Please check the response. {response}".format(response=response))
else:
return uuid
def get_order(uuid: str) -> Mapping:
params = {
"uuid": uuid
}
query_string = _build_query_string(params)
jwt_token = _create_jwt(access_key, secret_key, query_string)
url = "https://sg-api.upbit.com/v1/order"
headers = {
"Authorization": "Bearer {jwt_token}".format(jwt_token=jwt_token),
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers, params=params).json()
return response
# if __name__ == '__main__':
# trading_pair = "SGD-BTC"
# volume = "0.0001"
# trading_pair = get_trading_pair(trading_pair)
# orderbook_unit = get_best_bid_price(trading_pair)
# price_3percent_rounded = str(
# round_price_by_tick_size(orderbook_unit * Decimal(0.97)))
# volume = str(Decimal(volume).quantize(
# Decimal('1e-8'), rounding=ROUND_DOWN))
# order_uuid = create_order(trading_pair, price_3percent_rounded, volume)
# order_info = get_order(order_uuid)
# print(order_info)
Updated 6 days ago