├── .python-version ├── src ├── __init__.py ├── core │ ├── __init__.py │ └── key_manager.py ├── utils │ ├── __init__.py │ ├── exceptions.py │ └── events.py ├── exchanges │ ├── hyperliquid │ │ ├── __init__.py │ │ └── market_data.py │ └── __init__.py ├── strategies │ ├── grid │ │ ├── __init__.py │ │ └── basic_grid.py │ └── __init__.py ├── interfaces │ ├── __init__.py │ ├── strategy.py │ └── exchange.py └── run_bot.py ├── pyproject.toml ├── .gitignore ├── bots └── btc_conservative.yaml ├── learning_examples ├── 06_copy_trading │ ├── order_scenarios │ │ ├── cancel_order.py │ │ ├── cancel_orders.py │ │ ├── modify_order.py │ │ ├── modify_orders.py │ │ ├── place_order.py │ │ ├── cancel_twap_order.py │ │ ├── place_twap_order.py │ │ └── place_orders_limit.py │ ├── print_raw_websocket_messages.py │ └── print_parsed_user_events.py ├── 02_market_data │ ├── get_all_prices.py │ └── get_market_metadata.py ├── 03_account_info │ ├── get_user_state.py │ └── get_open_orders.py ├── 04_trading │ ├── place_limit_order.py │ └── cancel_orders.py ├── 01_websockets │ ├── realtime_prices.py │ └── realtime_prices_multiple_subs.py └── 05_funding │ ├── README.md │ ├── check_spot_perp_availability.py │ └── get_funding_rates.py ├── .env.example ├── README.md ├── AGENTS.md ├── CLAUDE.md └── LICENSE /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # Hyperliquid Trading Framework 2 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | # Core framework components 2 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Utility functions and classes 2 | -------------------------------------------------------------------------------- /src/exchanges/hyperliquid/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hyperliquid Exchange Integration 3 | 4 | Technical implementation of Hyperliquid DEX integration. 5 | Separated from business logic for clean architecture. 6 | """ 7 | 8 | from .adapter import HyperliquidAdapter 9 | from .market_data import HyperliquidMarketData 10 | 11 | __all__ = ["HyperliquidAdapter", "HyperliquidMarketData"] 12 | -------------------------------------------------------------------------------- /src/strategies/grid/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Grid Trading Strategies 3 | 4 | This module contains grid-based trading strategies: 5 | - BasicGridStrategy: Grid strategy with geometric spacing and rebalancing 6 | """ 7 | 8 | from .basic_grid import BasicGridStrategy, GridState, GridLevel, GridConfig 9 | 10 | __all__ = ["BasicGridStrategy", "GridState", "GridLevel", "GridConfig"] 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "hyperliquid-trading-bot" 3 | version = "0.1.0" 4 | description = "Extensible trading bot for Hyperliquid DEX" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "hyperliquid-python-sdk>=0.20.0", 9 | "eth-account>=0.10.0", 10 | "pyyaml>=6.0", 11 | "typing-extensions>=4.0", 12 | "psutil>=7.0.0", 13 | "httpx>=0.28.1", 14 | "python-dotenv>=1.1.1", 15 | "websockets>=15.0.1", 16 | ] 17 | 18 | [project.optional-dependencies] 19 | test = [ 20 | "pytest>=7.0", 21 | "pytest-asyncio>=0.21", 22 | "pytest-mock>=3.0", 23 | ] 24 | dev = [ 25 | "black", 26 | "isort", 27 | "mypy", 28 | ] 29 | -------------------------------------------------------------------------------- /src/interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interfaces for extending the trading system. 3 | 4 | These interfaces define clear contracts for adding: 5 | - New trading strategies (implement TradingStrategy) 6 | - New exchanges/DEXes (implement ExchangeAdapter) 7 | """ 8 | 9 | from .strategy import TradingStrategy, TradingSignal, SignalType, MarketData, Position 10 | from .exchange import ( 11 | ExchangeAdapter, 12 | Order, 13 | OrderSide, 14 | OrderType, 15 | OrderStatus, 16 | Balance, 17 | MarketInfo, 18 | ) 19 | 20 | __all__ = [ 21 | # Strategy interface 22 | "TradingStrategy", 23 | "TradingSignal", 24 | "SignalType", 25 | "MarketData", 26 | "Position", 27 | # Exchange interface 28 | "ExchangeAdapter", 29 | "Order", 30 | "OrderSide", 31 | "OrderType", 32 | "OrderStatus", 33 | "Balance", 34 | "MarketInfo", 35 | ] 36 | -------------------------------------------------------------------------------- /src/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | class TradingFrameworkError(Exception): 2 | """Base exception for the trading framework""" 3 | 4 | pass 5 | 6 | 7 | class ConfigurationError(TradingFrameworkError): 8 | """Raised when configuration is invalid""" 9 | 10 | pass 11 | 12 | 13 | class StrategyError(TradingFrameworkError): 14 | """Raised when strategy encounters an error""" 15 | 16 | pass 17 | 18 | 19 | class ExchangeError(TradingFrameworkError): 20 | """Raised when exchange operations fail""" 21 | 22 | pass 23 | 24 | 25 | class OrderError(TradingFrameworkError): 26 | """Raised when order operations fail""" 27 | 28 | pass 29 | 30 | 31 | class PositionError(TradingFrameworkError): 32 | """Raised when position operations fail""" 33 | 34 | pass 35 | 36 | 37 | class GridError(TradingFrameworkError): 38 | """Raised when grid trading operations fail""" 39 | 40 | pass 41 | 42 | 43 | class TradingError(TradingFrameworkError): 44 | """Raised for general trading operations errors""" 45 | 46 | pass 47 | -------------------------------------------------------------------------------- /src/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Trading Strategies 3 | 4 | Business logic for different trading strategies. 5 | Add new strategies by implementing the TradingStrategy interface. 6 | 7 | Available strategies: 8 | - BasicGridStrategy: Grid trading with geometric spacing and rebalancing 9 | """ 10 | 11 | from .grid import BasicGridStrategy 12 | 13 | # Strategy registry - makes it easy to add new strategies 14 | STRATEGY_REGISTRY = { 15 | "basic_grid": BasicGridStrategy, 16 | "grid": BasicGridStrategy, # Alias 17 | } 18 | 19 | 20 | def create_strategy(strategy_type: str, config: dict): 21 | """ 22 | Factory function to create strategies. 23 | 24 | Makes it easy for newbies to add new strategies: 25 | 1. Implement TradingStrategy interface 26 | 2. Add to STRATEGY_REGISTRY 27 | 3. Done! 28 | """ 29 | if strategy_type not in STRATEGY_REGISTRY: 30 | available = ", ".join(STRATEGY_REGISTRY.keys()) 31 | raise ValueError( 32 | f"Unknown strategy type: {strategy_type}. Available: {available}" 33 | ) 34 | 35 | strategy_class = STRATEGY_REGISTRY[strategy_type] 36 | return strategy_class(config) 37 | 38 | 39 | __all__ = ["BasicGridStrategy", "STRATEGY_REGISTRY", "create_strategy"] 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv/ 11 | venv/ 12 | env/ 13 | 14 | # Environment and configuration files 15 | .env 16 | .env.local 17 | .env.production 18 | .env.development 19 | 20 | # Private keys and secrets 21 | .private_key 22 | *.key 23 | *.pem 24 | private_keys/ 25 | secrets/ 26 | 27 | # Trading data and logs 28 | data/ 29 | logs/ 30 | *.log 31 | *.csv 32 | trade_history/ 33 | performance_data/ 34 | 35 | # Bot state and cache files 36 | state/ 37 | cache/ 38 | *.json.backup 39 | *_state.json 40 | *_backup.json 41 | 42 | # Temporary files 43 | tmp/ 44 | temp/ 45 | *.tmp 46 | *.swp 47 | *.swo 48 | *~ 49 | 50 | # IDE and editor files 51 | .vscode/ 52 | .idea/ 53 | *.sublime-* 54 | .DS_Store 55 | Thumbs.db 56 | 57 | # OS-specific files 58 | *.DS_Store 59 | *.DS_Store? 60 | ._* 61 | .Spotlight-V100 62 | .Trashes 63 | ehthumbs.db 64 | Thumbs.db 65 | 66 | # Jupyter Notebook 67 | .ipynb_checkpoints 68 | 69 | # pytest 70 | .pytest_cache/ 71 | .coverage 72 | htmlcov/ 73 | 74 | # mypy 75 | .mypy_cache/ 76 | .dmypy.json 77 | dmypy.json 78 | 79 | # Profiling data 80 | .prof 81 | 82 | # Database files 83 | *.db 84 | *.sqlite 85 | *.sqlite3 86 | 87 | # Backup files 88 | *.bak 89 | *~ 90 | 91 | COPY_TRAD_SPEC.md 92 | articles 93 | -------------------------------------------------------------------------------- /src/utils/events.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, List, Optional 2 | from dataclasses import dataclass 3 | from enum import Enum 4 | 5 | 6 | class EventType(Enum): 7 | """Event types for the trading framework""" 8 | 9 | ORDER_FILLED = "order_filled" 10 | ORDER_CANCELLED = "order_cancelled" 11 | ORDER_PLACED = "order_placed" 12 | POSITION_OPENED = "position_opened" 13 | POSITION_CLOSED = "position_closed" 14 | POSITION_UPDATED = "position_updated" 15 | PRICE_UPDATE = "price_update" 16 | STRATEGY_START = "strategy_start" 17 | STRATEGY_STOP = "strategy_stop" 18 | STRATEGY_UPDATE = "strategy_update" 19 | ERROR = "error" 20 | SYSTEM = "system" 21 | EMERGENCY_STOP = "emergency_stop" 22 | 23 | 24 | @dataclass 25 | class Event: 26 | """Base event class""" 27 | 28 | type: EventType 29 | timestamp: float 30 | data: Dict[str, Any] 31 | source: Optional[str] = None 32 | 33 | 34 | class EventBus: 35 | """Simple event bus for framework communication""" 36 | 37 | def __init__(self): 38 | self._listeners: Dict[EventType, List[Callable[[Event], None]]] = {} 39 | 40 | def subscribe( 41 | self, event_type: EventType, callback: Callable[[Event], None] 42 | ) -> None: 43 | """Subscribe to an event type""" 44 | if event_type not in self._listeners: 45 | self._listeners[event_type] = [] 46 | self._listeners[event_type].append(callback) 47 | 48 | def unsubscribe( 49 | self, event_type: EventType, callback: Callable[[Event], None] 50 | ) -> None: 51 | """Unsubscribe from an event type""" 52 | if event_type in self._listeners: 53 | try: 54 | self._listeners[event_type].remove(callback) 55 | except ValueError: 56 | pass 57 | 58 | def emit(self, event: Event) -> None: 59 | """Emit an event to all subscribers""" 60 | if event.type in self._listeners: 61 | for callback in self._listeners[event.type]: 62 | try: 63 | callback(event) 64 | except Exception as e: 65 | # Log error but don't stop other listeners 66 | print(f"Error in event listener: {e}") 67 | -------------------------------------------------------------------------------- /src/exchanges/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exchange Integrations 3 | 4 | Technical implementations for different exchanges/DEXes. 5 | Add new exchanges by implementing the ExchangeAdapter interface. 6 | 7 | To add a new exchange: 8 | 1. Implement ExchangeAdapter interface 9 | 2. Add to EXCHANGE_REGISTRY 10 | 3. Update configuration to use exchange type 11 | """ 12 | 13 | from .hyperliquid import HyperliquidAdapter, HyperliquidMarketData 14 | 15 | # Exchange registry - makes it easy to add new DEXes 16 | EXCHANGE_REGISTRY = { 17 | "hyperliquid": HyperliquidAdapter, 18 | } 19 | 20 | # Aliases for convenience 21 | EXCHANGE_REGISTRY["hl"] = HyperliquidAdapter 22 | 23 | 24 | def create_exchange_adapter(exchange_type: str, config: dict): 25 | """ 26 | Factory function to create exchange adapters. 27 | 28 | Makes it easy to add new exchanges: 29 | 1. Implement ExchangeAdapter interface 30 | 2. Add to EXCHANGE_REGISTRY 31 | 3. Done! 32 | 33 | Args: 34 | exchange_type: Type of exchange (e.g., "hyperliquid", "binance") 35 | config: Exchange configuration dictionary 36 | 37 | Returns: 38 | ExchangeAdapter instance 39 | """ 40 | if exchange_type not in EXCHANGE_REGISTRY: 41 | available = ", ".join(EXCHANGE_REGISTRY.keys()) 42 | raise ValueError( 43 | f"Unknown exchange type: {exchange_type}. Available: {available}" 44 | ) 45 | 46 | exchange_class = EXCHANGE_REGISTRY[exchange_type] 47 | 48 | # Extract common parameters for exchange initialization 49 | if exchange_type in ["hyperliquid", "hl"]: 50 | # Hyperliquid-specific initialization 51 | private_key = config.get("private_key") 52 | testnet = config.get("testnet", True) 53 | 54 | if not private_key: 55 | raise ValueError("private_key is required for Hyperliquid") 56 | 57 | return exchange_class(private_key, testnet) 58 | 59 | # Future exchanges will have their own initialization logic here 60 | # elif exchange_type == "binance": 61 | # api_key = config.get("api_key") 62 | # secret_key = config.get("secret_key") 63 | # return exchange_class(api_key, secret_key) 64 | 65 | else: 66 | # Default: try to pass config directly 67 | return exchange_class(config) 68 | 69 | 70 | __all__ = [ 71 | "HyperliquidAdapter", 72 | "HyperliquidMarketData", 73 | "EXCHANGE_REGISTRY", 74 | "create_exchange_adapter", 75 | ] 76 | -------------------------------------------------------------------------------- /bots/btc_conservative.yaml: -------------------------------------------------------------------------------- 1 | # CONSERVATIVE STRATEGY DEFINITION: 2 | # - LOW RISK: Uses only 10% of account balance to minimize potential losses 3 | # - NARROW RANGE: ±5% price range reduces exposure to large price swings 4 | # - FEWER LEVELS: 10 grid levels for simpler management 5 | # - SLOWER REBALANCING: Only rebalances if price moves >12% outside range 6 | 7 | name: "btc_conservative_clean" 8 | active: true # Options: true/false - Enable/disable this trading strategy 9 | 10 | exchange: 11 | type: "hyperliquid" # Exchange type (options: "hyperliquid", "hl") 12 | testnet: true # Options: true/false - Use testnet for development 13 | 14 | account: 15 | max_allocation_pct: 10.0 # Uses only 10% of total account (range: 1-100%) 16 | 17 | grid: 18 | symbol: "BTC" # Trading pair symbol (e.g., "BTC", "ETH", "SOL") 19 | levels: 10 # Number of buy/sell orders 20 | 21 | price_range: 22 | # Mode options: 23 | # - "auto": Calculate range automatically from current price 24 | # - "manual": Set fixed upper/lower price bounds 25 | mode: "auto" 26 | auto: 27 | range_pct: 5.0 # ±5% from center price (conservative: 3-8%, aggressive: 10-20%) 28 | # manual: # Only used when mode is "manual" 29 | # upper_price: 45000.0 30 | # lower_price: 35000.0 31 | 32 | risk_management: 33 | # Stop Loss: Automatically close positions when loss exceeds threshold 34 | stop_loss_enabled: false # Options: true/false - Enable automatic stop loss 35 | stop_loss_pct: 8.0 # Percentage loss before closing position (range: 1-20%) 36 | 37 | # Take Profit: Automatically close positions when profit exceeds threshold 38 | take_profit_enabled: false # Options: true/false - Enable automatic take profit 39 | take_profit_pct: 25.0 # Percentage profit before closing position (range: 5-100%) 40 | 41 | # Account Protection: Portfolio-level risk controls 42 | max_drawdown_pct: 15.0 # Stop all trading if account drawdown exceeds % (range: 5-50%) 43 | max_position_size_pct: 40.0 # Maximum position size as % of account (range: 10-100%) 44 | 45 | rebalance: 46 | # Rebalance threshold: when to recreate the grid 47 | # Lower values = more frequent rebalancing = higher fees but tighter control 48 | price_move_threshold_pct: 12.0 # Rebalance if price moves >12% outside range 49 | 50 | monitoring: 51 | # Log level options (from most to least verbose): 52 | # - "DEBUG": All messages, detailed execution info, good for troubleshooting 53 | # - "INFO": Important events, order placements, strategy changes (recommended) 54 | # - "WARNING": Only warnings and errors, minimal output 55 | # - "ERROR": Only critical errors that stop execution 56 | log_level: "INFO" -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/cancel_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for cancelling a specific spot order to see how it appears in WebSocket. 3 | Finds and cancels a single open spot order. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | from dotenv import load_dotenv 10 | from eth_account import Account 11 | from hyperliquid.exchange import Exchange 12 | from hyperliquid.info import Info 13 | 14 | load_dotenv() 15 | 16 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 17 | 18 | 19 | async def cancel_spot_order(): 20 | """Cancel a spot order""" 21 | print("Cancel Spot Order Test") 22 | print("=" * 40) 23 | 24 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 25 | if not private_key: 26 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 27 | return 28 | 29 | try: 30 | wallet = Account.from_key(private_key) 31 | exchange = Exchange(wallet, BASE_URL) 32 | info = Info(BASE_URL, skip_ws=True) 33 | 34 | print(f"📱 Wallet: {wallet.address}") 35 | 36 | # Get open orders using environment wallet address 37 | wallet_address = os.getenv("TESTNET_WALLET_ADDRESS") or wallet.address 38 | open_orders = info.open_orders(wallet_address) 39 | print(f"📋 Found {len(open_orders)} open orders") 40 | 41 | if not open_orders: 42 | print("❌ No open orders to cancel") 43 | print("💡 Run place_order.py first to create an order") 44 | return 45 | 46 | # Find the first spot order 47 | spot_order = None 48 | for order in open_orders: 49 | coin = order.get("coin", "") 50 | if coin.startswith("@") or "/" in coin: # Spot order indicators 51 | spot_order = order 52 | break 53 | 54 | if not spot_order: 55 | print("❌ No spot orders found to cancel") 56 | print("💡 Only perpetual orders are open") 57 | return 58 | 59 | order_id = spot_order.get("oid") 60 | coin_field = spot_order.get("coin") 61 | side = "BUY" if spot_order.get("side") == "B" else "SELL" 62 | size = spot_order.get("sz") 63 | price = spot_order.get("limitPx") 64 | 65 | print(f"🎯 Found spot order to cancel:") 66 | print(f" ID: {order_id}") 67 | print(f" Type: {side} {size} {coin_field} @ ${price}") 68 | 69 | # Cancel the order using correct parameter name 70 | print(f"🔄 Cancelling order {order_id}...") 71 | result = exchange.cancel(name=coin_field, oid=order_id) 72 | 73 | print(f"📋 Cancel result:") 74 | print(json.dumps(result, indent=2)) 75 | 76 | if result and result.get("status") == "ok": 77 | print(f"✅ Order {order_id} cancelled successfully!") 78 | print(f"🔍 Monitor this cancellation in your WebSocket stream") 79 | else: 80 | print(f"❌ Cancel failed: {result}") 81 | 82 | except Exception as e: 83 | print(f"❌ Error: {e}") 84 | 85 | 86 | if __name__ == "__main__": 87 | asyncio.run(cancel_spot_order()) 88 | -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/print_raw_websocket_messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple raw message printer for ALL WebSocket messages. 3 | Shows unprocessed JSON messages from the API including positions, fills, and orders. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | import signal 10 | from dotenv import load_dotenv 11 | import websockets 12 | 13 | load_dotenv() 14 | 15 | WS_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_WS_URL") 16 | LEADER_ADDRESS = os.getenv("TESTNET_WALLET_ADDRESS") 17 | 18 | 19 | async def monitor_raw_messages(): 20 | """Connect to WebSocket and print raw messages""" 21 | if not LEADER_ADDRESS or LEADER_ADDRESS == "0x...": 22 | print("❌ Please set LEADER_ADDRESS in the script") 23 | return 24 | 25 | print(f"Connecting to {WS_URL}") 26 | 27 | shutdown_event = asyncio.Event() 28 | 29 | def signal_handler(signum, frame): 30 | del signum, frame 31 | print("\nShutting down...") 32 | shutdown_event.set() 33 | 34 | signal.signal(signal.SIGINT, signal_handler) 35 | 36 | try: 37 | async with websockets.connect(WS_URL) as websocket: 38 | print("✅ WebSocket connected!") 39 | 40 | # Subscribe to user events (positions, fills, TP/SL updates) and orders 41 | subscriptions = [ 42 | { 43 | "method": "subscribe", 44 | "subscription": {"type": "userEvents", "user": LEADER_ADDRESS}, 45 | }, 46 | { 47 | "method": "subscribe", 48 | "subscription": {"type": "orderUpdates", "user": LEADER_ADDRESS}, 49 | }, 50 | # {"method": "subscribe", "subscription": {"type": "userFills", "user": LEADER_ADDRESS, "aggregateByTime": True}}, 51 | ] 52 | 53 | for sub in subscriptions: 54 | await websocket.send(json.dumps(sub)) 55 | 56 | print(f"Monitoring ALL user events and orders: {LEADER_ADDRESS}") 57 | print("=" * 80) 58 | 59 | async for message in websocket: 60 | if shutdown_event.is_set(): 61 | break 62 | 63 | try: 64 | data = json.loads(message) 65 | print(f"RAW MESSAGE: {json.dumps(data, indent=2)}") 66 | print("-" * 40) 67 | 68 | except json.JSONDecodeError: 69 | print("⚠️ Received invalid JSON") 70 | except Exception as e: 71 | print(f"❌ Error: {e}") 72 | 73 | except websockets.exceptions.ConnectionClosed: 74 | print("WebSocket connection closed") 75 | except Exception as e: 76 | print(f"❌ WebSocket error: {e}") 77 | finally: 78 | print("Disconnected") 79 | 80 | 81 | async def main(): 82 | print("Raw WebSocket Message Monitor") 83 | print("=" * 40) 84 | 85 | if not WS_URL: 86 | print("❌ Missing HYPERLIQUID_TESTNET_PUBLIC_WS_URL in .env file") 87 | return 88 | 89 | await monitor_raw_messages() 90 | 91 | 92 | if __name__ == "__main__": 93 | asyncio.run(main()) 94 | -------------------------------------------------------------------------------- /learning_examples/02_market_data/get_all_prices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fetches current market prices for all assets using SDK and raw HTTP API. 3 | Compares results to verify data consistency between methods. 4 | """ 5 | 6 | import asyncio 7 | import os 8 | from typing import Dict, Optional 9 | 10 | from dotenv import load_dotenv 11 | import httpx 12 | from hyperliquid.info import Info 13 | 14 | load_dotenv() 15 | 16 | # You can only use this endpoint on the official Hyperliquid public API. 17 | # It is not available through Chainstack, as the open-source node implementation does not support it yet. 18 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 19 | ASSETS_TO_SHOW = ["BTC", "ETH", "SOL", "DOGE", "AVAX"] 20 | 21 | 22 | async def method_1_sdk() -> Optional[Dict[str, str]]: 23 | """Method 1: Using Hyperliquid Python SDK""" 24 | print("Method 1: Hyperliquid SDK") 25 | print("-" * 30) 26 | 27 | try: 28 | info = Info(BASE_URL, skip_ws=True) 29 | all_prices = info.all_mids() 30 | 31 | print(f"Got prices for {len(all_prices)} assets") 32 | for asset in ASSETS_TO_SHOW: 33 | if asset in all_prices: 34 | price = float(all_prices[asset]) 35 | print(f" {asset}: ${price:,.2f}") 36 | 37 | return all_prices 38 | 39 | except Exception as e: 40 | print(f"SDK method failed: {e}") 41 | return None 42 | 43 | 44 | async def method_2_raw_api() -> Optional[Dict[str, str]]: 45 | """Method 2: Raw HTTP API call""" 46 | print("\nMethod 2: Raw HTTP API") 47 | print("-" * 30) 48 | 49 | try: 50 | async with httpx.AsyncClient() as client: 51 | response = await client.post( 52 | f"{BASE_URL}/info", 53 | json={"type": "allMids"}, 54 | headers={"Content-Type": "application/json"}, 55 | ) 56 | 57 | if response.status_code == 200: 58 | all_prices = response.json() 59 | print(f"Got prices for {len(all_prices)} assets") 60 | 61 | for asset in ASSETS_TO_SHOW: 62 | if asset in all_prices: 63 | price = float(all_prices[asset]) 64 | print(f" {asset}: ${price:,.2f}") 65 | 66 | return all_prices 67 | else: 68 | print(f"HTTP failed: {response.status_code}") 69 | return None 70 | 71 | except Exception as e: 72 | print(f"HTTP method failed: {e}") 73 | return None 74 | 75 | 76 | async def main() -> None: 77 | print("Hyperliquid Market Prices") 78 | print("=" * 40) 79 | 80 | sdk_prices = await method_1_sdk() 81 | http_prices = await method_2_raw_api() 82 | 83 | if sdk_prices and http_prices: 84 | print("\nComparison:") 85 | for asset in ["BTC", "ETH", "SOL"]: 86 | if asset in sdk_prices and asset in http_prices: 87 | sdk_price = float(sdk_prices[asset]) 88 | http_price = float(http_prices[asset]) 89 | match = "MATCH" if sdk_price == http_price else "DIFF" 90 | print( 91 | f" {asset}: SDK=${sdk_price:,.2f} | HTTP=${http_price:,.2f} {match}" 92 | ) 93 | 94 | 95 | if __name__ == "__main__": 96 | asyncio.run(main()) 97 | -------------------------------------------------------------------------------- /learning_examples/03_account_info/get_user_state.py: -------------------------------------------------------------------------------- 1 | """ 2 | Displays account state including balances, positions, and margin health. 3 | """ 4 | 5 | import asyncio 6 | import os 7 | from typing import Optional 8 | 9 | import httpx 10 | from dotenv import load_dotenv 11 | from eth_account import Account 12 | from hyperliquid.info import Info 13 | 14 | load_dotenv() 15 | 16 | BASE_URL = os.getenv("HYPERLIQUID_CHAINSTACK_BASE_URL") 17 | WALLET_ADDRESS = os.getenv("TESTNET_WALLET_ADDRESS") 18 | 19 | 20 | async def method_1_sdk() -> Optional[Account]: 21 | """Method 1: Using Hyperliquid Python SDK""" 22 | print("Method 1: Hyperliquid SDK") 23 | print("-" * 30) 24 | 25 | try: 26 | print("Connecting to Hyperliquid testnet...") 27 | info = Info(BASE_URL, skip_ws=True) 28 | 29 | user_state = info.user_state(WALLET_ADDRESS) 30 | print("Connection successful! API responded with account data") 31 | 32 | margin_summary = user_state.get("marginSummary", {}) 33 | account_value = float(margin_summary.get("accountValue", 0)) 34 | withdrawable = float(user_state.get("withdrawable", 0)) 35 | total_margin_used = float(margin_summary.get("totalMarginUsed", 0)) 36 | 37 | print(f"Account value: ${account_value:,.2f}") 38 | print(f"Withdrawable: ${withdrawable:,.2f}") 39 | print(f"Margin used: ${total_margin_used:,.2f}") 40 | 41 | return user_state 42 | 43 | except Exception as e: 44 | print(f"Connection failed: {e}") 45 | return None 46 | 47 | 48 | async def method_2_raw_api() -> Optional[Account]: 49 | """Method 2: Raw HTTP API calls""" 50 | print("\nMethod 2: Raw HTTP API") 51 | print("-" * 30) 52 | 53 | try: 54 | print("Making direct HTTP request to Hyperliquid API...") 55 | async with httpx.AsyncClient() as client: 56 | response = await client.post( 57 | f"{BASE_URL}/info", 58 | json={"type": "clearinghouseState", "user": WALLET_ADDRESS}, 59 | headers={"Content-Type": "application/json"}, 60 | ) 61 | 62 | if response.status_code == 200: 63 | print("Connection successful! HTTP API responded") 64 | user_state = response.json() 65 | 66 | margin_summary = user_state.get("marginSummary", {}) 67 | account_value = float(margin_summary.get("accountValue", 0)) 68 | withdrawable = float(user_state.get("withdrawable", 0)) 69 | cross_margin_used = float( 70 | user_state.get("crossMaintenanceMarginUsed", 0) 71 | ) 72 | 73 | print(f"Account value: ${account_value:,.2f}") 74 | print(f"Withdrawable: ${withdrawable:,.2f}") 75 | print(f"Margin used: ${cross_margin_used:,.2f}") 76 | return user_state 77 | else: 78 | print(f"Connection failed: HTTP {response.status_code}") 79 | return None 80 | 81 | except Exception as e: 82 | print(f"Connection failed: {e}") 83 | return None 84 | 85 | 86 | async def main() -> None: 87 | print("Hyperliquid Connection Methods") 88 | print("=" * 40) 89 | 90 | await method_1_sdk() 91 | await method_2_raw_api() 92 | 93 | 94 | if __name__ == "__main__": 95 | asyncio.run(main()) 96 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Hyperliquid Grid Trading Bot Configuration 2 | # Copy this file to .env and configure with your actual values 3 | # 4 | # QUICK START: 5 | # 1. Copy this file: cp .env.example .env 6 | # 2. Set your private key in HYPERLIQUID_TESTNET_PRIVATE_KEY 7 | # 3. Run the bot: uv run python src/run_bot.py bots/btc_conservative.yaml 8 | 9 | # ============================================================================= 10 | # REQUIRED: PRIVATE KEYS 11 | # ============================================================================= 12 | 13 | # Testnet (recommended for learning and testing - uses fake money) 14 | HYPERLIQUID_TESTNET_PRIVATE_KEY=... 15 | 16 | # Mainnet (real money - be very careful!) 17 | # HYPERLIQUID_MAINNET_PRIVATE_KEY=0x...your_mainnet_private_key 18 | 19 | # Alternative: File-based keys 20 | # HYPERLIQUID_TESTNET_KEY_FILE=/path/to/testnet.key 21 | # HYPERLIQUID_MAINNET_KEY_FILE=/path/to/mainnet.key 22 | # HYPERLIQUID_PRIVATE_KEY_FILE=/path/to/legacy.key 23 | 24 | # ============================================================================= 25 | # TRADING ENVIRONMENT 26 | # ============================================================================= 27 | 28 | # Set to true for testnet, false for mainnet 29 | HYPERLIQUID_TESTNET=true 30 | TESTNET_WALLET_ADDRESS=... 31 | 32 | # Paper trading mode (optional - for simulation without real orders) 33 | # PAPER_TRADING=false 34 | 35 | # ============================================================================= 36 | # ENDPOINT CONFIGURATION (Optional - uses defaults if not set) 37 | # ============================================================================= 38 | 39 | # Testnet Endpoints 40 | HYPERLIQUID_TESTNET_PUBLIC_BASE_URL=https://api.hyperliquid-testnet.xyz 41 | HYPERLIQUID_TESTNET_PUBLIC_INFO_URL=https://api.hyperliquid-testnet.xyz/info 42 | HYPERLIQUID_TESTNET_PUBLIC_EXCHANGE_URL=https://api.hyperliquid-testnet.xyz/exchange 43 | HYPERLIQUID_TESTNET_PUBLIC_WS_URL=wss://api.hyperliquid-testnet.xyz/ws 44 | 45 | # Mainnet Endpoints 46 | # HYPERLIQUID_PUBLIC_BASE_URL=https://api.hyperliquid.xyz 47 | # HYPERLIQUID_PUBLIC_INFO_URL=https://api.hyperliquid.xyz/info 48 | # HYPERLIQUID_PUBLIC_EXCHANGE_URL=https://api.hyperliquid.xyz/exchange 49 | # HYPERLIQUID_PUBLIC_WS_URL=wss://api.hyperliquid.xyz/ws 50 | 51 | # Chainstack Endpoints (if you have Chainstack access) 52 | # HYPERLIQUID_CHAINSTACK_BASE_URL=... 53 | # HYPERLIQUID_TESTNET_CHAINSTACK_INFO_URL=https://... 54 | # HYPERLIQUID_TESTNET_CHAINSTACK_EVM_URL=https://... 55 | # HYPERLIQUID_TESTNET_CHAINSTACK_WS_URL=wss://... 56 | # HYPERLIQUID_CHAINSTACK_BASE_URL=https://... 57 | # HYPERLIQUID_CHAINSTACK_INFO_URL=https://... 58 | # HYPERLIQUID_CHAINSTACK_EVM_URL=https://... 59 | # HYPERLIQUID_CHAINSTACK_WS_URL=wss://... 60 | 61 | # Endpoint Priorities (1 = highest priority) 62 | # HYPERLIQUID_TESTNET_PUBLIC_INFO_PRIORITY=1 63 | # HYPERLIQUID_TESTNET_PUBLIC_EXCHANGE_PRIORITY=1 64 | # HYPERLIQUID_TESTNET_PUBLIC_WS_PRIORITY=1 65 | # HYPERLIQUID_TESTNET_PUBLIC_EVM_PRIORITY=1 66 | 67 | # ============================================================================= 68 | # MONITORING AND HEALTH CHECKS 69 | # ============================================================================= 70 | 71 | # Endpoint health check settings 72 | ENDPOINT_HEALTH_CHECK_INTERVAL=300 73 | ENDPOINT_HEALTH_CHECK_TIMEOUT=10 74 | 75 | # ============================================================================= 76 | # LOGGING AND DEBUGGING 77 | # ============================================================================= 78 | 79 | # Log level (DEBUG, INFO, WARNING, ERROR) 80 | LOG_LEVEL=INFO -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/cancel_orders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for bulk cancelling multiple spot orders to see how it appears in WebSocket. 3 | Cancels all open spot orders using bulk_cancel method. 4 | """ 5 | 6 | import asyncio 7 | import os 8 | from dotenv import load_dotenv 9 | from eth_account import Account 10 | from hyperliquid.exchange import Exchange 11 | from hyperliquid.info import Info 12 | 13 | load_dotenv() 14 | 15 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 16 | 17 | 18 | async def cancel_multiple_spot_orders(): 19 | """Cancel multiple spot orders using bulk_cancel""" 20 | print("Cancel Multiple Spot Orders Test") 21 | print("=" * 40) 22 | 23 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 24 | if not private_key: 25 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 26 | return 27 | 28 | try: 29 | wallet = Account.from_key(private_key) 30 | exchange = Exchange(wallet, BASE_URL) 31 | info = Info(BASE_URL, skip_ws=True) 32 | 33 | print(f"📱 Wallet: {wallet.address}") 34 | 35 | # Get open orders using environment wallet address 36 | wallet_address = os.getenv("TESTNET_WALLET_ADDRESS") or wallet.address 37 | open_orders = info.open_orders(wallet_address) 38 | print(f"📋 Found {len(open_orders)} total open orders") 39 | 40 | if not open_orders: 41 | print("❌ No open orders to cancel") 42 | print("💡 Run place_order.py multiple times to create orders") 43 | return 44 | 45 | # Find all spot orders 46 | spot_orders = [] 47 | for order in open_orders: 48 | coin = order.get("coin", "") 49 | if coin.startswith("@") or "/" in coin: # Spot order indicators 50 | spot_orders.append(order) 51 | 52 | if not spot_orders: 53 | print("❌ No spot orders found to cancel") 54 | print("💡 Only perpetual orders are open") 55 | return 56 | 57 | print(f"🎯 Found {len(spot_orders)} spot orders to cancel:") 58 | 59 | # Cancel each order individually 60 | successful_cancels = 0 61 | failed_cancels = 0 62 | 63 | for order in spot_orders: 64 | order_id = order.get("oid") 65 | coin_field = order.get("coin") 66 | side = "BUY" if order.get("side") == "B" else "SELL" 67 | size = order.get("sz") 68 | price = order.get("limitPx") 69 | 70 | print(f" Cancelling ID {order_id}: {side} {size} {coin_field} @ ${price}") 71 | 72 | try: 73 | result = exchange.cancel(name=coin_field, oid=order_id) 74 | 75 | if result and result.get("status") == "ok": 76 | print(f" ✅ Order {order_id} cancelled successfully") 77 | successful_cancels += 1 78 | else: 79 | print(f" ❌ Order {order_id} cancel failed: {result}") 80 | failed_cancels += 1 81 | except Exception as e: 82 | print(f" ❌ Order {order_id} cancel error: {e}") 83 | failed_cancels += 1 84 | 85 | print(f"📋 Cancel Summary:") 86 | print(f" ✅ Successful: {successful_cancels}") 87 | print(f" ❌ Failed: {failed_cancels}") 88 | print(f"🔍 Monitor these cancellations in your WebSocket stream") 89 | 90 | except Exception as e: 91 | print(f"❌ Error: {e}") 92 | 93 | 94 | if __name__ == "__main__": 95 | asyncio.run(cancel_multiple_spot_orders()) 96 | -------------------------------------------------------------------------------- /learning_examples/04_trading/place_limit_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | Places limit orders using the Hyperliquid SDK with proper price calculation. 3 | Demonstrates order placement with market offset and result verification. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | from typing import Optional 10 | 11 | from dotenv import load_dotenv 12 | from eth_account import Account 13 | from hyperliquid.exchange import Exchange 14 | from hyperliquid.info import Info 15 | from hyperliquid.utils.signing import OrderType as HLOrderType 16 | 17 | load_dotenv() 18 | 19 | # You can only use this endpoint on the official Hyperliquid public API. 20 | # It is not available through Chainstack, as the open-source node implementation does not support it yet. 21 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 22 | SYMBOL = "BTC" 23 | ORDER_SIZE = 0.001 # Small test size 24 | PRICE_OFFSET_PCT = -5 # 5% below market for buy order 25 | 26 | 27 | async def method_sdk(private_key: str) -> Optional[str]: 28 | """Method: Using Hyperliquid Python SDK""" 29 | print("Method: Hyperliquid SDK") 30 | print("-" * 30) 31 | 32 | try: 33 | wallet = Account.from_key(private_key) 34 | exchange = Exchange(wallet, BASE_URL) 35 | info = Info(BASE_URL, skip_ws=True) 36 | 37 | all_prices = info.all_mids() 38 | market_price = float(all_prices.get(SYMBOL, 0)) 39 | 40 | if market_price == 0: 41 | print(f"Could not get {SYMBOL} price") 42 | return None 43 | 44 | order_price = market_price * (1 + PRICE_OFFSET_PCT / 100) 45 | order_price = round(order_price, 0) 46 | 47 | print(f"Current {SYMBOL} price: ${market_price:,.2f}") 48 | print(f"Placing buy order: {ORDER_SIZE} {SYMBOL} @ ${order_price:,.2f}") 49 | 50 | result = exchange.order( 51 | name=SYMBOL, 52 | is_buy=True, 53 | sz=ORDER_SIZE, 54 | limit_px=order_price, 55 | order_type=HLOrderType({"limit": {"tif": "Gtc"}}), 56 | reduce_only=False, 57 | ) 58 | 59 | print(f"Order result:") 60 | print(json.dumps(result, indent=2)) 61 | 62 | if result and result.get("status") == "ok": 63 | response_data = result.get("response", {}).get("data", {}) 64 | statuses = response_data.get("statuses", []) 65 | 66 | if statuses: 67 | status_info = statuses[0] 68 | if "resting" in status_info: 69 | order_id = status_info["resting"]["oid"] 70 | print(f"Order placed successfully! ID: {order_id}") 71 | return order_id 72 | elif "filled" in status_info: 73 | print(f"Order filled immediately!") 74 | return "filled" 75 | 76 | print(f"Order placement unclear") 77 | return None 78 | 79 | except Exception as e: 80 | print(f"SDK method failed: {e}") 81 | return None 82 | 83 | 84 | async def main() -> None: 85 | print("Hyperliquid Limit Orders") 86 | print("=" * 40) 87 | 88 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 89 | if not private_key: 90 | print("Set HYPERLIQUID_TESTNET_PRIVATE_KEY in your .env file") 91 | print("Create .env file with: HYPERLIQUID_TESTNET_PRIVATE_KEY=0x...") 92 | print("WARNING: This will place REAL orders on testnet!") 93 | return 94 | 95 | order_id = await method_sdk(private_key) 96 | 97 | if order_id: 98 | print("\nOrder placed successfully!") 99 | print("Check open orders to verify placement") 100 | 101 | 102 | if __name__ == "__main__": 103 | asyncio.run(main()) 104 | -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/modify_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for modifying a spot order to see how it appears in WebSocket. 3 | Finds an open spot order and modifies its price or size. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | from dotenv import load_dotenv 10 | from eth_account import Account 11 | from hyperliquid.exchange import Exchange 12 | from hyperliquid.info import Info 13 | from hyperliquid.utils.signing import ModifyRequest 14 | 15 | load_dotenv() 16 | 17 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 18 | 19 | 20 | async def modify_spot_order(): 21 | """Modify a spot order""" 22 | print("Modify Spot Order Test") 23 | print("=" * 40) 24 | 25 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 26 | if not private_key: 27 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 28 | return 29 | 30 | try: 31 | wallet = Account.from_key(private_key) 32 | exchange = Exchange(wallet, BASE_URL) 33 | info = Info(BASE_URL, skip_ws=True) 34 | 35 | print(f"📱 Wallet: {wallet.address}") 36 | 37 | # Get open orders using environment wallet address 38 | wallet_address = os.getenv("TESTNET_WALLET_ADDRESS") or wallet.address 39 | open_orders = info.open_orders(wallet_address) 40 | print(f"📋 Found {len(open_orders)} open orders") 41 | 42 | if not open_orders: 43 | print("❌ No open orders to modify") 44 | print("💡 Run place_order.py first to create an order") 45 | return 46 | 47 | # Find the first spot order 48 | spot_order = None 49 | for order in open_orders: 50 | coin = order.get("coin", "") 51 | if coin.startswith("@") or "/" in coin: # Spot order indicators 52 | spot_order = order 53 | break 54 | 55 | if not spot_order: 56 | print("❌ No spot orders found to modify") 57 | print("💡 Only perpetual orders are open") 58 | return 59 | 60 | order_id = spot_order.get("oid") 61 | coin_field = spot_order.get("coin") 62 | side = "BUY" if spot_order.get("side") == "B" else "SELL" 63 | current_size = float(spot_order.get("sz", 0)) 64 | current_price = float(spot_order.get("limitPx", 0)) 65 | 66 | print(f"🎯 Found spot order to modify:") 67 | print(f" ID: {order_id}") 68 | print(f" Current: {side} {current_size} {coin_field} @ ${current_price}") 69 | 70 | # Calculate new values 71 | price_modifier = ( 72 | 0.9 if side == "BUY" else 1.1 73 | ) # Make buy orders cheaper, sell orders more expensive 74 | new_price = round(current_price * price_modifier, 6) 75 | 76 | print(f" New: {side} {current_size} {coin_field} @ ${new_price}") 77 | 78 | # Create modify request 79 | print(f"🔄 Modifying order {order_id}...") 80 | result = exchange.modify_order( 81 | oid=order_id, 82 | name=coin_field, 83 | is_buy=(side == "BUY"), 84 | sz=current_size, 85 | limit_px=new_price, 86 | order_type={"limit": {"tif": "Gtc"}}, 87 | reduce_only=False, 88 | ) 89 | 90 | print(f"📋 Modify result:") 91 | print(json.dumps(result, indent=2)) 92 | 93 | if result and result.get("status") == "ok": 94 | print(f"✅ Order {order_id} modified successfully!") 95 | print(f"🔍 Monitor this modification in your WebSocket stream") 96 | else: 97 | print(f"❌ Modify failed: {result}") 98 | 99 | except Exception as e: 100 | print(f"❌ Error: {e}") 101 | 102 | 103 | if __name__ == "__main__": 104 | asyncio.run(modify_spot_order()) 105 | -------------------------------------------------------------------------------- /learning_examples/03_account_info/get_open_orders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Retrieves and displays open orders from your account. 3 | """ 4 | 5 | import asyncio 6 | import os 7 | 8 | import httpx 9 | from dotenv import load_dotenv 10 | from hyperliquid.info import Info 11 | 12 | load_dotenv() 13 | 14 | # You can only use this endpoint on the official Hyperliquid public API. 15 | # It is not available through Chainstack, as the open-source node implementation does not support it yet. 16 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 17 | WALLET_ADDRESS = os.getenv("TESTNET_WALLET_ADDRESS") 18 | 19 | 20 | async def method_1_sdk(): 21 | """Method 1: Using Hyperliquid Python SDK""" 22 | print("Method 1: Hyperliquid SDK") 23 | print("-" * 30) 24 | 25 | try: 26 | info = Info(BASE_URL, skip_ws=True) 27 | open_orders = info.open_orders(WALLET_ADDRESS) 28 | 29 | print(f"Found {len(open_orders)} open orders") 30 | 31 | if open_orders: 32 | for order in open_orders: 33 | oid = order.get("oid", "") 34 | coin = order.get("coin", "") 35 | side = "BUY" if order.get("side") == "B" else "SELL" 36 | size = order.get("sz", "0") 37 | limit_px = order.get("limitPx", "0") 38 | timestamp = order.get("timestamp", 0) 39 | 40 | order_value = float(size) * float(limit_px) 41 | print(f"\nOrder {oid}:") 42 | print(f" {side} {size} {coin} @ ${float(limit_px):,.2f}") 43 | print(f" Total value: ${order_value:,.2f}") 44 | print(f" Timestamp: {timestamp}") 45 | else: 46 | print("No open orders") 47 | 48 | return open_orders 49 | 50 | except Exception as e: 51 | print(f"SDK method failed: {e}") 52 | return None 53 | 54 | 55 | async def method_2_raw_api(): 56 | """Method 2: Raw HTTP API call""" 57 | print("\nMethod 2: Raw HTTP API") 58 | print("-" * 30) 59 | 60 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 61 | if not private_key: 62 | print("Set HYPERLIQUID_TESTNET_PRIVATE_KEY in your .env file") 63 | print("Create .env file with: HYPERLIQUID_TESTNET_PRIVATE_KEY=0x...") 64 | return None 65 | 66 | try: 67 | async with httpx.AsyncClient() as client: 68 | response = await client.post( 69 | f"{BASE_URL}/info", 70 | json={"type": "openOrders", "user": WALLET_ADDRESS}, 71 | headers={"Content-Type": "application/json"}, 72 | ) 73 | 74 | if response.status_code == 200: 75 | open_orders = response.json() 76 | 77 | print(f"Found {len(open_orders)} open orders") 78 | 79 | if open_orders: 80 | for order in open_orders: 81 | oid = order.get("oid", "") 82 | coin = order.get("coin", "") 83 | side = "BUY" if order.get("side") == "B" else "SELL" 84 | size = order.get("sz", "0") 85 | limit_px = order.get("limitPx", "0") 86 | 87 | print(f"\nOrder {oid}:") 88 | print(f" {side} {size} {coin} @ ${float(limit_px):,.2f}") 89 | 90 | return open_orders 91 | else: 92 | print(f"HTTP failed: {response.status_code}") 93 | return None 94 | 95 | except Exception as e: 96 | print(f"HTTP method failed: {e}") 97 | return None 98 | 99 | 100 | async def main(): 101 | print("Hyperliquid Open Orders") 102 | print("=" * 40) 103 | 104 | await method_1_sdk() 105 | await method_2_raw_api() 106 | 107 | 108 | if __name__ == "__main__": 109 | asyncio.run(main()) 110 | -------------------------------------------------------------------------------- /learning_examples/04_trading/cancel_orders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrates order cancellation strategies: single orders and batch cancellation by asset. 3 | Shows proper order lifecycle management and cancellation verification. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | 10 | from dotenv import load_dotenv 11 | from eth_account import Account 12 | from hyperliquid.exchange import Exchange 13 | from hyperliquid.info import Info 14 | 15 | load_dotenv() 16 | 17 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 18 | WALLET_ADDRESS = os.getenv("TESTNET_WALLET_ADDRESS") 19 | 20 | 21 | async def method_cancel_single_order(private_key: str) -> None: 22 | """Method: Cancel single order using SDK""" 23 | print("Method: Cancel Single Order") 24 | print("-" * 30) 25 | 26 | try: 27 | account = Account.from_key(private_key) 28 | exchange = Exchange(account, BASE_URL) 29 | info = Info(BASE_URL, skip_ws=True) 30 | 31 | open_orders = info.open_orders(WALLET_ADDRESS) 32 | 33 | if not open_orders: 34 | print("No open orders to cancel") 35 | return 36 | 37 | print(f"Found {len(open_orders)} open orders") 38 | 39 | for i, order in enumerate(open_orders, 1): 40 | oid = order.get("oid", "") 41 | coin = order.get("coin", "") 42 | side = "BUY" if order.get("side") == "B" else "SELL" 43 | size = order.get("sz", "0") 44 | price = float(order.get("limitPx", 0)) 45 | 46 | print(f" {i}. Order {oid}: {side} {size} {coin} @ ${price:,.2f}") 47 | 48 | if open_orders: 49 | first_order = open_orders[0] 50 | order_id = first_order.get("oid") 51 | 52 | print(f"\nCancelling order {order_id}...") 53 | print(f"Order details: coin={first_order.get('coin')}, oid={order_id}") 54 | 55 | result = exchange.cancel( 56 | name=first_order.get("coin", ""), oid=int(order_id) 57 | ) 58 | 59 | print(f"Cancel result type: {type(result)}") 60 | print(f"Cancel result:") 61 | print(json.dumps(result, indent=2)) 62 | 63 | # Check result structure 64 | if result and isinstance(result, dict): 65 | if result.get("status") == "ok": 66 | response_data = result.get("response", {}).get("data", {}) 67 | statuses = response_data.get("statuses", []) 68 | 69 | if statuses and statuses[0] == "success": 70 | print(f"✅ Order {order_id} cancelled successfully!") 71 | 72 | # Verify cancellation 73 | await asyncio.sleep(2) 74 | new_orders = info.open_orders(account.address) 75 | 76 | still_exists = any(o.get("oid") == order_id for o in new_orders) 77 | if not still_exists: 78 | print(f"✅ Cancellation confirmed - order removed") 79 | else: 80 | print(f"⚠️ Order still appears (may take time to update)") 81 | else: 82 | print(f"❌ Cancel failed with status: {statuses}") 83 | else: 84 | print(f"❌ Cancel request failed: {result}") 85 | else: 86 | print(f"❌ Unexpected response format: {result}") 87 | 88 | except Exception as e: 89 | print(f"❌ SDK method failed: {e}") 90 | 91 | 92 | async def main() -> None: 93 | print("Hyperliquid Order Cancellation") 94 | print("=" * 40) 95 | 96 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 97 | if not private_key: 98 | print("Set HYPERLIQUID_TESTNET_PRIVATE_KEY in your .env file") 99 | print("Create .env file with: HYPERLIQUID_TESTNET_PRIVATE_KEY=0x...") 100 | print("WARNING: This will cancel REAL orders on testnet!") 101 | return 102 | 103 | await method_cancel_single_order(private_key) 104 | 105 | 106 | if __name__ == "__main__": 107 | asyncio.run(main()) 108 | -------------------------------------------------------------------------------- /src/interfaces/strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Strategy Interface 3 | 4 | Simple interface for implementing trading strategies. 5 | Newbies can add new strategies by implementing this interface. 6 | """ 7 | 8 | from abc import ABC, abstractmethod 9 | from typing import Dict, List, Optional, Any 10 | from dataclasses import dataclass 11 | from enum import Enum 12 | 13 | 14 | class SignalType(Enum): 15 | """Trading signal types""" 16 | 17 | BUY = "buy" 18 | SELL = "sell" 19 | HOLD = "hold" 20 | CLOSE = "close" 21 | 22 | 23 | @dataclass 24 | class TradingSignal: 25 | """A trading signal from a strategy""" 26 | 27 | signal_type: SignalType 28 | asset: str 29 | size: float 30 | price: Optional[float] = None # None = market order 31 | reason: str = "" 32 | metadata: Dict[str, Any] = None 33 | 34 | def __post_init__(self): 35 | if self.metadata is None: 36 | self.metadata = {} 37 | 38 | 39 | @dataclass 40 | class MarketData: 41 | """Market data provided to strategies""" 42 | 43 | asset: str 44 | price: float 45 | volume_24h: float 46 | timestamp: float 47 | bid: Optional[float] = None 48 | ask: Optional[float] = None 49 | volatility: Optional[float] = None 50 | 51 | 52 | @dataclass 53 | class Position: 54 | """Current position information""" 55 | 56 | asset: str 57 | size: float # Positive = long, negative = short 58 | entry_price: float 59 | current_value: float 60 | unrealized_pnl: float 61 | timestamp: float 62 | 63 | 64 | class TradingStrategy(ABC): 65 | """ 66 | Base interface for all trading strategies. 67 | 68 | This is the ONLY class newbies need to understand to add new strategies. 69 | 70 | Example implementation: 71 | 72 | class MyStrategy(TradingStrategy): 73 | def __init__(self, config): 74 | super().__init__("my_strategy", config) 75 | 76 | def generate_signals(self, market_data, positions, balance): 77 | if market_data.price < 50000: 78 | return [TradingSignal(SignalType.BUY, "BTC", 0.001, reason="Price dip")] 79 | return [] 80 | """ 81 | 82 | def __init__(self, name: str, config: Dict[str, Any]): 83 | self.name = name 84 | self.config = config 85 | self.is_active = True 86 | 87 | @abstractmethod 88 | def generate_signals( 89 | self, market_data: MarketData, positions: List[Position], balance: float 90 | ) -> List[TradingSignal]: 91 | """ 92 | Generate trading signals based on market data and current positions. 93 | 94 | Args: 95 | market_data: Latest market data for the asset 96 | positions: Current positions 97 | balance: Available balance 98 | 99 | Returns: 100 | List of trading signals (can be empty) 101 | """ 102 | pass 103 | 104 | def on_trade_executed( 105 | self, signal: TradingSignal, executed_price: float, executed_size: float 106 | ) -> None: 107 | """ 108 | Called when a signal results in a trade execution. 109 | Override to implement custom logic (e.g., tracking, logging). 110 | """ 111 | pass 112 | 113 | def on_error(self, error: Exception, context: Dict[str, Any]) -> None: 114 | """ 115 | Called when an error occurs in strategy execution. 116 | Override to implement custom error handling. 117 | """ 118 | pass 119 | 120 | def get_status(self) -> Dict[str, Any]: 121 | """ 122 | Get strategy status and metrics. 123 | Override to provide strategy-specific information. 124 | """ 125 | return {"name": self.name, "active": self.is_active, "config": self.config} 126 | 127 | def start(self) -> None: 128 | """Called when strategy starts. Override for setup logic.""" 129 | self.is_active = True 130 | 131 | def stop(self) -> None: 132 | """Called when strategy stops. Override for cleanup logic.""" 133 | self.is_active = False 134 | 135 | def update_config(self, new_config: Dict[str, Any]) -> None: 136 | """Update strategy configuration. Override for custom logic.""" 137 | self.config.update(new_config) 138 | -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/modify_orders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for bulk modifying multiple spot orders to see how it appears in WebSocket. 3 | Modifies all open spot orders using bulk_modify_orders_new method. 4 | """ 5 | 6 | import asyncio 7 | import os 8 | from dotenv import load_dotenv 9 | from eth_account import Account 10 | from hyperliquid.exchange import Exchange 11 | from hyperliquid.info import Info 12 | 13 | load_dotenv() 14 | 15 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 16 | 17 | 18 | async def modify_multiple_spot_orders(): 19 | """Modify multiple spot orders using bulk_modify_orders_new""" 20 | print("Modify Multiple Spot Orders Test") 21 | print("=" * 40) 22 | 23 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 24 | if not private_key: 25 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 26 | return 27 | 28 | try: 29 | wallet = Account.from_key(private_key) 30 | exchange = Exchange(wallet, BASE_URL) 31 | info = Info(BASE_URL, skip_ws=True) 32 | 33 | print(f"📱 Wallet: {wallet.address}") 34 | 35 | # Get open orders using environment wallet address 36 | wallet_address = os.getenv("TESTNET_WALLET_ADDRESS") or wallet.address 37 | open_orders = info.open_orders(wallet_address) 38 | print(f"📋 Found {len(open_orders)} total open orders") 39 | 40 | if not open_orders: 41 | print("❌ No open orders to modify") 42 | print("💡 Run place_order.py multiple times to create orders") 43 | return 44 | 45 | # Find all spot orders 46 | spot_orders = [] 47 | for order in open_orders: 48 | coin = order.get("coin", "") 49 | if coin.startswith("@") or "/" in coin: # Spot order indicators 50 | spot_orders.append(order) 51 | 52 | if not spot_orders: 53 | print("❌ No spot orders found to modify") 54 | print("💡 Only perpetual orders are open") 55 | return 56 | 57 | print(f"🎯 Found {len(spot_orders)} spot orders to modify:") 58 | 59 | # Modify each order individually 60 | successful_modifies = 0 61 | failed_modifies = 0 62 | 63 | for order in spot_orders: 64 | order_id = order.get("oid") 65 | coin_field = order.get("coin") 66 | side = "BUY" if order.get("side") == "B" else "SELL" 67 | current_size = float(order.get("sz", 0)) 68 | current_price = float(order.get("limitPx", 0)) 69 | 70 | # Calculate new values 71 | price_modifier = 0.9 if side == "BUY" else 1.1 # Small price adjustment 72 | new_price = round(current_price * price_modifier, 6) 73 | 74 | print( 75 | f" Modifying ID {order_id}: {side} {current_size} -> {current_size} {coin_field} @ ${current_price} -> ${new_price}" 76 | ) 77 | 78 | try: 79 | result = exchange.modify_order( 80 | oid=order_id, 81 | name=coin_field, 82 | is_buy=(side == "BUY"), 83 | sz=current_size, 84 | limit_px=new_price, 85 | order_type={"limit": {"tif": "Gtc"}}, 86 | reduce_only=False, 87 | ) 88 | 89 | if result and result.get("status") == "ok": 90 | print(f" ✅ Order {order_id} modified successfully") 91 | successful_modifies += 1 92 | else: 93 | print(f" ❌ Order {order_id} modify failed: {result}") 94 | failed_modifies += 1 95 | except Exception as e: 96 | print(f" ❌ Order {order_id} modify error: {e}") 97 | failed_modifies += 1 98 | 99 | print(f"📋 Modify Summary:") 100 | print(f" ✅ Successful: {successful_modifies}") 101 | print(f" ❌ Failed: {failed_modifies}") 102 | print(f"🔍 Monitor these modifications in your WebSocket stream") 103 | 104 | except Exception as e: 105 | print(f"❌ Error: {e}") 106 | 107 | 108 | if __name__ == "__main__": 109 | asyncio.run(modify_multiple_spot_orders()) 110 | -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/place_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for placing spot orders to see how they appear in WebSocket. 3 | Creates a single spot order that can be monitored by the mirroring script. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | from dotenv import load_dotenv 10 | from eth_account import Account 11 | from hyperliquid.exchange import Exchange 12 | from hyperliquid.info import Info 13 | from hyperliquid.utils.signing import OrderType as HLOrderType 14 | 15 | load_dotenv() 16 | 17 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 18 | SYMBOL = "PURR/USDC" # Spot pair 19 | ORDER_SIZE = 3.0 # Size to meet minimum $10 USDC requirement 20 | PRICE_OFFSET_PCT = -50 # 50% below market for buy order (won't fill) 21 | 22 | 23 | async def place_spot_order(): 24 | """Place a spot order""" 25 | print("Placing Spot Order Test") 26 | print("=" * 40) 27 | 28 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 29 | if not private_key: 30 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 31 | return 32 | 33 | try: 34 | wallet = Account.from_key(private_key) 35 | exchange = Exchange(wallet, BASE_URL) 36 | info = Info(BASE_URL, skip_ws=True) 37 | 38 | print(f"📱 Wallet: {wallet.address}") 39 | print(f"🏛️ Vault address: {exchange.vault_address}") 40 | print(f"🌐 Is mainnet: {exchange.base_url == 'https://api.hyperliquid.xyz'}") 41 | 42 | # Get spot metadata to find the asset 43 | spot_data = info.spot_meta_and_asset_ctxs() 44 | if len(spot_data) >= 2: 45 | spot_meta = spot_data[0] 46 | asset_ctxs = spot_data[1] 47 | 48 | # Find PURR/USDC 49 | target_pair = None 50 | for pair in spot_meta.get("universe", []): 51 | if pair.get("name") == SYMBOL: 52 | target_pair = pair 53 | break 54 | 55 | if not target_pair: 56 | print(f"❌ Could not find {SYMBOL} in spot universe") 57 | return 58 | 59 | pair_index = target_pair.get("index") 60 | 61 | # Get current price 62 | if pair_index < len(asset_ctxs): 63 | ctx = asset_ctxs[pair_index] 64 | market_price = float(ctx.get("midPx", ctx.get("markPx", 0))) 65 | 66 | if market_price <= 0: 67 | print(f"❌ Could not get valid price for {SYMBOL}") 68 | return 69 | 70 | order_price = market_price * (1 + PRICE_OFFSET_PCT / 100) 71 | order_price = round(order_price, 6) # Round to 6 decimals 72 | 73 | print(f"💰 Current {SYMBOL} price: ${market_price}") 74 | print(f"📝 Placing BUY order: {ORDER_SIZE} {SYMBOL} @ ${order_price}") 75 | 76 | # Place the order using asset name for spot trading 77 | result = exchange.order( 78 | name=SYMBOL, 79 | is_buy=True, 80 | sz=ORDER_SIZE, 81 | limit_px=order_price, 82 | order_type=HLOrderType({"limit": {"tif": "Gtc"}}), 83 | reduce_only=False, 84 | ) 85 | 86 | print(f"📋 Order result:") 87 | print(json.dumps(result, indent=2)) 88 | 89 | if result and result.get("status") == "ok": 90 | response_data = result.get("response", {}).get("data", {}) 91 | statuses = response_data.get("statuses", []) 92 | 93 | if statuses: 94 | status_info = statuses[0] 95 | if "resting" in status_info: 96 | order_id = status_info["resting"]["oid"] 97 | print(f"✅ Order placed successfully! ID: {order_id}") 98 | print(f"🔍 Monitor this order in your WebSocket stream") 99 | elif "filled" in status_info: 100 | print(f"✅ Order filled immediately!") 101 | else: 102 | print(f"⚠️ Unexpected status: {status_info}") 103 | else: 104 | print(f"❌ Order failed: {result}") 105 | else: 106 | print(f"❌ Asset index {pair_index} out of range") 107 | else: 108 | print("❌ Could not get spot metadata") 109 | 110 | except Exception as e: 111 | print(f"❌ Error: {e}") 112 | 113 | 114 | if __name__ == "__main__": 115 | asyncio.run(place_spot_order()) 116 | -------------------------------------------------------------------------------- /learning_examples/02_market_data/get_market_metadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Retrieves trading metadata including size/price decimals, max leverage, and trading constraints. 3 | Calculates minimum order sizes and price tick sizes for major assets. 4 | """ 5 | 6 | import asyncio 7 | import os 8 | from dotenv import load_dotenv 9 | import httpx 10 | from hyperliquid.info import Info 11 | 12 | load_dotenv() 13 | 14 | BASE_URL = os.getenv("HYPERLIQUID_CHAINSTACK_BASE_URL") 15 | ASSETS_TO_ANALYZE = ["BTC", "ETH", "SOL"] 16 | 17 | 18 | async def method_1_sdk(): 19 | """Method 1: Using Hyperliquid Python SDK""" 20 | print("Method 1: Hyperliquid SDK") 21 | print("-" * 30) 22 | 23 | try: 24 | info = Info(BASE_URL, skip_ws=True) 25 | meta = info.meta() 26 | universe = meta.get("universe", []) 27 | 28 | print(f"Found {len(universe)} trading pairs") 29 | 30 | for asset_info in universe: 31 | asset_name = asset_info.get("name", "") 32 | if asset_name in ASSETS_TO_ANALYZE: 33 | print(f"\n{asset_name}:") 34 | print(f" Size decimals: {asset_info.get('szDecimals')}") 35 | print(f" Price decimals: {asset_info.get('priceDecimals')}") 36 | print(f" Max leverage: {asset_info.get('maxLeverage')}x") 37 | print(f" Only isolated: {asset_info.get('onlyIsolated', False)}") 38 | 39 | return meta 40 | 41 | except Exception as e: 42 | print(f"SDK method failed: {e}") 43 | return None 44 | 45 | 46 | async def method_2_raw_api(): 47 | """Method 2: Raw HTTP API call""" 48 | print("\nMethod 2: Raw HTTP API") 49 | print("-" * 30) 50 | 51 | try: 52 | async with httpx.AsyncClient() as client: 53 | response = await client.post( 54 | f"{BASE_URL}/info", 55 | json={"type": "meta"}, 56 | headers={"Content-Type": "application/json"}, 57 | ) 58 | 59 | if response.status_code == 200: 60 | meta = response.json() 61 | universe = meta.get("universe", []) 62 | 63 | print(f"Found {len(universe)} trading pairs") 64 | 65 | for asset_info in universe: 66 | asset_name = asset_info.get("name", "") 67 | if asset_name in ASSETS_TO_ANALYZE: 68 | print(f"\n{asset_name}:") 69 | print(f" Size decimals: {asset_info.get('szDecimals')}") 70 | print(f" Price decimals: {asset_info.get('priceDecimals')}") 71 | print(f" Max leverage: {asset_info.get('maxLeverage')}x") 72 | print( 73 | f" Only isolated: {asset_info.get('onlyIsolated', False)}" 74 | ) 75 | 76 | return meta 77 | else: 78 | print(f"HTTP failed: {response.status_code}") 79 | return None 80 | 81 | except Exception as e: 82 | print(f"HTTP method failed: {e}") 83 | return None 84 | 85 | 86 | async def calculate_trading_constraints(): 87 | """Calculate minimum sizes and price ticks""" 88 | print("\nTrading Constraints") 89 | print("-" * 25) 90 | 91 | try: 92 | async with httpx.AsyncClient() as client: 93 | response = await client.post( 94 | f"{BASE_URL}/info", 95 | json={"type": "meta"}, 96 | headers={"Content-Type": "application/json"}, 97 | ) 98 | 99 | if response.status_code == 200: 100 | meta = response.json() 101 | universe = meta.get("universe", []) 102 | 103 | for asset_info in universe[:3]: 104 | name = asset_info.get("name", "") 105 | sz_decimals = asset_info.get("szDecimals", 4) 106 | price_decimals = asset_info.get("priceDecimals", 2) 107 | 108 | min_size = 1 / (10**sz_decimals) 109 | price_tick = 1 / (10**price_decimals) 110 | 111 | print(f"\n{name}:") 112 | print(f" Min order size: {min_size:.{sz_decimals}f} {name}") 113 | print(f" Price tick size: ${price_tick:.{price_decimals}f}") 114 | print(f" Max leverage: {asset_info.get('maxLeverage')}x") 115 | 116 | except Exception as e: 117 | print(f"Analysis failed: {e}") 118 | 119 | 120 | async def main(): 121 | print("Hyperliquid Market Metadata") 122 | print("=" * 40) 123 | 124 | await method_1_sdk() 125 | await method_2_raw_api() 126 | await calculate_trading_constraints() 127 | 128 | 129 | if __name__ == "__main__": 130 | asyncio.run(main()) 131 | -------------------------------------------------------------------------------- /learning_examples/01_websockets/realtime_prices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Real-time price monitoring using WebSocket connections. 3 | Demonstrates subscribing to live market data and handling price updates. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | import signal 10 | from dotenv import load_dotenv 11 | import websockets 12 | from hyperliquid.info import Info 13 | 14 | load_dotenv() 15 | 16 | WS_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_WS_URL") 17 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_CHAINSTACK_BASE_URL") 18 | ASSETS_TO_TRACK = ["BTC", "ETH", "SOL", "DOGE", "AVAX"] 19 | 20 | # Global state for demo 21 | prices = {} 22 | id_to_symbol = {} 23 | running = False 24 | 25 | 26 | def signal_handler(signum, frame): 27 | """Handle Ctrl+C gracefully""" 28 | global running 29 | print("\nShutting down...") 30 | running = False 31 | 32 | 33 | async def load_symbol_mapping(): 34 | """Load mapping from asset IDs to symbols""" 35 | global id_to_symbol 36 | 37 | info = Info(BASE_URL, skip_ws=True) 38 | meta = info.meta() 39 | 40 | for i, asset_info in enumerate(meta["universe"]): 41 | symbol = asset_info["name"] 42 | id_to_symbol[str(i)] = symbol 43 | 44 | print(f"Loaded {len(id_to_symbol)} asset mappings") 45 | 46 | 47 | async def handle_price_message(data): 48 | """Process price update messages""" 49 | global prices 50 | 51 | channel = data.get("channel") 52 | if channel == "allMids": 53 | # Get the mids data from the nested structure 54 | mids_data = data.get("data", {}).get("mids", {}) 55 | 56 | # Update prices and show changes for tracked assets 57 | for asset_id_with_at, price_str in mids_data.items(): 58 | # Remove @ prefix from asset ID 59 | asset_id = asset_id_with_at.lstrip("@") 60 | symbol = id_to_symbol.get(asset_id) 61 | 62 | if symbol and symbol in ASSETS_TO_TRACK: 63 | try: 64 | new_price = float(price_str) 65 | old_price = prices.get(symbol) 66 | 67 | # Store new price 68 | prices[symbol] = new_price 69 | 70 | if old_price is not None: 71 | change = new_price - old_price 72 | change_pct = (change / old_price) * 100 if old_price != 0 else 0 73 | 74 | # Show all updates 75 | direction = "📈" if change > 0 else "📉" if change < 0 else "➡️" 76 | print( 77 | f"{direction} {symbol}: ${new_price:,.2f} ({change_pct:+.2f}%)" 78 | ) 79 | else: 80 | # First price update 81 | print(f"🔄 {symbol}: ${new_price:,.2f}") 82 | 83 | except (ValueError, TypeError): 84 | continue 85 | 86 | elif channel == "subscriptionResponse": 87 | print("✅ Subscription confirmed") 88 | 89 | 90 | async def monitor_prices(): 91 | """Connect to WebSocket and monitor real-time prices""" 92 | global running 93 | 94 | print("🔗 Loading asset mappings...") 95 | await load_symbol_mapping() 96 | 97 | print(f"🔗 Connecting to {WS_URL}") 98 | 99 | signal.signal(signal.SIGINT, signal_handler) 100 | 101 | try: 102 | async with websockets.connect(WS_URL) as websocket: 103 | print("✅ WebSocket connected!") 104 | 105 | subscribe_message = { 106 | "method": "subscribe", 107 | "subscription": {"type": "allMids"}, 108 | } 109 | 110 | await websocket.send(json.dumps(subscribe_message)) 111 | print(f"📊 Monitoring {', '.join(ASSETS_TO_TRACK)}") 112 | print("=" * 40) 113 | 114 | running = True 115 | 116 | # Listen for messages 117 | async for message in websocket: 118 | if not running: 119 | break 120 | 121 | try: 122 | data = json.loads(message) 123 | await handle_price_message(data) 124 | 125 | except json.JSONDecodeError: 126 | print("⚠️ Received invalid JSON") 127 | except Exception as e: 128 | print(f"❌ Error: {e}") 129 | 130 | except websockets.exceptions.ConnectionClosed: 131 | print("🔌 WebSocket connection closed") 132 | except Exception as e: 133 | print(f"❌ WebSocket error: {e}") 134 | finally: 135 | print("👋 Disconnected") 136 | 137 | 138 | async def main(): 139 | print("Hyperliquid Real-time Price Monitor") 140 | print("=" * 40) 141 | 142 | if not WS_URL or not BASE_URL: 143 | print("❌ Missing environment variables") 144 | print( 145 | "Set Hyperliquid endpoints in your .env file" 146 | ) 147 | return 148 | 149 | await monitor_prices() 150 | 151 | 152 | if __name__ == "__main__": 153 | print("Starting WebSocket demo...") 154 | asyncio.run(main()) 155 | -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/cancel_twap_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for cancelling TWAP orders to see how they appear in WebSocket. 3 | TWAP order cancellation uses raw API calls since they're not in the SDK yet. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | from dotenv import load_dotenv 10 | from eth_account import Account 11 | from hyperliquid.exchange import Exchange 12 | from hyperliquid.info import Info 13 | from hyperliquid.utils.signing import get_timestamp_ms, sign_l1_action 14 | 15 | load_dotenv() 16 | 17 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 18 | SYMBOL = "PURR/USDC" 19 | 20 | 21 | async def cancel_twap_order(): 22 | """Cancel a TWAP order using raw API""" 23 | print("Cancel TWAP Order Test") 24 | print("=" * 40) 25 | 26 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 27 | if not private_key: 28 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 29 | return 30 | 31 | try: 32 | wallet = Account.from_key(private_key) 33 | exchange = Exchange(wallet, BASE_URL) 34 | info = Info(BASE_URL, skip_ws=True) 35 | 36 | print(f"📱 Wallet: {wallet.address}") 37 | 38 | # Check for active TWAP orders 39 | print("🔍 Looking for active TWAP orders...") 40 | 41 | # Note: TWAP orders don't appear in regular open_orders() or frontend_open_orders() 42 | # In copy trading, you'd get the TWAP ID from WebSocket feed when tracking someone 43 | # For this test, we use the TWAP ID from place_twap_order.py output 44 | latest_twap_id = 8154 # Replace with actual TWAP ID you want to cancel 45 | coin = "PURR/USDC" # Replace with actual coin symbol 46 | 47 | print(f"🎯 Attempting to cancel TWAP order:") 48 | print(f" TWAP ID: {latest_twap_id}") 49 | print(f" Coin: {coin}") 50 | 51 | # Get spot metadata to find the asset index 52 | spot_data = info.spot_meta_and_asset_ctxs() 53 | if len(spot_data) < 2: 54 | print("❌ Could not get spot metadata") 55 | return 56 | 57 | spot_meta = spot_data[0] 58 | target_pair = None 59 | 60 | # Find the matching asset 61 | for pair in spot_meta.get("universe", []): 62 | if pair.get("name") == coin: 63 | target_pair = pair 64 | break 65 | 66 | if not target_pair: 67 | print(f"❌ Could not find asset {coin} in spot universe") 68 | return 69 | 70 | asset_index = target_pair.get("index") 71 | asset_name = target_pair.get("name") 72 | 73 | print( 74 | f"💰 Asset: {asset_name} (#{asset_index}, spot ID: {10000 + asset_index})" 75 | ) 76 | print(f"🔄 Cancelling TWAP order ID: {latest_twap_id}") 77 | 78 | # Prepare TWAP cancellation action 79 | twap_cancel_action = { 80 | "type": "twapCancel", 81 | "a": 10000 + asset_index, 82 | "t": latest_twap_id, 83 | } 84 | 85 | print("📋 TWAP cancel action:") 86 | print(json.dumps(twap_cancel_action, indent=2)) 87 | 88 | # Sign and send TWAP cancellation 89 | try: 90 | timestamp = get_timestamp_ms() 91 | 92 | signature = sign_l1_action( 93 | exchange.wallet, 94 | twap_cancel_action, 95 | exchange.vault_address, 96 | timestamp, 97 | exchange.expires_after, 98 | False, 99 | ) 100 | 101 | result = exchange._post_action( 102 | twap_cancel_action, 103 | signature, 104 | timestamp, 105 | ) 106 | 107 | print("📋 TWAP cancel result:") 108 | print(json.dumps(result, indent=2)) 109 | 110 | if result and result.get("status") == "ok": 111 | response_data = result.get("response", {}).get("data", {}) 112 | 113 | if response_data.get("status") == "success": 114 | print(f"✅ TWAP order {latest_twap_id} cancelled successfully!") 115 | print("🔍 Monitor this cancellation in your WebSocket stream") 116 | elif ( 117 | isinstance(response_data.get("status"), dict) 118 | and "error" in response_data["status"] 119 | ): 120 | error_msg = response_data["status"]["error"] 121 | print(f"❌ TWAP cancel failed: {error_msg}") 122 | if ( 123 | "never placed" in error_msg.lower() 124 | or "already canceled" in error_msg.lower() 125 | ): 126 | print( 127 | "💡 TWAP order may have already finished or been cancelled" 128 | ) 129 | else: 130 | print(f"⚠️ Unexpected response: {response_data}") 131 | else: 132 | print(f"❌ TWAP cancel request failed: {result}") 133 | 134 | except Exception as api_error: 135 | print(f"❌ TWAP Cancel API Error: {api_error}") 136 | print("⚠️ TWAP cancellation may not be available on testnet") 137 | 138 | except Exception as e: 139 | print(f"❌ Error: {e}") 140 | 141 | 142 | if __name__ == "__main__": 143 | asyncio.run(cancel_twap_order()) 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Extensible grid trading bot for [Hyperliquid DEX](https://hyperliquid.xyz) 2 | 3 | > ⚠️ This software is for educational and research purposes. Trading cryptocurrencies involves substantial risk of loss. Never trade with funds you cannot afford to lose. Always thoroughly test strategies on testnet before live deployment. 4 | 5 | This project is under active development. Feel free to submit questions, suggestions, and issues through GitHub. 6 | 7 | You're welcome to use the best docs on Hyperliquid API via [Chainstack Developer Portal MCP server](https://docs.chainstack.com/docs/developer-portal-mcp-server). 8 | 9 | ## 🚀 Quick start 10 | 11 | ### **Prerequisites** 12 | - [uv package manager](https://github.com/astral-sh/uv) 13 | - Hyperliquid testnet account with testnet funds (see [Chainstack Hyperliquid faucet](https://faucet.chainstack.com/hyperliquid-testnet-faucet)) 14 | 15 | ### **Installation** 16 | 17 | ```bash 18 | # Clone the repository 19 | git clone https://github.com/chainstacklabs/hyperliquid-trading-bot 20 | cd hyperliquid-trading-bot 21 | 22 | # Install dependencies using uv 23 | uv sync 24 | 25 | # Set up environment variables 26 | cp .env.example .env 27 | # Edit .env with your Hyperliquid testnet private key 28 | ``` 29 | 30 | ### **Configuration** 31 | 32 | Create your environment file: 33 | ```bash 34 | # .env 35 | HYPERLIQUID_TESTNET_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE 36 | HYPERLIQUID_TESTNET=true 37 | ``` 38 | 39 | The bot comes with a pre-configured conservative BTC grid strategy in `bots/btc_conservative.yaml`. Review and adjust parameters as needed. 40 | 41 | ### **Running the bot** 42 | 43 | ```bash 44 | # Auto-discover and run the first active configuration 45 | uv run src/run_bot.py 46 | 47 | # Validate configuration before running 48 | uv run src/run_bot.py --validate 49 | 50 | # Run specific configuration 51 | uv run src/run_bot.py bots/btc_conservative.yaml 52 | ``` 53 | 54 | ## ⚙️ Configuration 55 | 56 | Bot configurations use YAML format with comprehensive parameter documentation: 57 | 58 | ```yaml 59 | # Conservative BTC Grid Strategy 60 | name: "btc_conservative_clean" 61 | active: true # Enable/disable this strategy 62 | 63 | account: 64 | max_allocation_pct: 10.0 # Use only 10% of account balance 65 | 66 | grid: 67 | symbol: "BTC" 68 | levels: 10 # Number of grid levels 69 | price_range: 70 | mode: "auto" # Auto-calculate from current price 71 | auto: 72 | range_pct: 5.0 # ±5% price range (conservative) 73 | 74 | risk_management: 75 | # Exit Strategies 76 | stop_loss_enabled: false # Auto-close positions on loss threshold 77 | stop_loss_pct: 8.0 # Loss % before closing (1-20%) 78 | take_profit_enabled: false # Auto-close positions on profit threshold 79 | take_profit_pct: 25.0 # Profit % before closing (5-100%) 80 | 81 | # Account Protection 82 | max_drawdown_pct: 15.0 # Stop trading on account drawdown % (5-50%) 83 | max_position_size_pct: 40.0 # Max position as % of account (10-100%) 84 | 85 | # Grid Rebalancing 86 | rebalance: 87 | price_move_threshold_pct: 12.0 # Rebalance trigger 88 | 89 | monitoring: 90 | log_level: "INFO" # DEBUG/INFO/WARNING/ERROR 91 | ``` 92 | 93 | ## 📚 Learning examples 94 | 95 | Master the Hyperliquid API with standalone educational scripts: 96 | 97 | ```bash 98 | # Authentication and connection 99 | uv run learning_examples/01_authentication/basic_connection.py 100 | 101 | # Market data and pricing 102 | uv run learning_examples/02_market_data/get_all_prices.py 103 | uv run learning_examples/02_market_data/get_market_metadata.py 104 | 105 | # Account information 106 | uv run learning_examples/03_account_info/get_user_state.py 107 | uv run learning_examples/03_account_info/get_open_orders.py 108 | 109 | # Trading operations 110 | uv run learning_examples/04_trading/place_limit_order.py 111 | uv run learning_examples/04_trading/cancel_orders.py 112 | 113 | # Real-time data 114 | uv run learning_examples/05_websockets/realtime_prices.py 115 | ``` 116 | 117 | ## 🛡️ Exit strategies 118 | 119 | The bot includes automated risk management and position exit features: 120 | 121 | **Position-level exits:** 122 | - **Stop loss**: Automatically close positions when loss exceeds configured percentage (1-20%) 123 | - **Take profit**: Automatically close positions when profit exceeds configured percentage (5-100%) 124 | 125 | **Account-level protection:** 126 | - **Max drawdown**: Stop all trading when account-level losses exceed threshold (5-50%) 127 | - **Position size limits**: Prevent individual positions from exceeding percentage of account (10-100%) 128 | 129 | **Operational exits:** 130 | - **Grid rebalancing**: Cancel orders and recreate grid when price moves outside range 131 | - **Graceful shutdown**: Cancel pending orders on bot termination (positions preserved by default) 132 | 133 | All exit strategies are configurable per bot and disabled by default for safety. 134 | 135 | ## 🔧 Development 136 | 137 | ### **Package management** 138 | This project uses [uv](https://github.com/astral-sh/uv) for fast, reliable dependency management: 139 | 140 | ```bash 141 | uv sync # Install/sync dependencies 142 | uv add # Add new dependencies 143 | uv run # Run commands in virtual environment 144 | ``` 145 | 146 | ### **Testing** 147 | All components are tested against Hyperliquid testnet: 148 | 149 | ```bash 150 | # Test learning examples 151 | uv run learning_examples/04_trading/place_limit_order.py 152 | 153 | # Validate bot configuration 154 | uv run src/run_bot.py --validate 155 | 156 | # Run bot in testnet mode (default) 157 | uv run src/run_bot.py 158 | ``` 159 | -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/place_twap_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for placing TWAP orders to see how they appear in WebSocket. 3 | TWAP orders use raw API calls since they're not in the SDK yet. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | from dotenv import load_dotenv 10 | 11 | from eth_account import Account 12 | from hyperliquid.exchange import Exchange 13 | from hyperliquid.info import Info 14 | from hyperliquid.utils.signing import get_timestamp_ms, sign_l1_action, float_to_wire 15 | 16 | 17 | load_dotenv() 18 | 19 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 20 | SYMBOL = "PURR/USDC" # Spot pair to match place_order.py 21 | ORDER_SIZE = 20.0 # Total size 22 | TWAP_DURATION_MINUTES = 5 # 5 min duration 23 | 24 | 25 | async def place_twap_order(): 26 | """Place a TWAP order using raw API""" 27 | print("Place TWAP Order Test") 28 | print("=" * 40) 29 | 30 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 31 | if not private_key: 32 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 33 | return 34 | 35 | try: 36 | wallet = Account.from_key(private_key) 37 | exchange = Exchange(wallet, BASE_URL) 38 | info = Info(BASE_URL, skip_ws=True) 39 | 40 | print(f"📱 Wallet: {wallet.address}") 41 | 42 | # Get spot metadata to find the asset 43 | spot_data = info.spot_meta_and_asset_ctxs() 44 | if len(spot_data) >= 2: 45 | spot_meta = spot_data[0] 46 | 47 | # Find PURR/USDC 48 | target_pair = None 49 | for pair in spot_meta.get("universe", []): 50 | if pair.get("name") == SYMBOL: 51 | target_pair = pair 52 | break 53 | 54 | if not target_pair: 55 | print(f"❌ Could not find {SYMBOL} in spot universe") 56 | return 57 | 58 | pair_index = target_pair.get("index") 59 | 60 | print(f"💰 Asset: {SYMBOL} (#{pair_index}, spot ID: {10000 + pair_index})") 61 | print( 62 | f"📝 Placing TWAP BUY order: {ORDER_SIZE} {SYMBOL} over {TWAP_DURATION_MINUTES} minutes" 63 | ) 64 | 65 | twap_action = { 66 | "type": "twapOrder", 67 | "twap": { 68 | "a": 10000 + pair_index, # Asset 69 | "b": True, # Buy/sell 70 | "s": float_to_wire( 71 | ORDER_SIZE 72 | ), # Use SDK's conversion to avoid trailing zeros 73 | "r": False, # Reduce-only 74 | "m": TWAP_DURATION_MINUTES, # Minutes 75 | "t": False, # Randomize 76 | }, 77 | } 78 | 79 | print("📋 TWAP order action:") 80 | print(json.dumps(twap_action, indent=2)) 81 | 82 | try: 83 | print(f"🔐 Sending TWAP order with wallet: {exchange.wallet.address}") 84 | 85 | # Use exchange's internal methods to sign and send the TWAP order 86 | timestamp = get_timestamp_ms() 87 | 88 | signature = sign_l1_action( 89 | exchange.wallet, 90 | twap_action, 91 | exchange.vault_address, 92 | timestamp, 93 | exchange.expires_after, 94 | False, 95 | ) 96 | 97 | result = exchange._post_action( 98 | twap_action, 99 | signature, 100 | timestamp, 101 | ) 102 | 103 | print("📋 TWAP order result:") 104 | print(json.dumps(result, indent=2)) 105 | 106 | if result and result.get("status") == "ok": 107 | response_data = result.get("response", {}).get("data", {}) 108 | status_info = response_data.get("status", {}) 109 | 110 | if "running" in status_info: 111 | twap_id = status_info["running"]["twapId"] 112 | print(f"✅ TWAP order placed successfully! TWAP ID: {twap_id}") 113 | print("🔍 Monitor this TWAP order in your WebSocket stream") 114 | print( 115 | f"⏱️ Order will execute over {TWAP_DURATION_MINUTES} minutes" 116 | ) 117 | else: 118 | print(f"⚠️ Unexpected TWAP status: {status_info}") 119 | else: 120 | print(f"❌ TWAP order failed: {result}") 121 | if ( 122 | result 123 | and isinstance(result, dict) 124 | and "Invalid TWAP duration" in str(result.get("response", "")) 125 | ): 126 | print( 127 | "💡 Try increasing TWAP_DURATION_MINUTES (minimum may be required)" 128 | ) 129 | 130 | except Exception as api_error: 131 | import traceback 132 | 133 | print(f"❌ TWAP API Error: {str(api_error)}") 134 | print(f"❌ Error type: {type(api_error).__name__}") 135 | print("❌ Full traceback:") 136 | traceback.print_exc() 137 | print( 138 | "⚠️ TWAP orders may not be available on testnet or for this asset" 139 | ) 140 | print("💡 Try with a mainnet connection or different asset if needed") 141 | else: 142 | print("❌ Could not get spot metadata") 143 | 144 | except Exception as e: 145 | print(f"❌ Error: {e}") 146 | print("💡 Note: TWAP orders may not be available on testnet or for all assets") 147 | 148 | 149 | if __name__ == "__main__": 150 | asyncio.run(place_twap_order()) 151 | -------------------------------------------------------------------------------- /src/interfaces/exchange.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exchange Interface 3 | 4 | Simple interface for integrating new exchanges/DEXes. 5 | Newbies can add new exchanges by implementing this interface. 6 | """ 7 | 8 | from abc import ABC, abstractmethod 9 | from typing import Dict, List, Optional, Any 10 | from dataclasses import dataclass 11 | from enum import Enum 12 | 13 | 14 | class OrderSide(Enum): 15 | """Order side""" 16 | 17 | BUY = "buy" 18 | SELL = "sell" 19 | 20 | 21 | class OrderType(Enum): 22 | """Order type""" 23 | 24 | MARKET = "market" 25 | LIMIT = "limit" 26 | 27 | 28 | class OrderStatus(Enum): 29 | """Order status""" 30 | 31 | PENDING = "pending" 32 | SUBMITTED = "submitted" 33 | PARTIALLY_FILLED = "partially_filled" 34 | FILLED = "filled" 35 | CANCELLED = "cancelled" 36 | REJECTED = "rejected" 37 | 38 | 39 | @dataclass 40 | class Order: 41 | """Order representation""" 42 | 43 | id: str 44 | asset: str 45 | side: OrderSide 46 | size: float 47 | order_type: OrderType 48 | price: Optional[float] = None 49 | status: OrderStatus = OrderStatus.PENDING 50 | filled_size: float = 0.0 51 | average_fill_price: float = 0.0 52 | exchange_order_id: Optional[str] = None 53 | created_at: float = 0.0 # Timestamp when order was created 54 | 55 | 56 | @dataclass 57 | class Balance: 58 | """Account balance""" 59 | 60 | asset: str 61 | available: float 62 | locked: float 63 | total: float 64 | 65 | 66 | @dataclass 67 | class MarketInfo: 68 | """Market/trading pair information""" 69 | 70 | symbol: str 71 | base_asset: str 72 | quote_asset: str 73 | min_order_size: float 74 | price_precision: int 75 | size_precision: int 76 | is_active: bool 77 | 78 | 79 | class ExchangeAdapter(ABC): 80 | """ 81 | Base interface for all exchange integrations. 82 | 83 | This is the ONLY class newbies need to understand to add new exchanges. 84 | 85 | Example implementation: 86 | 87 | class MyDEXAdapter(ExchangeAdapter): 88 | def __init__(self, api_key, secret): 89 | super().__init__("MyDEX") 90 | self.api_key = api_key 91 | self.secret = secret 92 | 93 | async def get_balance(self, asset): 94 | # Call your DEX API 95 | return Balance(asset, available=1000, locked=0, total=1000) 96 | """ 97 | 98 | def __init__(self, exchange_name: str): 99 | self.exchange_name = exchange_name 100 | self.is_connected = False 101 | 102 | @abstractmethod 103 | async def connect(self) -> bool: 104 | """ 105 | Connect to the exchange. 106 | 107 | Returns: 108 | True if connection successful, False otherwise 109 | """ 110 | pass 111 | 112 | @abstractmethod 113 | async def disconnect(self) -> None: 114 | """Disconnect from the exchange.""" 115 | pass 116 | 117 | @abstractmethod 118 | async def get_balance(self, asset: str) -> Balance: 119 | """ 120 | Get account balance for an asset. 121 | 122 | Args: 123 | asset: Asset symbol (e.g., "BTC", "ETH") 124 | 125 | Returns: 126 | Balance information 127 | """ 128 | pass 129 | 130 | @abstractmethod 131 | async def get_market_price(self, asset: str) -> float: 132 | """ 133 | Get current market price for an asset. 134 | 135 | Args: 136 | asset: Asset symbol 137 | 138 | Returns: 139 | Current market price 140 | """ 141 | pass 142 | 143 | @abstractmethod 144 | async def place_order(self, order: Order) -> str: 145 | """ 146 | Place an order on the exchange. 147 | 148 | Args: 149 | order: Order to place 150 | 151 | Returns: 152 | Exchange order ID 153 | """ 154 | pass 155 | 156 | @abstractmethod 157 | async def cancel_order(self, exchange_order_id: str) -> bool: 158 | """ 159 | Cancel an order. 160 | 161 | Args: 162 | exchange_order_id: Exchange's order ID 163 | 164 | Returns: 165 | True if cancelled successfully 166 | """ 167 | pass 168 | 169 | @abstractmethod 170 | async def get_order_status(self, exchange_order_id: str) -> Order: 171 | """ 172 | Get order status. 173 | 174 | Args: 175 | exchange_order_id: Exchange's order ID 176 | 177 | Returns: 178 | Updated order information 179 | """ 180 | pass 181 | 182 | @abstractmethod 183 | async def get_market_info(self, asset: str) -> MarketInfo: 184 | """ 185 | Get market information for an asset. 186 | 187 | Args: 188 | asset: Asset symbol 189 | 190 | Returns: 191 | Market information 192 | """ 193 | pass 194 | 195 | # Position management methods (optional - implement if exchange supports positions) 196 | 197 | async def get_positions(self) -> List["Position"]: 198 | """ 199 | Get all current positions. 200 | 201 | Returns: 202 | List of current positions (empty if not supported) 203 | """ 204 | return [] 205 | 206 | async def close_position(self, asset: str, size: Optional[float] = None) -> bool: 207 | """ 208 | Close a position (market sell/buy to close). 209 | 210 | Args: 211 | asset: Asset symbol 212 | size: Size to close (None = close entire position) 213 | 214 | Returns: 215 | True if close order placed successfully 216 | """ 217 | return False 218 | 219 | async def get_account_metrics(self) -> Dict[str, Any]: 220 | """ 221 | Get account-level metrics for risk assessment. 222 | 223 | Returns: 224 | Dictionary with account metrics (total_value, pnl, drawdown, etc.) 225 | """ 226 | return { 227 | "total_value": 0.0, 228 | "total_pnl": 0.0, 229 | "unrealized_pnl": 0.0, 230 | "realized_pnl": 0.0, 231 | "drawdown_pct": 0.0, 232 | } 233 | 234 | # Optional methods with default implementations 235 | 236 | async def get_open_orders(self) -> List[Order]: 237 | """Get all open orders. Override if exchange supports this.""" 238 | return [] 239 | 240 | async def cancel_all_orders(self) -> int: 241 | """Cancel all open orders. Override if exchange supports this.""" 242 | orders = await self.get_open_orders() 243 | cancelled = 0 244 | for order in orders: 245 | if order.exchange_order_id: 246 | if await self.cancel_order(order.exchange_order_id): 247 | cancelled += 1 248 | return cancelled 249 | 250 | def get_status(self) -> Dict[str, Any]: 251 | """Get exchange adapter status.""" 252 | return {"exchange": self.exchange_name, "connected": self.is_connected} 253 | 254 | async def health_check(self) -> bool: 255 | """Perform health check. Override for exchange-specific checks.""" 256 | return self.is_connected 257 | -------------------------------------------------------------------------------- /src/run_bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Grid Trading Bot Runner 4 | 5 | Clean, simple entry point for running grid trading strategies. 6 | No confusing naming - just "run_bot.py". 7 | """ 8 | 9 | import asyncio 10 | import argparse 11 | import sys 12 | import os 13 | import signal 14 | from pathlib import Path 15 | import yaml 16 | from typing import Optional 17 | 18 | # Load .env file if it exists 19 | from dotenv import load_dotenv 20 | 21 | load_dotenv() 22 | 23 | # Add src to path for imports 24 | sys.path.append(str(Path(__file__).parent)) 25 | 26 | from core.engine import TradingEngine 27 | from core.enhanced_config import EnhancedBotConfig 28 | 29 | 30 | class GridTradingBot: 31 | """ 32 | Simple grid trading bot runner 33 | 34 | Clean interface - no "enhanced" or "advanced" confusion. 35 | Just a bot that runs grid trading strategies. 36 | """ 37 | 38 | def __init__(self, config_path: str): 39 | self.config_path = config_path 40 | self.config = None 41 | self.engine = None 42 | self.running = False 43 | 44 | # Setup signal handlers 45 | signal.signal(signal.SIGINT, self._signal_handler) 46 | signal.signal(signal.SIGTERM, self._signal_handler) 47 | 48 | def _signal_handler(self, signum, frame): 49 | """Handle shutdown signals""" 50 | print(f"\n📡 Received signal {signum}, shutting down...") 51 | self.running = False 52 | if self.engine: 53 | asyncio.create_task(self.engine.stop()) 54 | 55 | async def run(self) -> None: 56 | """Run the bot""" 57 | 58 | try: 59 | # Load configuration 60 | print(f"📁 Loading configuration: {self.config_path}") 61 | self.config = EnhancedBotConfig.from_yaml(Path(self.config_path)) 62 | print(f"✅ Configuration loaded: {self.config.name}") 63 | 64 | # Convert to engine config format 65 | engine_config = self._convert_config() 66 | 67 | # Initialize trading engine 68 | self.engine = TradingEngine(engine_config) 69 | 70 | if not await self.engine.initialize(): 71 | print("❌ Failed to initialize trading engine") 72 | return 73 | 74 | # Start trading 75 | print(f"🚀 Starting {self.config.name}") 76 | self.running = True 77 | await self.engine.start() 78 | 79 | except KeyboardInterrupt: 80 | print("\n📡 Keyboard interrupt received") 81 | except Exception as e: 82 | print(f"❌ Error: {e}") 83 | finally: 84 | if self.engine: 85 | await self.engine.stop() 86 | 87 | def _convert_config(self) -> dict: 88 | """Convert EnhancedBotConfig to engine config format""" 89 | 90 | testnet = os.getenv("HYPERLIQUID_TESTNET", "true").lower() == "true" 91 | 92 | # Calculate total allocation in USD from account balance percentage 93 | # Note: This is a simplified approach - in production, you'd get actual account balance 94 | # For now, using a default base amount of $1000 USD 95 | base_allocation_usd = 1000.0 96 | total_allocation_usd = base_allocation_usd * ( 97 | self.config.account.max_allocation_pct / 100.0 98 | ) 99 | 100 | return { 101 | "exchange": { 102 | "type": self.config.exchange.type, 103 | "testnet": self.config.exchange.testnet, 104 | }, 105 | "strategy": { 106 | "type": "basic_grid", # Default to basic grid 107 | "symbol": self.config.grid.symbol, 108 | "levels": self.config.grid.levels, 109 | "range_pct": self.config.grid.price_range.auto.range_pct, 110 | "total_allocation": total_allocation_usd, 111 | "rebalance_threshold_pct": self.config.risk_management.rebalance.price_move_threshold_pct, 112 | }, 113 | "bot_config": { 114 | # Pass through the entire config so KeyManager can look for bot-specific keys 115 | "name": self.config.name, 116 | "private_key_file": getattr(self.config, "private_key_file", None), 117 | "testnet_key_file": getattr(self.config, "testnet_key_file", None), 118 | "mainnet_key_file": getattr(self.config, "mainnet_key_file", None), 119 | "private_key": getattr(self.config, "private_key", None), 120 | "testnet_private_key": getattr( 121 | self.config, "testnet_private_key", None 122 | ), 123 | "mainnet_private_key": getattr( 124 | self.config, "mainnet_private_key", None 125 | ), 126 | }, 127 | "log_level": self.config.monitoring.log_level, 128 | } 129 | 130 | 131 | def find_first_active_config() -> Optional[Path]: 132 | """Find the first active config in the bots folder""" 133 | 134 | # Look for bots folder relative to the script location 135 | script_dir = Path(__file__).parent 136 | bots_dir = script_dir.parent / "bots" 137 | 138 | if not bots_dir.exists(): 139 | return None 140 | 141 | # Scan for YAML files 142 | yaml_files = list(bots_dir.glob("*.yaml")) + list(bots_dir.glob("*.yml")) 143 | 144 | for yaml_file in sorted(yaml_files): 145 | try: 146 | with open(yaml_file, "r") as f: 147 | data = yaml.safe_load(f) 148 | 149 | # Check if config is active 150 | if data and data.get("active", False): 151 | print(f"📁 Found active config: {yaml_file.name}") 152 | return yaml_file 153 | 154 | except Exception as e: 155 | print(f"⚠️ Error reading {yaml_file.name}: {e}") 156 | continue 157 | 158 | return None 159 | 160 | 161 | async def main(): 162 | """Main entry point""" 163 | parser = argparse.ArgumentParser(description="Grid Trading Bot") 164 | parser.add_argument( 165 | "config", 166 | nargs="?", 167 | help="Configuration file path (optional - will auto-discover if not provided)", 168 | ) 169 | parser.add_argument( 170 | "--validate", action="store_true", help="Validate configuration only" 171 | ) 172 | 173 | args = parser.parse_args() 174 | 175 | # Determine config file 176 | config_path = None 177 | if args.config: 178 | config_path = Path(args.config) 179 | if not config_path.exists(): 180 | print(f"❌ Config file not found: {args.config}") 181 | return 1 182 | else: 183 | # Auto-discover first active config 184 | print("🔍 No config specified, auto-discovering active config...") 185 | config_path = find_first_active_config() 186 | if not config_path: 187 | print("❌ No active config found in bots/ folder") 188 | print("💡 Create a config file in bots/ folder with 'active: true'") 189 | return 1 190 | 191 | if args.validate: 192 | # Just validate the config 193 | try: 194 | config = EnhancedBotConfig.from_yaml(config_path) 195 | config.validate() 196 | print("✅ Configuration is valid") 197 | return 0 198 | except Exception as e: 199 | print(f"❌ Configuration error: {e}") 200 | return 1 201 | 202 | # Run the bot 203 | bot = GridTradingBot(str(config_path)) 204 | await bot.run() 205 | return 0 206 | 207 | 208 | if __name__ == "__main__": 209 | sys.exit(asyncio.run(main())) 210 | -------------------------------------------------------------------------------- /learning_examples/05_funding/README.md: -------------------------------------------------------------------------------- 1 | # Funding Rate Examples 2 | 3 | This directory contains examples for monitoring and analyzing funding rates on Hyperliquid. 4 | 5 | ## Scripts 6 | 7 | - **`get_funding_rates.py`** - Fetch current funding rates for perpetual markets 8 | - **`check_spot_perp_pairs_availability.py`** - Find assets tradable in both spot and perp markets (for funding arbitrage) 9 | - **`check_spot_perp_availability.py`** - DEPRECATED: Use `check_spot_perp_pairs_availability.py` instead 10 | 11 | ## Why Funding Rates Matter 12 | 13 | Funding rates determine the cost of holding leveraged perpetual positions. Understanding and monitoring them is critical for: 14 | - Identifying funding arbitrage opportunities 15 | - Avoiding trades with structural headwinds 16 | - Managing position costs and risk 17 | 18 | --- 19 | 20 | # How to Monitor and Operationalize Funding 21 | 22 | **Pre-requisite:** [Hyperliquid Docs: Funding](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/funding) 23 | ``` 24 | Funding $ = Position Size × Oracle Price × Funding Rate 25 | ``` 26 | 27 | ## Why monitoring funding keeps you alive 28 | 29 | Funding tells you three things simultaneously: 30 | 31 | 1. Which side is crowded 32 | 33 | 2. How expensive it is to stay wrong 34 | 35 | 3. How fast your margin is decaying 36 | 37 | Monitoring funding lets you 38 | 39 | - Avoid entering trades with structural headwinds 40 | 41 | - Size leverage safely 42 | 43 | - Decide hold vs scalp 44 | 45 | - Decide exit vs endure 46 | 47 | - Decide flip vs hedge 48 | 49 | ## Bot checklist - Before entering a trade 50 | 51 | ### 1. Funding direction vs your position 52 | 53 | - Paying funding → you’re on the crowded side 54 | 55 | - Receiving funding → you’re on the correcting side 56 | 57 | "Trade horizon > 1 funding period" means your strategy expects to hold the position long enough to cross at least one funding event (funding is hourly). 58 | 59 | ``` 60 | If [paying funding] AND [trade horizon > 1 funding period] → penalize entry 61 | ``` 62 | 63 | Penalizing entry includes one or more: 64 | 65 | - Reduce size 66 | 67 | - Require stronger signal 68 | 69 | - Shorten time horizon 70 | 71 | - Improve entry price 72 | 73 | - Raise exit urgency 74 | 75 | You can encode penalize in logic as follows: 76 | 77 | *Size penalty* 78 | 79 | ``` 80 | Effective size = base size × funding_penalty_factor 81 | ``` 82 | 83 | Example: funding_penalty_factor = 0.5 84 | 85 | *Signal threshold* 86 | 87 | ``` 88 | If paying funding: 89 | required_signal_strength += delta 90 | ``` 91 | 92 | Could be confidence from an ML model 93 | 94 | *Time constraint* 95 | 96 | ``` 97 | If paying funding: 98 | max_hold_time = 1 funding period 99 | ``` 100 | 101 | ### 2. Normalize funding to account size 102 | 103 | ``` 104 | Daily funding cost = Notional × hourly funding × 24 105 | Funding burn % = Daily funding cost / Account equity 106 | ``` 107 | 108 | Thresholds: 109 | 110 | - < 0.5% / day → safe 111 | 112 | - 0.5 – 1% → caution 113 | 114 | - 1% → high risk 115 | 116 | - 2% → do not enter 117 | 118 | ### 3. Funding vs Expected edge 119 | 120 | Ask: Can my signal realistically overcome funding? 121 | 122 | ``` 123 | if [Expected move] < [2 × daily funding cost] → trade is structurally negative 124 | ``` 125 | 126 | Funding is a fixed tax on weak signals 127 | 128 | ### 4. Funding trend 129 | 130 | Look at: 131 | 132 | - Funding now 133 | 134 | - Funding 3h ago 135 | 136 | - Funding 24h ago 137 | 138 | ``` 139 | If funding accelerating against position → reduce size or avoid entry 140 | ``` 141 | 142 | Acceleration can drain your account faster 143 | 144 | ### 5. Volatility adjusted leverage cap 145 | 146 | High funding + high volatility means your max leverage must decrease 147 | 148 | ``` 149 | Max leverage = min(base_leverage, 1 / daily funding %) 150 | ``` 151 | 152 | Example: 153 | 154 | Funding = 1% per day → max leverage = 1x 155 | 156 | ### 6. Funding and time alignment 157 | 158 | Funding is time based 159 | 160 | Ask: How many funding events will I sit through? 161 | 162 | If trade horizon: 163 | 164 | - <1h → funding irrelevant 165 | 166 | - 4–12h → funding matters 167 | 168 | - Overnight → funding critical 169 | 170 | ## Bot checklist - During an active trade 171 | 172 | ### 7. Track funding burn in real time 173 | 174 | Maintain 175 | 176 | ``` 177 | Funding paid so far 178 | Funding paid as % of equity 179 | ``` 180 | 181 | Hard stop 182 | 183 | ``` 184 | If [funding loss] > [X% of planned max loss] → exit 185 | ``` 186 | 187 | Don't let funding kill you slowly 188 | 189 | ### 8. Watch liquidation distance shrink 190 | 191 | Funding reduces equity, and therefore liquidation price moves toward you. 192 | 193 | If normal market noise is large enough to reach your liquidation price, you are already dead but you just don’t know it yet. 194 | 195 | ATR = Average True Range. ATR is a measure of how much an asset typically moves over a given time window. You can understand how big normal price moves are currently. 196 | 197 | Liquidation distance is how far price can move against you before liquidation. 198 | 199 | Y is your safety multiplier. 200 | 201 | Common values: 202 | 203 | - Y = 1 → extremely dangerous 204 | 205 | - Y = 2 → risky 206 | 207 | - Y = 3 → minimum survivable 208 | 209 | - Y = 4–5 → professional-grade buffer 210 | 211 | For a long: 212 | 213 | ``` 214 | Liquidation distance = Entry price − Liquidation price 215 | ``` 216 | 217 | For a short: 218 | 219 | ``` 220 | Liquidation distance = Liquidation price − Entry price 221 | ``` 222 | 223 | Measured in price units ($) 224 | 225 | ``` 226 | If [liquidation distance] < [Y × recent ATR] → reduce size or exit 227 | ``` 228 | 229 | If 230 | 231 | ``` 232 | Liquidation distance < 1 × ATR 233 | ``` 234 | 235 | Then a single normal candle with no trend change can liquidate you. Hence that is not acceptable risk. 236 | 237 | Configure your bot such that 238 | 239 | ``` 240 | Liquidation distance ≥ 3 × ATR 241 | ``` 242 | 243 | Operationally if your rule is violated: 244 | 245 | - Reduce size 246 | 247 | - Lower notional 248 | - Increase liquidation distance 249 | - Keeps trade idea alive 250 | 251 | - Exit 252 | 253 | - If funding + volatility + price align against you 254 | - Capital preservation > Conviction 255 | 256 | ``` 257 | Compute ATR over timeframe matching holding horizon 258 | Compute liquidation distance 259 | If liquidation_distance < Y × ATR: 260 | if partial reduction possible: 261 | reduce size 262 | else: 263 | exit position 264 | ``` 265 | 266 | ### 9. Funding flip alerts 267 | 268 | A funding flip happens when: 269 | 270 | - Funding was positive and becomes negative 271 | 272 | - Or funding was negative and becomes positive 273 | 274 | 275 | Funding flips often signal 276 | 277 | - Crowded side unwinding 278 | 279 | - Price mean reversion 280 | 281 | Bot action: 282 | 283 | ``` 284 | If funding flips sign: 285 | tighten stops 286 | or partially exit 287 | or Reduce exposure 288 | ``` 289 | 290 | ``` 291 | If funding_rate[t] * funding_rate[t-1] < 0: 292 | funding_flip = True 293 | 294 | If funding_flip: 295 | tighten_stop = True 296 | reduce_size = partial 297 | lower max_hold_time 298 | ``` 299 | 300 | When funding flips, the crowd has moved. 301 | 302 | Don’t be the last one holding risk. 303 | 304 | ## Funding management Tools 305 | 306 | 1. Reduce notional (best lever) 307 | 2. Reduce leverage (does not change funding but increases time to react) 308 | 3. Shorten holding period 309 | 4. Flip bias when funding is hostile 310 | 5. Hedge temporarily 311 | 312 | ## Minimum viable ruleset 313 | 314 | ``` 315 | ENTRY: 316 | - Funding burn < 1% equity / day 317 | - Funding not accelerating against position 318 | - Expected move > 2× funding cost 319 | 320 | ACTIVE: 321 | - Exit if funding loss > 30% of max risk 322 | - Exit if liquidation distance < 3× ATR 323 | - Reduce size if funding spikes 2 reminder checks in a row 324 | ``` -------------------------------------------------------------------------------- /learning_examples/06_copy_trading/order_scenarios/place_orders_limit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for placing different types of limit orders on spot market. 3 | Tests scenarios 1-9: GTC, IOC, ALO limit orders and time-limited orders. 4 | 5 | Available scenarios (1-9): 6 | === LIMIT ORDERS === 7 | 1. GTC Limit Buy 2. IOC Limit Buy 3. ALO Limit Buy 8 | 4. GTC Limit Sell 5. IOC Limit Sell 6. ALO Limit Sell 9 | 7. GTC Reduce-Only Buy 8. GTC Reduce-Only Sell 9. GTC Expires-After (30s) 10 | 11 | All limit orders use 50% price offset to avoid immediate fills. 12 | """ 13 | 14 | import asyncio 15 | import os 16 | import time 17 | from dotenv import load_dotenv 18 | from eth_account import Account 19 | from hyperliquid.exchange import Exchange 20 | from hyperliquid.info import Info 21 | from hyperliquid.utils.signing import OrderType as HLOrderType 22 | 23 | load_dotenv() 24 | 25 | BASE_URL = os.getenv("HYPERLIQUID_TESTNET_PUBLIC_BASE_URL") 26 | SYMBOL = "PURR/USDC" # Spot pair 27 | ORDER_SIZE = 3.0 # Size to meet minimum $10 USDC requirement 28 | PRICE_OFFSET_PCT = -50 # 50% below market for buy order (won't fill) 29 | 30 | 31 | def round_to_tick_size(price: float, tick_size: float) -> float: 32 | """Round price to the nearest valid tick size""" 33 | if tick_size <= 0: 34 | return price 35 | return round(price / tick_size) * tick_size 36 | 37 | 38 | # Test scenarios - limit orders only 39 | SCENARIOS = { 40 | # === LIMIT ORDERS === 41 | # Buy orders 42 | 1: { 43 | "name": "GTC Limit Buy", 44 | "order_type": HLOrderType({"limit": {"tif": "Gtc"}}), 45 | "reduce_only": False, 46 | "is_buy": True, 47 | }, 48 | 2: { 49 | "name": "IOC Limit Buy", 50 | "order_type": HLOrderType({"limit": {"tif": "Ioc"}}), 51 | "reduce_only": False, 52 | "is_buy": True, 53 | }, 54 | 3: { 55 | "name": "ALO Limit Buy", 56 | "order_type": HLOrderType({"limit": {"tif": "Alo"}}), 57 | "reduce_only": False, 58 | "is_buy": True, 59 | }, 60 | # Sell orders 61 | 4: { 62 | "name": "GTC Limit Sell", 63 | "order_type": HLOrderType({"limit": {"tif": "Gtc"}}), 64 | "reduce_only": False, 65 | "is_buy": False, 66 | }, 67 | 5: { 68 | "name": "IOC Limit Sell", 69 | "order_type": HLOrderType({"limit": {"tif": "Ioc"}}), 70 | "reduce_only": False, 71 | "is_buy": False, 72 | }, 73 | 6: { 74 | "name": "ALO Limit Sell", 75 | "order_type": HLOrderType({"limit": {"tif": "Alo"}}), 76 | "reduce_only": False, 77 | "is_buy": False, 78 | }, 79 | # Reduce-only orders (not applicable for spot, but included for completeness) 80 | # 7: {"name": "GTC Reduce-Only Buy", "order_type": HLOrderType({"limit": {"tif": "Gtc"}}), "reduce_only": True, "is_buy": True}, 81 | # 8: {"name": "GTC Reduce-Only Sell", "order_type": HLOrderType({"limit": {"tif": "Gtc"}}), "reduce_only": True, "is_buy": False}, 82 | # Time-limited orders 83 | 9: { 84 | "name": "GTC Expires-After (30s)", 85 | "order_type": HLOrderType({"limit": {"tif": "Gtc"}}), 86 | "reduce_only": False, 87 | "is_buy": True, 88 | "expires_after": 15, 89 | }, 90 | } 91 | 92 | 93 | async def place_limit_orders(): 94 | """Place limit orders for scenarios 1-9""" 95 | print("Running Limit Order Scenarios (1-9)") 96 | print("=" * 50) 97 | 98 | private_key = os.getenv("HYPERLIQUID_TESTNET_PRIVATE_KEY") 99 | if not private_key: 100 | print("❌ Missing HYPERLIQUID_TESTNET_PRIVATE_KEY in .env file") 101 | return 102 | 103 | try: 104 | wallet = Account.from_key(private_key) 105 | print(f"📱 Wallet: {wallet.address}") 106 | 107 | # Get spot metadata once 108 | info = Info(BASE_URL, skip_ws=True) 109 | spot_data = info.spot_meta_and_asset_ctxs() 110 | if len(spot_data) < 2: 111 | print("❌ Could not get spot metadata") 112 | return 113 | 114 | spot_meta = spot_data[0] 115 | asset_ctxs = spot_data[1] 116 | 117 | # Find PURR/USDC 118 | target_pair = None 119 | for pair in spot_meta.get("universe", []): 120 | if pair.get("name") == SYMBOL: 121 | target_pair = pair 122 | break 123 | 124 | if not target_pair: 125 | print(f"❌ Could not find {SYMBOL} in spot universe") 126 | return 127 | 128 | pair_index = target_pair.get("index") 129 | if pair_index >= len(asset_ctxs): 130 | print(f"❌ Asset index {pair_index} out of range") 131 | return 132 | 133 | # Get price decimals and calculate tick size 134 | price_decimals = target_pair.get("priceDecimals", 2) 135 | tick_size = 1 / (10**price_decimals) 136 | print(f"📏 Price decimals: {price_decimals}, Tick size: ${tick_size}") 137 | 138 | # Get current price 139 | ctx = asset_ctxs[pair_index] 140 | market_price = float(ctx.get("midPx", ctx.get("markPx", 0))) 141 | if market_price <= 0: 142 | print(f"❌ Could not get valid price for {SYMBOL}") 143 | return 144 | 145 | print(f"💰 Current {SYMBOL} price: ${market_price}") 146 | print() 147 | 148 | # Run each scenario 149 | for scenario_id, scenario in SCENARIOS.items(): 150 | print(f"🔹 Scenario {scenario_id}: {scenario['name']}") 151 | print("-" * 40) 152 | 153 | # Create fresh exchange instance for each scenario 154 | exchange = Exchange(wallet, BASE_URL) 155 | 156 | # Set expires_after if the scenario requires it 157 | if "expires_after" in scenario: 158 | expires_time = int(time.time() * 1000) + ( 159 | scenario["expires_after"] * 1000 160 | ) 161 | exchange.set_expires_after(expires_time) 162 | print(f"⏰ Order will expire in {scenario['expires_after']} seconds") 163 | 164 | is_buy = scenario["is_buy"] 165 | order_side = "BUY" if is_buy else "SELL" 166 | 167 | try: 168 | # Handle regular limit orders 169 | # For buy orders: below market price, for sell orders: above market price 170 | price_multiplier = ( 171 | (1 + PRICE_OFFSET_PCT / 100) 172 | if is_buy 173 | else (1 - PRICE_OFFSET_PCT / 100) 174 | ) 175 | order_price = market_price * price_multiplier 176 | order_price = round_to_tick_size(order_price, tick_size) 177 | print( 178 | f"📝 Placing {scenario['name']} {order_side} order: {ORDER_SIZE} {SYMBOL} @ ${order_price}" 179 | ) 180 | 181 | result = exchange.order( 182 | name=SYMBOL, 183 | is_buy=is_buy, 184 | sz=ORDER_SIZE, 185 | limit_px=order_price, 186 | order_type=scenario["order_type"], 187 | reduce_only=scenario["reduce_only"], 188 | ) 189 | 190 | if result and result.get("status") == "ok": 191 | response_data = result.get("response", {}).get("data", {}) 192 | statuses = response_data.get("statuses", []) 193 | 194 | if statuses: 195 | status_info = statuses[0] 196 | if "resting" in status_info: 197 | order_id = status_info["resting"]["oid"] 198 | print(f"✅ Order placed successfully! ID: {order_id}") 199 | elif "filled" in status_info: 200 | print("✅ Order filled immediately!") 201 | else: 202 | print(f"⚠️ Unexpected status: {status_info}") 203 | else: 204 | print(f"❌ Order failed: {result}") 205 | 206 | except Exception as e: 207 | print(f"❌ Scenario {scenario_id} failed: {e}") 208 | 209 | print() # Empty line between scenarios 210 | await asyncio.sleep(1) # Small delay between orders 211 | 212 | except Exception as e: 213 | print(f"❌ Error: {e}") 214 | 215 | 216 | if __name__ == "__main__": 217 | asyncio.run(place_limit_orders()) 218 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS.md 2 | 3 | This file provides guidance to AI agents when working with code in this repository. 4 | 5 | ## Development Workflow 6 | 7 | Before implementing anything, send me your plan of action for approval. 8 | Start implementing things only after my explicit approval. 9 | 10 | Follow test driven development. 11 | 12 | ## Package Management 13 | 14 | Use UV package manager for all commands in this repo: 15 | - `uv sync` - Install/sync dependencies 16 | - `uv add ` - Add new dependencies 17 | - `uv run ` - Run commands in the virtual environment 18 | - `uv run