This bot watches for liquidation events on the Hyperliquid exchange's HIP-3 markets and automatically trades them. HIP-3 markets are special "tokenized asset perpetuals" - these are futures contracts for real-world assets like crude oil, gold, and stocks that trade on a crypto exchange.
The bot runs in a loop, checking every 30 seconds for:
A decentralized exchange (DEX) for trading perpetual futures contracts. Unlike centralized exchanges (like Binance), it runs on a blockchain and you keep custody of your own funds.
A special feature that lets you trade real-world assets (oil, gold, stocks) as perpetual futures on Hyperliquid. These are called "tokenized asset perps."
A type of futures contract that never expires. You can hold it as long as you want. Unlike traditional futures that settle on a specific date, perps use a "funding rate" mechanism to keep the contract price close to the underlying asset price.
def get_h3_position(coin, account, dex='xyz'):
info = get_h3_info(dex=dex, skip_ws=True)
symbol = resolve_h3_symbol(coin, dex=dex, info=info)
user_state = info.user_state(account.address, dex=dex)
for asset in user_state.get('assetPositions', []):
position = asset.get('position', {})
if position.get('coin') == symbol and float(position.get('szi', 0)) != 0:
return position, True
return None, False
What it does: Asks Hyperliquid "do I already have an open position for this coin?" It looks through all your positions and checks if any match the coin you want to trade. Returns the position data and True if found, or None, False if you have no position.
Why it matters: The bot needs to know if you're already in a trade before placing a new one. You don't want to accidentally double your exposure.
def h3_price_to_order_price(coin, px, dex='xyz', info=None):
# Gets the number of decimal places allowed for this asset
info = info or get_h3_info(dex=dex, skip_ws=True)
symbol = resolve_h3_symbol(coin, dex=dex, info=info)
asset = info.name_to_asset(symbol)
sz_decimals = info.asset_to_sz_decimals[asset]
return round(float(f"{float(px):.5g}"), 6 - sz_decimals)
What it does: Different assets have different precision rules. Oil might need 2 decimal places, while a stock might need 4. This function rounds the price to the correct number of decimal places that the exchange expects.
Think of it like: You can't buy gas at $3.123456 per gallon - the pump only shows $3.12. Exchanges have similar rounding rules.
def h3_limit_order(coin, is_buy, sz, limit_px, reduce_only, account, dex='xyz'):
# Connects to exchange, formats the order, and sends it
info = get_h3_info(dex=dex, skip_ws=True)
exchange = get_h3_exchange(account, dex=dex)
symbol = resolve_h3_symbol(coin, dex=dex, info=info)
# Round size to proper decimals
sz = round(float(sz), sz_decimals)
# Round price to proper decimals
limit_px = h3_price_to_order_price(symbol, limit_px, ...)
return exchange.order(
symbol, is_buy, sz, float(limit_px),
{"limit": {"tif": "Gtc"}}, # Good-til-Cancelled
reduce_only=reduce_only,
)
What it does: This is the function that actually places a trade order on Hyperliquid. It takes:
coin - What to trade (e.g., "CL" for crude oil)is_buy - True = go long, False = go shortsz - How big the position should be (size)limit_px - The maximum/minimum price you're willing to payreduce_only - If True, this order can only reduce an existing position, not open a new oneThe "tif": "Gtc" means "Good-til-Cancelled" - the order stays open until it fills or you cancel it.
def cancel_all_h3_orders(account, dex='xyz', coin=None):
orders = get_h3_open_orders(account, ...)
for order in orders:
exchange.cancel(symbol, order['oid']) # Cancel by order ID
What it does: Cancels all your open orders (or just orders for a specific coin). This is important for risk management - if market conditions change, you want to pull your orders quickly.
def close_h3_position(coin, account, dex='xyz', slippage=0.05):
# Uses market order to immediately close the position
return exchange.market_close(symbol, slippage=slippage)
What it does: Closes your position immediately at the current market price. The slippage parameter (default 5%) is the maximum price difference you're willing to accept. This prevents you from getting a terrible price if the market is moving fast.
| Variable | What It Controls | Beginner Tip |
|---|---|---|
| Poll interval | How often the bot checks for signals | 30 seconds is typical. Faster = more API calls. |
| Take Profit (TP) | At what profit % to close the trade | Start with smaller TP for more frequent wins. |
| Stop Loss (SL) | At what loss % to close the trade | Always set this! It limits your maximum loss. |
| Position size | How much money to risk per trade | Start very small while testing. |
Before running this bot, install these Python packages:
pip install hyperliquid-python-sdk ccxt pandas python-dotenv colorama eth-account requests
You also need a .env file with your keys:
PRIVATE_KEY=your_private_key_here
HYPER_LIQUID_KEY=your_hyperliquid_key_here
.env file to a public repository. These keys control your money - anyone who has them can steal your funds.
| Term | Meaning |
|---|---|
| Liquidation | When the exchange force-closes your position because you lost too much |
| Liquidation Cascade | A chain reaction where one liquidation causes price moves that trigger more liquidations |
| HIP-3 | Hyperliquid's system for trading real-world assets (oil, gold, stocks) as crypto perps |
| Perpetual (Perp) | A futures contract that never expires |
| Limit Order | An order that only fills at your specified price or better |
| Market Order | An order that fills immediately at whatever the current price is |
| Slippage | The difference between the price you expected and what you actually got |
| TP (Take Profit) | Automatically close the trade when you've made enough profit |
| SL (Stop Loss) | Automatically close the trade to prevent further losses |
| Long | Betting the price will go up |
| Short | Betting the price will go down |
| DEX | Decentralized Exchange - a trading platform without a central authority |
| OI (Open Interest) | Total value of all outstanding (unclosed) positions |
Below is every significant section of the bot's code translated line-by-line into plain English pseudo-code so you can trace exactly what the program does.
# --- PYTHON ---
DEX = os.getenv("H3_BOT_DEX", "xyz")
SYMBOL = os.getenv("H3_BOT_SYMBOL", "CL")
LEVERAGE = int(os.getenv("H3_BOT_LEVERAGE", "3"))
POSITION_SIZE_USD = float(os.getenv("H3_BOT_POSITION_USD", "15"))
TAKE_PROFIT_PERCENT = float(os.getenv("H3_BOT_TP_PERCENT", "1.5"))
STOP_LOSS_PERCENT = float(os.getenv("H3_BOT_SL_PERCENT", "-1.0"))
MAX_HOLD_HOURS = float(os.getenv("H3_BOT_MAX_HOLD_HOURS", "2"))
LOOP_INTERVAL_SECONDS = int(os.getenv("H3_BOT_LOOP_SECONDS", "30"))
HYPER_LIQUID_KEY = os.getenv("HYPER_LIQUID_KEY")
account = eth_account.Account.from_key(HYPER_LIQUID_KEY)
# --- PSEUDO-CODE ---
SET DEX to the value of environment variable "H3_BOT_DEX", default "xyz"
SET SYMBOL to the value of environment variable "H3_BOT_SYMBOL", default "CL" (crude oil)
SET LEVERAGE to the integer value of env var "H3_BOT_LEVERAGE", default 3
SET POSITION_SIZE_USD to the decimal value of env var "H3_BOT_POSITION_USD", default $15
SET TAKE_PROFIT_PERCENT to the decimal value of env var "H3_BOT_TP_PERCENT", default 1.5%
SET STOP_LOSS_PERCENT to the decimal value of env var "H3_BOT_SL_PERCENT", default -1.0%
SET MAX_HOLD_HOURS to the decimal value of env var "H3_BOT_MAX_HOLD_HOURS", default 2 hours
SET LOOP_INTERVAL_SECONDS to the integer value of env var "H3_BOT_LOOP_SECONDS", default 30 seconds
READ HYPER_LIQUID_KEY from the environment variables (your private key)
CREATE an account object from that private key (this authenticates you on Hyperliquid)
# --- PYTHON ---
def get_h3_position(coin, account, dex='xyz'):
info = get_h3_info(dex=dex, skip_ws=True)
symbol = resolve_h3_symbol(coin, dex=dex, info=info)
user_state = info.user_state(account.address, dex=dex)
for asset in user_state.get('assetPositions', []):
position = asset.get('position', {})
if position.get('coin') == symbol and float(position.get('szi', 0) or 0) != 0:
return position, True
return None, False
# --- PSEUDO-CODE ---
FUNCTION get_h3_position(coin, account, dex):
CONNECT to Hyperliquid's HIP-3 info service
RESOLVE the coin name (e.g. "CL") to the full symbol name used on this DEX
ASK Hyperliquid: "What are all positions for this wallet address?"
FOR EACH position in the response:
GET the position details
IF the position's coin matches our symbol AND the size is NOT zero:
RETURN this position data and TRUE (yes, we have a position)
RETURN None and FALSE (no position found)
# --- PYTHON ---
def h3_limit_order(coin, is_buy, sz, limit_px, reduce_only, account, dex='xyz'):
info = get_h3_info(dex=dex, skip_ws=True)
exchange = get_h3_exchange(account, dex=dex)
symbol = resolve_h3_symbol(coin, dex=dex, info=info)
asset = info.name_to_asset(symbol)
sz_decimals = info.asset_to_sz_decimals[asset]
sz = round(float(sz), sz_decimals)
limit_px = h3_price_to_order_price(symbol, limit_px, dex=dex, info=info)
return exchange.order(symbol, is_buy, sz, float(limit_px),
{"limit": {"tif": "Gtc"}}, reduce_only=reduce_only)
# --- PSEUDO-CODE ---
FUNCTION h3_limit_order(coin, is_buy, size, limit_price, reduce_only, account, dex):
CONNECT to Hyperliquid's HIP-3 info service
CREATE an exchange connection using the account's private key
RESOLVE the coin name to the full exchange symbol
FIND how many decimal places are allowed for the size (e.g. 2 decimals)
ROUND the order size to the allowed number of decimal places
ROUND the limit price to the correct number of decimal places
SEND the order to the exchange with:
- the symbol to trade
- whether this is a BUY or SELL
- the size of the order
- the limit price (maximum price to pay for a buy, minimum to accept for a sell)
- instruction: "Good-til-Cancelled" (keep the order open until it fills or is cancelled)
- whether this order should only reduce an existing position (not open new)
RETURN the exchange's response
# --- PYTHON ---
def get_signal_context():
near_liq = get_moondev_hip3_near_liq_positions(SYMBOL)
liq_stats = get_moondev_hip3_liquidations(LIQUIDATION_TIMEFRAME, symbol=SYMBOL)
momentum = get_momentum_context()
longs = near_liq.get("longs", [])
shorts = near_liq.get("shorts", [])
total_long_value = float(near_liq.get("total_long_value", 0))
total_short_value = float(near_liq.get("total_short_value", 0))
longs_in_range = [pos for pos in longs
if float(pos.get("distance_pct", 999)) <= TRIGGER_DISTANCE_PCT]
shorts_in_range = [pos for pos in shorts
if float(pos.get("distance_pct", 999)) <= TRIGGER_DISTANCE_PCT]
near_long_value = sum(float(pos.get("value", 0)) for pos in longs_in_range)
near_short_value = sum(float(pos.get("value", 0)) for pos in shorts_in_range)
return { ... }
# --- PSEUDO-CODE ---
FUNCTION get_signal_context():
CALL Moon Dev API to get positions that are NEAR liquidation for our asset
CALL Moon Dev API to get historical liquidation stats for our asset
GET the current market momentum (is price going up or down based on 1h candles?)
EXTRACT the list of near-liquidation LONG positions
EXTRACT the list of near-liquidation SHORT positions
CALCULATE total USD value of all near-liquidation longs
CALCULATE total USD value of all near-liquidation shorts
FILTER: keep only longs that are within TRIGGER_DISTANCE_PCT of their liquidation price
FILTER: keep only shorts that are within TRIGGER_DISTANCE_PCT of their liquidation price
SUM the USD value of longs that are very close to liquidation
SUM the USD value of shorts that are very close to liquidation
RETURN a dictionary with all this data:
- near-liquidation positions data
- historical liquidation statistics
- current momentum direction
- total value of longs and shorts near liquidation
- the closest long and closest short to being liquidated
# --- PYTHON ---
def choose_trade(context):
closest_long = context["closest_long"]
closest_short = context["closest_short"]
near_long_value = context["near_long_value"]
near_short_value = context["near_short_value"]
momentum = context["momentum"]
if (near_long_value >= NEAR_LIQ_MIN_TOTAL_USD
and long_distance <= TRIGGER_DISTANCE_PCT
and momentum["bearish"]):
return {"should_trade": True, "is_buy": False, "reason": "Long cascade incoming"}
if (near_short_value >= NEAR_LIQ_MIN_TOTAL_USD
and short_distance <= TRIGGER_DISTANCE_PCT
and momentum["bullish"]):
return {"should_trade": True, "is_buy": True, "reason": "Short squeeze incoming"}
return {"should_trade": False, "is_buy": None, "reason": "No setup"}
# --- PSEUDO-CODE ---
FUNCTION choose_trade(context):
GET the closest long position to being liquidated
GET the closest short position to being liquidated
GET the total value of positions near liquidation (both sides)
GET the current market momentum direction
IF there is a LOT of money in longs about to be liquidated
AND the closest long is VERY close to its liquidation price
AND the market momentum is BEARISH (price going down):
THEN: return "YES, trade! Go SHORT (sell)"
Reason: "A cascade of long liquidations is about to push price down"
IF there is a LOT of money in shorts about to be liquidated
AND the closest short is VERY close to its liquidation price
AND the market momentum is BULLISH (price going up):
THEN: return "YES, trade! Go LONG (buy)"
Reason: "A short squeeze is about to push price up"
OTHERWISE: return "No, don't trade. No setup detected."
# --- PYTHON ---
def manage_open_position():
global position_entry_time
state = get_h3_position_state()
if not state["in_position"]:
position_entry_time = None
return False
if position_entry_time is None:
position_entry_time = datetime.now()
elapsed_hours = (datetime.now() - position_entry_time).total_seconds() / 3600
if state["pnl_perc"] >= TAKE_PROFIT_PERCENT:
close_position(f"take profit hit ({state['pnl_perc']:.2f}%)")
return True
if state["pnl_perc"] <= STOP_LOSS_PERCENT:
close_position(f"stop loss hit ({state['pnl_perc']:.2f}%)")
return True
if elapsed_hours >= MAX_HOLD_HOURS:
close_position(f"max hold reached ({elapsed_hours:.2f}h)")
return True
return True
# --- PSEUDO-CODE ---
FUNCTION manage_open_position():
CHECK: do we currently have an open position?
IF no position:
CLEAR the entry time tracker
RETURN False (nothing to manage)
IF we have a position but no recorded entry time:
RECORD the current time as the entry time
CALCULATE how many hours we've been in this position
IF our profit is at or above the TAKE PROFIT threshold (e.g. +1.5%):
CLOSE the position immediately
RETURN True (position was managed)
IF our loss is at or below the STOP LOSS threshold (e.g. -1.0%):
CLOSE the position immediately
RETURN True (position was managed)
IF we've been in this position longer than MAX HOLD HOURS (e.g. 2 hours):
CLOSE the position regardless of P&L
RETURN True (position was managed)
RETURN True (position is still open, being monitored)
# --- PYTHON ---
def bot():
if manage_open_position():
return
context = get_signal_context()
trade = choose_trade(context)
if not trade["should_trade"]:
return
place_entry(trade["is_buy"], trade["reason"])
if __name__ == "__main__":
try:
bot()
while True:
time.sleep(LOOP_INTERVAL_SECONDS)
bot()
except KeyboardInterrupt:
print("Stopped by user.")
# --- PSEUDO-CODE ---
FUNCTION bot():
STEP 1: Check if we already have an open position
IF yes: manage it (check TP, SL, max hold time), then STOP here
IF no: continue to step 2
STEP 2: Gather all signal data
Get near-liquidation positions from Moon Dev API
Get historical liquidation stats
Get current market momentum
Calculate how much money is near liquidation on each side
STEP 3: Decide whether to trade
Based on the signal data, determine if there's a trading opportunity
IF no opportunity: STOP here, wait for next loop
STEP 4: Place the trade
Calculate the correct order size and price
Cancel any leftover orders
Send the limit order to the exchange
Record the entry time
MAIN PROGRAM:
Run the bot function once
THEN loop forever:
Wait 30 seconds
Run the bot function again
IF the user presses Ctrl+C:
Stop cleanly