Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Trade perpetual futures and spot tokens on Hyperliquid DEX with 18 tools for orders, positions, funding, and USDC transfers.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
tools.py
1"""2Hyperliquid Trading Tools — BaseTool subclasses for agent use.34Info tools (9): hl_account, hl_balances, hl_total_balance, hl_open_orders, hl_market,5hl_orderbook, hl_fills, hl_candles, hl_funding6Exchange tools (10): hl_order, hl_spot_order, hl_cancel, hl_cancel_all,7hl_modify, hl_leverage, hl_transfer_usd, hl_withdraw, hl_deposit,8hl_set_abstraction9"""1011import time12import math13import logging1415from core.tool import BaseTool, ToolContext, ToolResult16from .client import HyperliquidClient1718logger = logging.getLogger(__name__)1920# Module-level shared client instance21_client: HyperliquidClient = None222324def _get_client() -> HyperliquidClient:25global _client26if _client is None:27_client = HyperliquidClient()28return _client293031async def _get_address() -> str:32"""Get the agent's EVM address."""33return await _get_client()._get_address()343536def _coerce_float(value, field_name: str):37"""Best-effort numeric coercion for tolerant tool inputs."""38if isinstance(value, bool) or value is None:39raise ValueError(f"'{field_name}' must be a number")40if isinstance(value, (int, float)):41f = float(value)42elif isinstance(value, str):43s = value.strip()44if not s:45raise ValueError(f"'{field_name}' must be a number")46try:47f = float(s)48except ValueError:49raise ValueError(f"'{field_name}' must be a number")50else:51raise ValueError(f"'{field_name}' must be a number")5253if not math.isfinite(f):54raise ValueError(f"'{field_name}' must be a finite number")55return f565758def _coerce_int(value, field_name: str):59"""Best-effort integer coercion with strict decimal rejection."""60if isinstance(value, bool) or value is None:61raise ValueError(f"'{field_name}' must be an integer")62if isinstance(value, int):63return value64if isinstance(value, float):65if value.is_integer() and math.isfinite(value):66return int(value)67raise ValueError(f"'{field_name}' must be an integer")68if isinstance(value, str):69s = value.strip()70if not s:71raise ValueError(f"'{field_name}' must be an integer")72if s.lstrip("+-").isdigit():73return int(s)74# allow "5.0" but reject "5.1"75try:76f = float(s)77if math.isfinite(f) and f.is_integer():78return int(f)79except ValueError:80pass81raise ValueError(f"'{field_name}' must be an integer")828384def _coerce_bool(value, field_name: str):85"""Best-effort boolean coercion."""86if isinstance(value, bool):87return value88if isinstance(value, (int, float)) and value in (0, 1):89return bool(value)90if isinstance(value, str):91lowered = value.strip().lower()92if lowered in {"true", "1", "yes", "y", "on"}:93return True94if lowered in {"false", "0", "no", "n", "off"}:95return False96raise ValueError(f"'{field_name}' must be a boolean")979899# Accepted aliases for the `side` parameter across all Hyperliquid order tools.100# Some models emit Hyperliquid's L1 wire codes (B/A) or natural-language101# directions (long/short, 做多/做空) instead of the documented buy/sell.102# Normalizing here is safer than silently falling back to sell when the input103# doesn't match the literal "buy" — accidental direction reversal on a104# leveraged order is the worst failure mode we can produce.105_SIDE_BUY_ALIASES = frozenset({106"buy", "b", "bid", "long", "l", "做多",107"1", "true", # some models stringify booleans when unsure108})109_SIDE_SELL_ALIASES = frozenset({110"sell", "s", "a", "ask", "short", "做空",111"0", "false",112})113114115def _coerce_side(value, field_name: str = "side") -> bool:116"""Normalize a `side` parameter into a boolean ``is_buy``.117118Accepts the documented ``buy``/``sell`` plus common aliases that models119reach for when they guess:120121- Buy family: buy, B, bid, long, L, 做多, 1, true122- Sell family: sell, S, A, ask, short, 做空, 0, false123124Also accepts a real ``bool`` (True → buy, False → sell) — some tool125runtimes pre-coerce boolean-looking strings.126127Raises ``ValueError`` for anything else. NEVER falls back to a default —128on a leveraged order, guessing the wrong direction is catastrophic.129"""130# Pre-coerced boolean from the tool runtime or an explicit caller.131if isinstance(value, bool):132return value133134if value is None:135raise ValueError(f"'{field_name}' is required (one of: buy, sell)")136137if isinstance(value, (int, float)) and not isinstance(value, bool):138if value == 1:139return True140if value == 0:141return False142raise ValueError(143f"'{field_name}' numeric must be 1 (buy) or 0 (sell); got {value!r}"144)145146if isinstance(value, str):147s = value.strip().lower()148if not s:149raise ValueError(f"'{field_name}' is required (one of: buy, sell)")150if s in _SIDE_BUY_ALIASES:151return True152if s in _SIDE_SELL_ALIASES:153return False154raise ValueError(155f"'{field_name}' must be one of: buy/sell (also accepted: "156f"B/A, long/short, 做多/做空); got {value!r}"157)158159raise ValueError(160f"'{field_name}' must be a string like 'buy' or 'sell'; "161f"got {type(value).__name__}"162)163164165# ── Info Tools ───────────────────────────────────────────────────────────────166167168class HLAccountTool(BaseTool):169"""Get perp account state — positions, margin, PnL."""170171@property172def name(self) -> str:173return "hl_account"174175@property176def description(self) -> str:177return """Get Hyperliquid perpetual account state: open positions, margin balances, unrealized PnL, and account value.178179**⚠️ WARNING: This tool shows PERP account only! With unified account mode (default), your funds may be in SPOT.**180**🎯 Use `hl_total_balance` instead to check if you have funds to place orders!**181182**Understanding account modes:**183- **Unified Account (default)**: Funds are shared across spot/perp. This tool may show $0 even if you have USDC in spot!184- **Disabled mode**: Spot and perp are separate. This tool accurately shows perp margin.185186**When to use this tool:**187- To check open positions and their PnL188- To see margin usage on perp side189- To monitor position sizes and liquidation risk190- NOT to check if you have funds to place orders (use `hl_total_balance` for that!)191192**🚨 CRITICAL for RWA/Stock Perps (xyz:NVDA, xyz:TSLA, etc.):**193- ✅ Check MAIN account (`hl_account()`) for positions, NOT for available funds194- ❌ DO NOT check xyz dex (`hl_account(dex="xyz")`) before placing orders - it shows $0 until you have positions!195- ✅ xyz dex showing $0 is NORMAL and EXPECTED before your first builder perp order196- ✅ DEX abstraction auto-transfers funds from main → xyz dex when you place orders197- ✅ Only check xyz dex AFTER placing orders to verify positions opened198199Parameters:200- dex: (optional) Builder dex name (e.g. "xyz"). Omit for main crypto perps account.201202Returns: marginSummary (accountValue, totalMarginUsed, totalNtlPos), assetPositions array"""203204@property205def parameters(self) -> dict:206return {207"type": "object",208"properties": {209"dex": {210"type": "string",211"description": "Builder dex name (e.g. 'xyz') for RWA/stock perps. Omit for default crypto perps.",212},213},214}215216async def execute(self, ctx: ToolContext, dex: str = "", **kwargs) -> ToolResult:217try:218client = _get_client()219address = await _get_address()220data = await client.get_account_state(address, dex=dex if dex else None)221return ToolResult(success=True, output=data)222except Exception as e:223return ToolResult(success=False, error=str(e))224225226class HLBalancesTool(BaseTool):227"""Get spot token balances (USDC, tokens)."""228229@property230def name(self) -> str:231return "hl_balances"232233@property234def description(self) -> str:235return """Get Hyperliquid spot balances: USDC and all token holdings.236237**⚠️ WARNING: This tool shows SPOT account only!**238**🎯 Use `hl_total_balance` to check total available funds for trading!**239240**Understanding account modes:**241- **Unified Account (default)**: SPOT USDC is shared collateral for all trading (spot/perp/builder-dexes)242- **Disabled mode**: Spot and perp are separate, you may need to transfer between them243244**When to use this tool:**245- To check spot token holdings (e.g. PURR, HYPE, etc.)246- To see USDC available for spot trading247- NOT as the sole indicator of available margin (use `hl_total_balance` for that!)248249Returns: balances array with coin, hold, total for each token"""250251@property252def parameters(self) -> dict:253return {"type": "object", "properties": {}}254255async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:256try:257client = _get_client()258address = await _get_address()259data = await client.get_spot_state(address)260return ToolResult(success=True, output=data)261except Exception as e:262return ToolResult(success=False, error=str(e))263264265class HLTotalBalanceTool(BaseTool):266"""Get total available balance (accounts for unified account mode)."""267268@property269def name(self) -> str:270return "hl_total_balance"271272@property273def description(self) -> str:274return """Get total available balance across all Hyperliquid accounts.275276**🎯 Use this to check if you have enough funds to place orders!**277278This tool intelligently checks the RIGHT account based on your abstraction mode:279- **Unified Account Mode (default)**: Returns SPOT balance (shared collateral for all trading)280- **Disabled Mode**: Returns PERP balance separately281282**Why use this instead of hl_account or hl_balances?**283- hl_account shows perp only ($0 if funds are in spot with unified account)284- hl_balances shows spot only285- hl_total_balance shows ACTUAL available margin regardless of mode286287Returns:288- totalAvailable: Total USDC available for trading289- abstractionMode: Current mode (unifiedAccount, disabled, etc.)290- breakdown: Where the funds actually are (spot/perp)"""291292@property293def parameters(self) -> dict:294return {"type": "object", "properties": {}}295296async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:297try:298client = _get_client()299address = await _get_address()300301# Check abstraction mode302try:303abstraction_result = await client.get_user_abstraction_state(address)304if isinstance(abstraction_result, str):305abstraction_mode = abstraction_result306elif isinstance(abstraction_result, dict):307abstraction_mode = abstraction_result.get("type", abstraction_result.get("state", "default"))308else:309abstraction_mode = "default"310except Exception:311abstraction_mode = "default"312313# Get spot and perp balances314spot_state = await client.get_spot_state(address)315perp_state = await client.get_account_state(address)316317# Calculate spot USDC318spot_usdc = 0.0319for bal in spot_state.get("balances", []):320if bal.get("coin") == "USDC":321spot_usdc = float(bal.get("total", 0))322break323324# Calculate perp margin325perp_margin = perp_state.get("marginSummary", {})326perp_value = float(perp_margin.get("accountValue", 0))327perp_used = float(perp_margin.get("totalMarginUsed", 0))328perp_available = perp_value - perp_used329330# Determine total available based on mode331if abstraction_mode == "unifiedAccount":332# Unified mode: all USDC is shared collateral333total_available = spot_usdc + perp_available334note = "Unified account: funds are shared across spot/perp/builder-dexes"335else:336# Separate mode: perp and spot are independent337total_available = perp_available338note = "Disabled mode: perp and spot are separate"339340return ToolResult(341success=True,342output={343"totalAvailable": round(total_available, 2),344"abstractionMode": abstraction_mode,345"note": note,346"breakdown": {347"spot": {348"usdc": round(spot_usdc, 2),349},350"perp": {351"accountValue": round(perp_value, 2),352"marginUsed": round(perp_used, 2),353"available": round(perp_available, 2),354},355},356},357)358except Exception as e:359return ToolResult(success=False, error=str(e))360361362class HLOpenOrdersTool(BaseTool):363"""Get all open orders (perp + spot)."""364365@property366def name(self) -> str:367return "hl_open_orders"368369@property370def description(self) -> str:371return """Get all open orders on Hyperliquid (both perp and spot).372373Use this to see pending limit orders, check order status, or find order IDs for cancellation.374375Returns: array of orders with coin, side, sz, limitPx, oid, timestamp"""376377@property378def parameters(self) -> dict:379return {"type": "object", "properties": {}}380381async def execute(self, ctx: ToolContext, **kwargs) -> ToolResult:382try:383client = _get_client()384address = await _get_address()385data = await client.get_open_orders(address)386return ToolResult(success=True, output=data)387except Exception as e:388return ToolResult(success=False, error=str(e))389390391class HLMarketTool(BaseTool):392"""Get market info — mid prices, metadata."""393394@property395def name(self) -> str:396return "hl_market"397398@property399def description(self) -> str:400return """Get Hyperliquid market info: current mid prices and asset metadata.401402If coin is specified, returns targeted info for that asset. Otherwise returns all mid prices.403404**Crypto perps**: Use plain names — "BTC", "ETH", "SOL", etc.405**RWA / stocks / commodities**: Use "xyz:TICKER" format — "xyz:NVDA", "xyz:TSLA", "xyz:AAPL", "xyz:GOLD", "xyz:SILVER", etc.406Use dex="xyz" to list all available RWA/stock perps.407408Parameters:409- coin: (optional) Specific asset like "BTC", "ETH", "xyz:NVDA". Omit for all crypto perps.410- dex: (optional) Builder dex name like "xyz" to list all assets on that dex (stocks/RWAs).411412Returns: mid prices, and if coin specified: maxLeverage, szDecimals"""413414@property415def parameters(self) -> dict:416return {417"type": "object",418"properties": {419"coin": {420"type": "string",421"description": "Asset name (e.g. 'BTC', 'xyz:NVDA'). Omit for all.",422},423"dex": {424"type": "string",425"description": "Builder dex name (e.g. 'xyz') to list all RWA/stock perps on that dex.",426},427},428}429430async def execute(self, ctx: ToolContext, coin: str = "", dex: str = "", **kwargs) -> ToolResult:431try:432client = _get_client()433434# List all assets on a builder dex (e.g. dex="xyz" for RWA/stocks)435if dex and not coin:436await client._ensure_meta()437await client._ensure_builder_dex(dex)438mids = await client.get_all_mids(dex=dex)439return ToolResult(success=True, output=mids)440441if coin and ":" in coin:442# Builder perp (e.g. "xyz:NVDA")443dex_name = coin.split(":", 1)[0]444await client._ensure_meta()445await client._ensure_builder_dex(dex_name)446mids = await client.get_all_mids(dex=dex_name)447# allMids keys are prefixed: "xyz:NVDA"448mid = mids.get(coin)449if mid is None:450return ToolResult(451success=False,452error=f"No mid price for {coin}. Available: {list(mids.keys())[:10]}",453)454meta = client._builder_meta.get(dex_name, {})455universe = meta.get("universe", [])456asset_info = {}457for asset in universe:458# meta returns names like "xyz:NVDA" (prefixed)459if asset["name"] == coin or asset["name"] == coin.split(":", 1)[1]:460asset_info = asset461break462return ToolResult(463success=True,464output={465"coin": coin,466"dex": dex_name,467"midPrice": mid,468"maxLeverage": asset_info.get("maxLeverage"),469"szDecimals": asset_info.get("szDecimals"),470},471)472473mids = await client.get_all_mids()474475if coin:476mid = mids.get(coin)477# Case-insensitive fallback (e.g. "ai16z" -> "AI16Z")478if mid is None:479coin_upper = coin.upper()480for key, val in mids.items():481if key.upper() == coin_upper:482coin = key # Use the canonical name483mid = val484break485if mid is None:486return ToolResult(487success=False,488error=f"No data for {coin}. If this is a stock/RWA, use 'xyz:{coin}' format (e.g. 'xyz:NVDA'). Use hl_market(dex=\"xyz\") to list all available RWA/stock perps.",489)490await client._ensure_meta()491meta = client._meta or {}492universe = meta.get("universe", [])493asset_info = {}494for asset in universe:495if asset["name"] == coin:496asset_info = asset497break498return ToolResult(499success=True,500output={501"coin": coin,502"midPrice": mid,503"maxLeverage": asset_info.get("maxLeverage"),504"szDecimals": asset_info.get("szDecimals"),505},506)507508return ToolResult(success=True, output=mids)509except Exception as e:510return ToolResult(success=False, error=str(e))511512class HLOrderbookTool(BaseTool):513"""Get L2 orderbook snapshot."""514515@property516def name(self) -> str:517return "hl_orderbook"518519@property520def description(self) -> str:521return """Get the L2 orderbook for a Hyperliquid asset.522523Shows current bid/ask levels with sizes. Useful for checking liquidity and spread.524Supports builder perps via "dex:COIN" format (e.g. "xyz:NVDA").525526Parameters:527- coin: Asset name (required, e.g. "BTC", "ETH", "xyz:NVDA")528529Returns: levels array with [[price, size], ...] for bids and asks"""530531@property532def parameters(self) -> dict:533return {534"type": "object",535"properties": {536"coin": {537"type": "string",538"description": "Asset name (e.g. 'BTC', 'xyz:NVDA')",539},540},541"required": ["coin"],542}543544async def execute(self, ctx: ToolContext, coin: str = "", **kwargs) -> ToolResult:545if not coin:546return ToolResult(success=False, error="'coin' is required")547try:548client = _get_client()549data = await client.get_l2_book(coin)550return ToolResult(success=True, output=data)551except Exception as e:552return ToolResult(success=False, error=str(e))553554555class HLFillsTool(BaseTool):556"""Get recent trade fills."""557558@property559def name(self) -> str:560return "hl_fills"561562@property563def description(self) -> str:564return """Get recent trade fills (executed orders) for this wallet.565566Use this to verify if orders were filled, check execution prices, or review trade history.567568Parameters:569- limit: Number of fills to return (default: 20)570571Returns: array of fills with coin, side, px, sz, fee, time"""572573@property574def parameters(self) -> dict:575return {576"type": "object",577"properties": {578"limit": {579"type": "integer",580"description": "Number of fills (default: 20)",581},582},583}584585async def execute(self, ctx: ToolContext, limit: int = 20, **kwargs) -> ToolResult:586try:587client = _get_client()588address = await _get_address()589fills = await client.get_user_fills(address)590return ToolResult(success=True, output=fills[:limit])591except Exception as e:592return ToolResult(success=False, error=str(e))593594595class HLCandlesTool(BaseTool):596"""Get OHLCV candlestick data."""597598@property599def name(self) -> str:600return "hl_candles"601602@property603def description(self) -> str:604return """Get OHLCV candlestick data for a Hyperliquid asset.605606Use this for price analysis, charting, and identifying trends.607608Parameters:609- coin: Asset name (required, e.g. "BTC")610- interval: Candle interval (default: "1h"). Options: 1m, 5m, 15m, 1h, 4h, 1d611- lookback: Hours of history to fetch (default: 24)612613Returns: array of candles with t (time), o, h, l, c (prices), v (volume)"""614615@property616def parameters(self) -> dict:617return {618"type": "object",619"properties": {620"coin": {621"type": "string",622"description": "Asset name (e.g. 'BTC')",623},624"interval": {625"type": "string",626"description": "Candle interval: 1m, 5m, 15m, 1h, 4h, 1d (default: 1h)",627},628"lookback": {629"type": "integer",630"description": "Hours of history (default: 24)",631},632},633"required": ["coin"],634}635636async def execute(637self,638ctx: ToolContext,639coin: str = "",640interval: str = "1h",641lookback: int = 24,642**kwargs,643) -> ToolResult:644if not coin:645return ToolResult(success=False, error="'coin' is required")646try:647client = _get_client()648end = int(time.time() * 1000)649start = end - (lookback * 3600 * 1000)650data = await client.get_candles(coin, interval, start, end)651return ToolResult(success=True, output=data)652except Exception as e:653return ToolResult(success=False, error=str(e))654655656class HLFundingTool(BaseTool):657"""Get funding rate info."""658659@property660def name(self) -> str:661return "hl_funding"662663@property664def description(self) -> str:665return """Get funding rate information for Hyperliquid perps.666667Shows predicted next funding and recent historical funding rates. Positive = longs pay shorts.668669Parameters:670- coin: (optional) Specific asset. Omit for all predicted fundings.671- lookback: Hours of funding history (default: 24, only used if coin specified)672673Returns: predicted funding rates, and if coin specified: historical rates"""674675@property676def parameters(self) -> dict:677return {678"type": "object",679"properties": {680"coin": {681"type": "string",682"description": "Asset name (e.g. 'BTC'). Omit for all.",683},684"lookback": {685"type": "integer",686"description": "Hours of history (default: 24)",687},688},689}690691async def execute(692self, ctx: ToolContext, coin: str = "", lookback: int = 24, **kwargs693) -> ToolResult:694try:695client = _get_client()696predicted = await client.get_predicted_fundings()697698if coin:699# predictedFundings returns [[coin, [[venue, data], ...]], ...]700coin_predicted = None701for entry in predicted:702if isinstance(entry, list) and len(entry) >= 2 and entry[0] == coin:703# entry[1] is [[venue, {fundingRate, ...}], ...]704coin_predicted = {705venue: data706for venue, data in entry[1]707}708break709710# Fetch historical711start = int((time.time() - lookback * 3600) * 1000)712history = await client.get_funding_history(coin, start)713714return ToolResult(715success=True,716output={717"coin": coin,718"predicted": coin_predicted or "No predicted funding found",719"history": history,720},721)722723# Summarize all predicted fundings into a readable dict724summary = {}725for entry in predicted:726if isinstance(entry, list) and len(entry) >= 2:727name = entry[0]728venues = entry[1]729# Pick HlPerp rate if available, otherwise first venue730for venue, data in venues:731if venue == "HlPerp":732summary[name] = data.get("fundingRate", "N/A")733break734else:735if venues:736summary[name] = venues[0][1].get("fundingRate", "N/A")737738return ToolResult(success=True, output={"predicted": summary})739except Exception as e:740return ToolResult(success=False, error=str(e))741742743# ── Exchange Tools ───────────────────────────────────────────────────────────744745746class HLOrderTool(BaseTool):747"""Place a perp limit or market order."""748749@property750def name(self) -> str:751return "hl_order"752753@property754def description(self) -> str:755return """Place a perpetual futures order on Hyperliquid.756757**Crypto**: Use plain names — "BTC", "ETH", "SOL"758**Stocks/RWA**: Use "xyz:TICKER" — "xyz:NVDA", "xyz:TSLA", "xyz:AAPL", "xyz:GOLD"759760Parameters:761- coin: Asset name (required, e.g. "BTC", "ETH", "xyz:NVDA")762- side: Direction. **Use "buy" or "sell"** — these are the documented values.763Aliases are also accepted as a safety net (B/A, long/short, 做多/做空)764but "buy"/"sell" is strongly preferred for clarity and auditability.765An unrecognized value will FAIL the call rather than default, to766prevent accidental direction reversal on leveraged orders.767- size: Order size in base asset (required, e.g. 0.01 for 0.01 BTC)768- price: Limit price (optional — omit for market order)769- order_type: "limit" (GTC, default), "ioc" (fill-or-kill), "alo" (post-only)770- reduce_only: If true, only reduces existing position (default: false)771772Market orders (no price): Uses IoC at mid price +/- 3% slippage.773774Returns: order status with oid, filled size, price"""775776@property777def parameters(self) -> dict:778return {779"type": "object",780"properties": {781"coin": {782"type": "string",783"description": "Asset name (e.g. 'BTC', 'xyz:NVDA')",784},785"side": {786"type": "string",787"enum": ["buy", "sell"],788"description": "Order side",789},790"size": {791"type": "number",792"description": "Order size in base asset",793},794"price": {795"type": "number",796"description": "Limit price (omit for market order)",797},798"order_type": {799"type": "string",800"enum": ["limit", "ioc", "alo"],801"description": "Order type (default: limit)",802},803"reduce_only": {804"type": "boolean",805"description": "Reduce-only (default: false)",806},807},808"required": ["coin", "side", "size"],809}810811async def execute(812self,813ctx: ToolContext,814coin: str = "",815side: str = "",816size: float = 0,817price: float = None,818order_type: str = "limit",819reduce_only: bool = False,820**kwargs,821) -> ToolResult:822try:823if not coin or not side:824return ToolResult(success=False, error="'coin' and 'side' are required")825826size = _coerce_float(size, "size")827if size <= 0:828return ToolResult(success=False, error="'size' must be positive")829830if price is not None:831price = _coerce_float(price, "price")832if price <= 0:833return ToolResult(success=False, error="'price' must be positive when provided")834835reduce_only = _coerce_bool(reduce_only, "reduce_only")836837if isinstance(order_type, str):838order_type = order_type.strip().lower() or "limit"839if order_type not in {"limit", "ioc", "alo"}:840return ToolResult(success=False, error="'order_type' must be one of: limit, ioc, alo")841842is_buy = _coerce_side(side)843logger.info(844"hl_order: coin=%s side_raw=%r → is_buy=%s size=%s price=%s "845"order_type=%s reduce_only=%s",846coin, side, is_buy, size, price, order_type, reduce_only,847)848849client = _get_client()850data = await client.place_order(851coin=coin,852is_buy=is_buy,853size=size,854price=price,855order_type=order_type,856reduce_only=reduce_only,857)858return ToolResult(success=True, output=data)859except ValueError as e:860return ToolResult(success=False, error=str(e))861except Exception as e:862return ToolResult(success=False, error=str(e))863864865class HLSpotOrderTool(BaseTool):866"""Place a spot limit or market order."""867868@property869def name(self) -> str:870return "hl_spot_order"871872@property873def description(self) -> str:874return """Place a spot order on Hyperliquid.875876Parameters:877- coin: Token name (required, e.g. "PURR", "HYPE")878- side: Direction. **Use "buy" or "sell"** — these are the documented values.879Aliases accepted (B/A, long/short, 做多/做空) but "buy"/"sell" is880preferred. Unknown values FAIL rather than default.881- size: Order size in base token (required)882- price: Limit price (optional — omit for market order)883- order_type: "limit" (GTC, default), "ioc" (fill-or-kill), "alo" (post-only)884885Market orders (no price): Uses IoC at mid price +/- 3% slippage.886887Returns: order status with oid, filled size, price"""888889@property890def parameters(self) -> dict:891return {892"type": "object",893"properties": {894"coin": {895"type": "string",896"description": "Token name (e.g. 'PURR', 'HYPE')",897},898"side": {899"type": "string",900"enum": ["buy", "sell"],901"description": "Order side",902},903"size": {904"type": "number",905"description": "Order size in base token",906},907"price": {908"type": "number",909"description": "Limit price (omit for market order)",910},911"order_type": {912"type": "string",913"enum": ["limit", "ioc", "alo"],914"description": "Order type (default: limit)",915},916},917"required": ["coin", "side", "size"],918}919920async def execute(921self,922ctx: ToolContext,923coin: str = "",924side: str = "",925size: float = 0,926price: float = None,927order_type: str = "limit",928**kwargs,929) -> ToolResult:930try:931if not coin or not side:932return ToolResult(success=False, error="'coin' and 'side' are required")933934size = _coerce_float(size, "size")935if size <= 0:936return ToolResult(success=False, error="'size' must be positive")937938if price is not None:939price = _coerce_float(price, "price")940if price <= 0:941return ToolResult(success=False, error="'price' must be positive when provided")942943if isinstance(order_type, str):944order_type = order_type.strip().lower() or "limit"945if order_type not in {"limit", "ioc", "alo"}:946return ToolResult(success=False, error="'order_type' must be one of: limit, ioc, alo")947948is_buy = _coerce_side(side)949logger.info(950"hl_spot_order: coin=%s side_raw=%r → is_buy=%s size=%s price=%s "951"order_type=%s",952coin, side, is_buy, size, price, order_type,953)954955client = _get_client()956data = await client.place_order(957coin=coin,958is_buy=is_buy,959size=size,960price=price,961order_type=order_type,962is_spot=True,963)964return ToolResult(success=True, output=data)965except ValueError as e:966return ToolResult(success=False, error=str(e))967except Exception as e:968return ToolResult(success=False, error=str(e))969970971class HLTPSLOrderTool(BaseTool):972"""Place a stop loss or take profit order."""973974@property975def name(self) -> str:976return "hl_tpsl_order"977978@property979def description(self) -> str:980return """Place a stop loss or take profit order on Hyperliquid.981982**Stop Loss**: Automatically sell when price drops to a trigger level to limit losses.983**Take Profit**: Automatically sell when price rises to a trigger level to lock in gains.984985**Crypto**: Use plain names — "BTC", "ETH", "SOL"986**Stocks/RWA**: Use "xyz:TICKER" — "xyz:NVDA", "xyz:TSLA", "xyz:GOLD"987988Parameters:989- coin: Asset name (required, e.g. "BTC", "ETH", "xyz:NVDA")990- side: Direction to close the position. **Use "buy" or "sell"** — these are991the documented values. For a long position, use "sell" to close; for a992short, use "buy". Aliases accepted (B/A, long/short, 做多/做空) but993"buy"/"sell" is preferred. Unknown values FAIL rather than default.994- size: Order size in base asset (required, e.g. 0.01 for 0.01 BTC)995- trigger_px: Price that triggers the order (required)996- tpsl: "tp" for take profit, "sl" for stop loss (required)997- is_market: If true, execute as market order when triggered. If false, use limit_px. (default: true)998- limit_px: Limit price for the order when it triggers (optional, only used if is_market=false)999- reduce_only: If true, only reduces existing position (default: true)10001001**How it works:**10021. Order sits dormant until market price reaches trigger_px10032. When triggered, executes immediately as market order (or limit if is_market=false)10043. Use reduce_only=true to ensure it only closes positions (recommended for TP/SL)10051006**Examples:**1007- Stop loss: If you're long BTC at $95k and want to exit if it drops to $90k:1008`hl_tpsl_order(coin="BTC", side="sell", size=0.1, trigger_px=90000, tpsl="sl")`10091010- Take profit: If you're long ETH and want to take profit at $3500:1011`hl_tpsl_order(coin="ETH", side="sell", size=1.0, trigger_px=3500, tpsl="tp")`10121013Returns: order status with oid, trigger details"""10141015@property1016def parameters(self) -> dict:1017return {1018"type": "object",1019"properties": {1020"coin": {1021"type": "string",1022"description": "Asset name (e.g. 'BTC', 'xyz:NVDA')",1023},1024"side": {1025"type": "string",1026"enum": ["buy", "sell"],1027"description": "Order side (usually opposite of your position)",1028},1029"size": {1030"type": "number",1031"description": "Order size in base asset",1032},1033"trigger_px": {1034"type": "number",1035"description": "Price that triggers the order",1036},1037"tpsl": {1038"type": "string",1039"enum": ["tp", "sl"],1040"description": "Order type: 'tp' for take profit, 'sl' for stop loss",1041},1042"is_market": {1043"type": "boolean",1044"description": "Execute as market order when triggered (default: true)",1045},1046"limit_px": {1047"type": "number",1048"description": "Limit price when triggered (only if is_market=false)",1049},1050"reduce_only": {1051"type": "boolean",1052"description": "Only reduce position, don't open new (default: true)",1053},1054},1055"required": ["coin", "side", "size", "trigger_px", "tpsl"],1056}10571058async def execute(1059self,1060ctx: ToolContext,1061coin: str = "",1062side: str = "",1063size: float = 0,1064trigger_px: float = 0,1065tpsl: str = "",1066is_market: bool = True,1067limit_px: float = None,1068reduce_only: bool = True,1069**kwargs,1070) -> ToolResult:1071try:1072if not coin or not side or not tpsl:1073return ToolResult(1074success=False,1075error="'coin', 'side', and 'tpsl' are required",1076)10771078size = _coerce_float(size, "size")1079trigger_px = _coerce_float(trigger_px, "trigger_px")1080if size <= 0:1081return ToolResult(success=False, error="'size' must be positive")1082if trigger_px <= 0:1083return ToolResult(success=False, error="'trigger_px' must be positive")10841085is_market = _coerce_bool(is_market, "is_market")1086reduce_only = _coerce_bool(reduce_only, "reduce_only")10871088if isinstance(tpsl, str):1089tpsl = tpsl.strip().lower()1090if tpsl not in ("tp", "sl"):1091return ToolResult(1092success=False,1093error="'tpsl' must be either 'tp' (take profit) or 'sl' (stop loss)",1094)10951096if limit_px is not None:1097limit_px = _coerce_float(limit_px, "limit_px")1098if limit_px <= 0:1099return ToolResult(success=False, error="'limit_px' must be positive when provided")11001101is_buy = _coerce_side(side)11021103# For trigger orders: always pass a limit price1104# - For market triggers: use the trigger price as limit (matches SDK)1105# - For limit triggers: use the specified limit price1106price = trigger_px if is_market else (limit_px or trigger_px)11071108logger.info(1109"hl_tpsl_order: coin=%s side_raw=%r → is_buy=%s tpsl=%s "1110"size=%s trigger_px=%s is_market=%s reduce_only=%s",1111coin, side, is_buy, tpsl, size, trigger_px, is_market, reduce_only,1112)11131114client = _get_client()1115data = await client.place_order(1116coin=coin,1117is_buy=is_buy,1118size=size,1119price=price,1120reduce_only=reduce_only,1121trigger_px=trigger_px,1122tpsl=tpsl,1123)1124return ToolResult(success=True, output=data)1125except ValueError as e:1126return ToolResult(success=False, error=str(e))1127except Exception as e:1128return ToolResult(success=False, error=str(e))112911301131class HLCancelTool(BaseTool):1132"""Cancel an order (perp or spot)."""11331134@property1135def name(self) -> str:1136return "hl_cancel"11371138@property1139def description(self) -> str:1140return """Cancel an open order on Hyperliquid by order ID.11411142Parameters:1143- coin: Asset name (required, e.g. "BTC", "xyz:NVDA")1144- order_id: Order ID to cancel (required — get from hl_open_orders)11451146Returns: cancel confirmation"""11471148@property1149def parameters(self) -> dict:1150return {1151"type": "object",1152"properties": {1153"coin": {1154"type": "string",1155"description": "Asset name (e.g. 'BTC', 'xyz:NVDA')",1156},1157"order_id": {1158"type": "integer",1159"description": "Order ID (oid) to cancel",1160},1161},1162"required": ["coin", "order_id"],1163}11641165async def execute(1166self, ctx: ToolContext, coin: str = "", order_id: int = 0, **kwargs1167) -> ToolResult:1168try:1169if not coin:1170return ToolResult(success=False, error="'coin' is required")11711172order_id = _coerce_int(order_id, "order_id")1173if order_id <= 0:1174return ToolResult(success=False, error="'order_id' must be positive")11751176client = _get_client()1177data = await client.cancel_order(coin, order_id)1178return ToolResult(success=True, output=data)1179except ValueError as e:1180return ToolResult(success=False, error=str(e))1181except Exception as e:1182return ToolResult(success=False, error=str(e))118311841185class HLCancelAllTool(BaseTool):1186"""Cancel all open orders."""11871188@property1189def name(self) -> str:1190return "hl_cancel_all"11911192@property1193def description(self) -> str:1194return """Cancel all open orders on Hyperliquid.11951196Parameters:1197- coin: (optional) Asset name to cancel orders for. Omit to cancel ALL orders.11981199Returns: array of cancel results"""12001201@property1202def parameters(self) -> dict:1203return {1204"type": "object",1205"properties": {1206"coin": {1207"type": "string",1208"description": "Asset name (optional — omit for all)",1209},1210},1211}12121213async def execute(self, ctx: ToolContext, coin: str = "", **kwargs) -> ToolResult:1214try:1215client = _get_client()1216results = await client.cancel_all(coin or None)1217return ToolResult(1218success=True,1219output={1220"cancelled": len(results),1221"results": results,1222},1223)1224except Exception as e:1225return ToolResult(success=False, error=str(e))122612271228class HLModifyTool(BaseTool):1229"""Modify an existing order."""12301231@property1232def name(self) -> str:1233return "hl_modify"12341235@property1236def description(self) -> str:1237return """Modify an existing order on Hyperliquid (change price or size).12381239Parameters:1240- order_id: Order ID to modify (required)1241- coin: Asset name (required, e.g. "BTC", "xyz:NVDA")1242- side: Direction. **Use "buy" or "sell"** — these are the documented values.1243Aliases accepted (B/A, long/short, 做多/做空) but "buy"/"sell" is1244preferred. Unknown values FAIL rather than default.1245- size: New size (required)1246- price: New price (required)12471248Returns: modified order confirmation"""12491250@property1251def parameters(self) -> dict:1252return {1253"type": "object",1254"properties": {1255"order_id": {1256"type": "integer",1257"description": "Order ID to modify",1258},1259"coin": {1260"type": "string",1261"description": "Asset name (e.g. 'BTC', 'xyz:NVDA')",1262},1263"side": {1264"type": "string",1265"enum": ["buy", "sell"],1266"description": "Order side",1267},1268"size": {1269"type": "number",1270"description": "New order size",1271},1272"price": {1273"type": "number",1274"description": "New limit price",1275},1276},1277"required": ["order_id", "coin", "side", "size", "price"],1278}12791280async def execute(1281self,1282ctx: ToolContext,1283order_id: int = 0,1284coin: str = "",1285side: str = "",1286size: float = 0,1287price: float = 0,1288**kwargs,1289) -> ToolResult:1290try:1291if not coin or not side:1292return ToolResult(1293success=False,1294error="'coin' and 'side' are required",1295)12961297order_id = _coerce_int(order_id, "order_id")1298size = _coerce_float(size, "size")1299price = _coerce_float(price, "price")1300if order_id <= 0:1301return ToolResult(success=False, error="'order_id' must be positive")1302if size <= 0:1303return ToolResult(success=False, error="'size' must be positive")1304if price <= 0:1305return ToolResult(success=False, error="'price' must be positive")13061307is_buy = _coerce_side(side)1308logger.info(1309"hl_modify: order_id=%s coin=%s side_raw=%r → is_buy=%s "1310"size=%s price=%s",1311order_id, coin, side, is_buy, size, price,1312)13131314client = _get_client()1315data = await client.modify_order(1316oid=order_id,1317coin=coin,1318is_buy=is_buy,1319size=size,1320price=price,1321)1322return ToolResult(success=True, output=data)1323except ValueError as e:1324return ToolResult(success=False, error=str(e))1325except Exception as e:1326return ToolResult(success=False, error=str(e))132713281329class HLLeverageTool(BaseTool):1330"""Set leverage for a perp."""13311332@property1333def name(self) -> str:1334return "hl_leverage"13351336@property1337def description(self) -> str:1338return """Set leverage for a Hyperliquid perpetual asset.13391340Parameters:1341- coin: Asset name (required, e.g. "BTC", "xyz:NVDA")1342- leverage: Leverage multiplier (required, e.g. 5 for 5x)1343- cross: If true, use cross margin. If false, use isolated margin. (default: true)13441345Returns: leverage update confirmation"""13461347@property1348def parameters(self) -> dict:1349return {1350"type": "object",1351"properties": {1352"coin": {1353"type": "string",1354"description": "Asset name (e.g. 'BTC', 'xyz:NVDA')",1355},1356"leverage": {1357"type": "integer",1358"description": "Leverage multiplier (e.g. 5)",1359},1360"cross": {1361"type": "boolean",1362"description": "Cross margin (default: true)",1363},1364},1365"required": ["coin", "leverage"],1366}13671368async def execute(1369self,1370ctx: ToolContext,1371coin: str = "",1372leverage: int = 0,1373cross: bool = True,1374**kwargs,1375) -> ToolResult:1376try:1377if not coin:1378return ToolResult(success=False, error="'coin' is required")13791380leverage = _coerce_int(leverage, "leverage")1381cross = _coerce_bool(cross, "cross")1382if leverage <= 0:1383return ToolResult(success=False, error="'leverage' must be positive")13841385client = _get_client()1386# Builder perps (HIP-3) require isolated margin1387if ":" in coin:1388cross = False1389data = await client.update_leverage(coin, leverage, is_cross=cross)1390return ToolResult(success=True, output=data)1391except ValueError as e:1392return ToolResult(success=False, error=str(e))1393except Exception as e:1394return ToolResult(success=False, error=str(e))139513961397class HLTransferUsdTool(BaseTool):1398"""Transfer USDC between spot and perp."""13991400@property1401def name(self) -> str:1402return "hl_transfer_usd"14031404@property1405def description(self) -> str:1406return """Transfer USDC between Hyperliquid spot and perp accounts.14071408**⚠️ IMPORTANT: This will FAIL if unified account mode is active!**14091410Unified account mode (default) shares funds automatically - manual transfers are disabled.1411If you get "Action disabled when unified account is active", this is expected behavior.14121413**When to use:**1414- Only when unified account is DISABLED1415- To manually move USDC between spot ↔ perp1416- Not needed when unified account is active (funds are already shared!)14171418**To check your mode:** Use `hl_total_balance` - it shows the current abstraction mode14191420Parameters:1421- amount: USDC amount to transfer (required, e.g. 100.0)1422- to_perp: If true, transfer from spot to perp. If false, from perp to spot. (default: true)14231424Returns: transfer confirmation"""14251426@property1427def parameters(self) -> dict:1428return {1429"type": "object",1430"properties": {1431"amount": {1432"type": "number",1433"description": "USDC amount to transfer",1434},1435"to_perp": {1436"type": "boolean",1437"description": "True = spot→perp, False = perp→spot (default: true)",1438},1439},1440"required": ["amount"],1441}14421443async def execute(1444self, ctx: ToolContext, amount: float = 0, to_perp: bool = True, **kwargs1445) -> ToolResult:1446try:1447amount = _coerce_float(amount, "amount")1448to_perp = _coerce_bool(to_perp, "to_perp")1449if amount <= 0:1450return ToolResult(success=False, error="'amount' must be positive")14511452client = _get_client()1453data = await client.transfer_usd(amount, to_perp=to_perp)1454return ToolResult(success=True, output=data)1455except ValueError as e:1456return ToolResult(success=False, error=str(e))1457except Exception as e:1458return ToolResult(success=False, error=str(e))145914601461class HLWithdrawTool(BaseTool):1462"""Withdraw USDC from Hyperliquid to Arbitrum wallet."""14631464@property1465def name(self) -> str:1466return "hl_withdraw"14671468@property1469def description(self) -> str:1470return """Withdraw USDC from Hyperliquid to an Arbitrum wallet (L1 bridge withdrawal).14711472Fee: 1 USDC (deducted by Hyperliquid). Processing time: ~5 minutes.14731474Parameters:1475- amount: USDC amount to withdraw (required, e.g. 100.0)1476- destination: Target wallet address (optional — defaults to this agent's own wallet)14771478Returns: withdrawal confirmation"""14791480@property1481def parameters(self) -> dict:1482return {1483"type": "object",1484"properties": {1485"amount": {1486"type": "number",1487"description": "USDC amount to withdraw",1488},1489"destination": {1490"type": "string",1491"description": "Target Arbitrum wallet address (default: own wallet)",1492},1493},1494"required": ["amount"],1495}14961497async def execute(1498self, ctx: ToolContext, amount: float = 0, destination: str = "", **kwargs1499) -> ToolResult:1500try:1501amount = _coerce_float(amount, "amount")1502if amount <= 0:1503return ToolResult(success=False, error="'amount' must be positive")15041505client = _get_client()1506data = await client.withdraw_from_bridge(1507amount, destination=destination or None1508)1509return ToolResult(success=True, output=data)1510except ValueError as e:1511return ToolResult(success=False, error=str(e))1512except Exception as e:1513return ToolResult(success=False, error=str(e))151415151516class HLDepositTool(BaseTool):1517"""Deposit USDC from Arbitrum wallet into Hyperliquid."""15181519@property1520def name(self) -> str:1521return "hl_deposit"15221523@property1524def description(self) -> str:1525return """Deposit USDC from this agent's Arbitrum wallet into Hyperliquid.15261527Sends an on-chain ERC-20 transfer of USDC to the Hyperliquid bridge contract.1528Minimum deposit: 5 USDC. Requires USDC balance on Arbitrum.15291530Parameters:1531- amount: USDC amount to deposit (required, minimum 5.0)15321533Returns: approve_tx_hash, transfer_tx_hash, amount_deposited"""15341535@property1536def parameters(self) -> dict:1537return {1538"type": "object",1539"properties": {1540"amount": {1541"type": "number",1542"description": "USDC amount to deposit (minimum 5)",1543},1544},1545"required": ["amount"],1546}15471548async def execute(1549self, ctx: ToolContext, amount: float = 0, **kwargs1550) -> ToolResult:1551try:1552amount = _coerce_float(amount, "amount")1553if amount <= 0:1554return ToolResult(success=False, error="'amount' must be positive")1555if amount < 5:1556return ToolResult(1557success=False,1558error="Minimum Hyperliquid deposit is 5 USDC",1559)15601561client = _get_client()1562data = await client.deposit_usdc(amount)1563return ToolResult(success=True, output=data)1564except ValueError as e:1565return ToolResult(success=False, error=str(e))1566except Exception as e:1567return ToolResult(success=False, error=str(e))156815691570class HLSetAbstractionTool(BaseTool):1571"""Enable or disable unified account (DEX abstraction)."""15721573@property1574def name(self) -> str:1575return "hl_set_abstraction"15761577@property1578def description(self) -> str:1579return """Set Hyperliquid account abstraction mode.15801581**Abstraction modes:**1582- "unifiedAccount": Unified margin across spot and perp (auto-transfers, can't manually transfer)1583- "disabled": Separate spot/perp accounts (can manually transfer between them)1584- "portfolioMargin": Advanced portfolio margin (if eligible)15851586**When to use:**1587- Disable unified account to manually transfer funds and trade builder perps (xyz:NVDA, etc.)1588- Enable unified account for automatic fund management across spot and perp15891590Parameters:1591- mode: Abstraction mode (required: "unifiedAccount", "disabled", or "portfolioMargin")15921593Returns: abstraction update confirmation"""15941595@property1596def parameters(self) -> dict:1597return {1598"type": "object",1599"properties": {1600"mode": {1601"type": "string",1602"enum": ["unifiedAccount", "disabled", "portfolioMargin"],1603"description": "Abstraction mode to set",1604},1605},1606"required": ["mode"],1607}16081609async def execute(1610self, ctx: ToolContext, mode: str = "", **kwargs1611) -> ToolResult:1612if mode not in ("unifiedAccount", "disabled", "portfolioMargin"):1613return ToolResult(1614success=False,1615error="'mode' must be 'unifiedAccount', 'disabled', or 'portfolioMargin'",1616)1617try:1618client = _get_client()1619address = await _get_address()1620# Call the private method directly for setting abstraction1621data = await client._user_set_abstraction(address, mode)1622return ToolResult(success=True, output=data)1623except Exception as e:1624return ToolResult(success=False, error=str(e))1625