Universe¤
The Universe
class is central to asset management in hl-sdk
. It provides mappings between asset names and IDs, detailed asset metadata, and utility methods for price and size rounding according to Hyperliquid's exchange rules.
Automatic Universe Loading
When you create an Api
instance using Api.create()
, the SDK automatically makes two API requests to populate the Universe:
perpetual_meta()
- Fetches metadata for all perpetual trading pairsspot_meta()
- Fetches metadata for all spot trading pairs
This ensures the Universe is always up-to-date with the latest available assets and their properties.
Official Hyperliquid Documentation
This page documents the Universe
class which implements Python helpers for Hyperliquid's official asset and pricing rules:
- Asset IDs - How asset identification works for perpetuals, spot, and builder-deployed assets
- Tick and Lot Size - Official price and size formatting rules (5 sig figs, decimal limits, etc.)
The Universe
class automatically handles these complex rules so you don't have to implement them manually.
Overview¤
The Universe contains all the information you need about available assets on the exchange:
- Asset Mappings: Convert between asset names ("BTC") and IDs (0)
- Asset Metadata: Decimals, type (SPOT/PERPETUAL), and other properties
- Rounding Utilities: Automatically round prices and sizes to exchange requirements
Accessing the Universe¤
The Universe is automatically loaded when you create an API instance and is accessible via api.universe
:
import asyncio
from hl import Api
async def main():
api = await Api.create()
# Universe is automatically available
print(f"BTC asset ID: {api.universe.to_asset_id('BTC')}")
print(f"Asset 0 name: {api.universe.to_asset_name(0)}")
asyncio.run(main())
Don't Create Universe Directly
Never instantiate Universe
directly. It's automatically created and populated when you create an Api
instance. The universe data comes from the exchange's metadata endpoints.
Asset Name and ID Conversion¤
Converting Between Names and IDs¤
# Get asset ID from name
btc_id = api.universe.to_asset_id("BTC") # Returns: 0
eth_id = api.universe.to_asset_id("ETH") # Returns: 1
# Get asset name from ID
asset_name = api.universe.to_asset_name(0) # Returns: "BTC"
# Works with both strings and integers
btc_id = api.universe.to_asset_id(0) # Returns: 0 (no change)
btc_name = api.universe.to_asset_name("BTC") # Returns: "BTC" (no change)
Direct Access to Mappings¤
# Access the mapping dictionaries directly
name_to_id = api.universe.name_to_id
id_to_name = api.universe.id_to_name
id_to_info = api.universe.id_to_info
print(f"All asset names: {list(name_to_id.keys())}")
print(f"BTC info: {id_to_info[0]}")
Price and Size Rounding¤
Hyperliquid has specific rules for valid prices and sizes. The Universe automatically handles rounding to comply with these rules.
Price Rounding¤
Prices must satisfy two rules:
- ≤5 significant figures
- ≤pxDecimals
decimal places
from decimal import Decimal
# Round a price to exchange requirements
price = Decimal("65432.123456789")
rounded_price = api.universe.round_price("BTC", price)
print(f"Rounded price: {rounded_price}") # e.g., "65432"
# The method chooses the more restrictive rule
precise_price = Decimal("1.123456789")
rounded = api.universe.round_price("BTC", precise_price)
print(f"Rounded precise price: {rounded}") # e.g., "1.1235"
Size Rounding¤
Sizes must respect the asset's szDecimals
property:
# Round size to valid increments
size = Decimal("0.123456789")
rounded_size = api.universe.round_size("BTC", size)
print(f"Rounded size: {rounded_size}") # e.g., "0.001235" (depends on asset)
# Different assets have different size decimals
eth_size = api.universe.round_size("ETH", Decimal("1.123456789"))
print(f"ETH rounded size: {eth_size}")
Custom Rounding Modes¤
Both methods support custom rounding modes:
from decimal import ROUND_DOWN, ROUND_UP
# Round down (conservative for buys)
conservative_price = api.universe.round_price(
"BTC",
Decimal("65432.7"),
rounding=ROUND_DOWN
)
# Round up (conservative for sells)
conservative_size = api.universe.round_size(
"BTC",
Decimal("0.0015"),
rounding=ROUND_UP
)
Asset Information¤
Asset Types¤
Assets can be either perpetual futures or spot pairs:
# Check asset type
btc_info = api.universe.id_to_info[api.universe.to_asset_id("BTC")]
print(f"BTC type: {btc_info['type']}") # "PERPETUAL"
# Spot assets start from ID 10,000
usdc_info = api.universe.id_to_info[10000] # First spot asset
print(f"Spot asset type: {usdc_info['type']}") # "SPOT"
Decimal Properties¤
Each asset has specific decimal configurations:
btc_info = api.universe.id_to_info[0]
print(f"Price decimals: {btc_info['pxDecimals']}") # Max decimal places for prices
print(f"Size decimals: {btc_info['szDecimals']}") # Max decimal places for sizes
Practical Usage Patterns¤
Safe Order Placement¤
Always round values before placing orders:
async def place_safe_order(api, asset, is_buy, size, price):
# Round to exchange requirements
safe_size = api.universe.round_size(asset, size)
safe_price = api.universe.round_price(asset, price)
return await api.exchange.place_order(
asset=asset,
is_buy=is_buy,
size=safe_size,
limit_price=safe_price,
order_type={"limit": {"tif": "Gtc"}},
reduce_only=False
)
Asset Validation¤
Check if an asset exists before using it:
def is_valid_asset(api, asset):
"""Check if an asset exists in the universe."""
try:
api.universe.to_asset_id(asset)
return True
except KeyError:
return False
# Validate before trading
if is_valid_asset(api, "BTC"):
# Safe to proceed
pass
else:
print("Asset not found!")
Asset Categories¤
Perpetual Assets¤
Perpetual futures have IDs starting from 0:
# Find all perpetual assets
perpetuals = []
for asset_id, info in api.universe.id_to_info.items():
if info['type'] == 'PERPETUAL':
perpetuals.append(info['name'])
print(f"Available perpetuals: {perpetuals}")
Spot Assets¤
Spot pairs have IDs starting from 10,000:
# Find all spot assets
spot_assets = []
for asset_id, info in api.universe.id_to_info.items():
if info['type'] == 'SPOT':
spot_assets.append(info['name'])
print(f"Available spot pairs: {spot_assets}")
Advanced: Manual Universe Creation¤
Advanced Usage Only
This section is for advanced users who need to manually create a Universe instance. In most cases, the Api
class handles this automatically and you should use api.universe
instead.
Creating Universe from Info Instance¤
If you need to manually retrieve the Universe (e.g., for custom transport implementations), you can use the Info
class directly:
from hl import Info
from hl.transport import HttpTransport
from hl.network import MAINNET
async def manual_universe_creation():
# Create Info instance with custom transport
transport = HttpTransport(MAINNET, "info")
info = Info(transport=transport)
# Manually fetch universe - this makes the same API calls as Api.create()
universe = await info.get_universe()
# Now you can use the universe
btc_id = universe.to_asset_id("BTC")
print(f"BTC asset ID: {btc_id}")
return universe
Creating Universe from Raw Metadata¤
For even more control, you can create a Universe from raw metadata responses:
from hl.universe import Universe
async def create_from_raw_meta():
# Fetch metadata manually
perpetual_meta_result = await info.perpetual_meta()
spot_meta_result = await info.spot_meta()
if perpetual_meta_result.is_ok() and spot_meta_result.is_ok():
perpetual_meta = perpetual_meta_result.unwrap()
spot_meta = spot_meta_result.unwrap()
# Create Universe from metadata
universe = Universe.from_perpetual_meta_and_spot_meta(perpetual_meta, spot_meta)
return universe
else:
raise Exception("Failed to fetch metadata")
Use Cases for Manual Creation¤
Manual Universe creation might be useful for:
- Custom caching strategies - Cache Universe data locally
- Offline analysis - Work with saved metadata without live API calls
- Custom transport layers - Use alternative HTTP clients or proxies
- Testing scenarios - Mock Universe data for unit tests
# Example: Caching Universe data
import json
from pathlib import Path
async def cached_universe():
cache_file = Path("universe_cache.json")
if cache_file.exists():
# Load from cache
with open(cache_file) as f:
cached_data = json.load(f)
universe = Universe(cached_data["id_to_info"])
else:
# Fetch fresh and cache
universe = await info.get_universe()
# Save to cache
with open(cache_file, "w") as f:
json.dump({"id_to_info": universe.id_to_info}, f)
return universe
Stick to Api.create()
Unless you have specific advanced requirements, always use Api.create()
which handles Universe creation automatically and ensures all components work together correctly.