import argparse import logging import os import sys import json import time # --- REVERTED: Removed math import --- from datetime import datetime import multiprocessing from eth_account import Account from hyperliquid.exchange import Exchange from hyperliquid.info import Info from hyperliquid.utils import constants from dotenv import load_dotenv from logging_utils import setup_logging load_dotenv() class TradeExecutor: """ Executes orders from a queue and, upon API success, updates the shared 'opened_positions.json' state file. It is the single source of truth for position state. """ def __init__(self, log_level: str, order_execution_queue: multiprocessing.Queue): # Note: Logging is set up by the run_trade_executor function self.order_execution_queue = order_execution_queue self.vault_address = os.environ.get("MAIN_WALLET_ADDRESS") if not self.vault_address: logging.error("MAIN_WALLET_ADDRESS not set.") sys.exit(1) self.info = Info(constants.MAINNET_API_URL, skip_ws=True) self.exchanges = self._load_agents() if not self.exchanges: logging.error("No trading agents found in .env file.") sys.exit(1) # --- REVERTED: Removed asset_meta loading --- # self.asset_meta = self._load_asset_metadata() # --- NEW: State management logic --- self.opened_positions_file = os.path.join("_data", "opened_positions.json") self.opened_positions = self._load_opened_positions() logging.info(f"Trade Executor started. Loaded {len(self.opened_positions)} positions.") def _load_agents(self) -> dict: # ... (omitted for brevity, this logic is correct and unchanged) ... exchanges = {} logging.info("Discovering agents from environment variables...") for env_var, private_key in os.environ.items(): agent_name = None if env_var == "AGENT_PRIVATE_KEY": agent_name = "default" elif env_var.endswith("_AGENT_PK"): agent_name = env_var.replace("_AGENT_PK", "").lower() if agent_name and private_key: try: agent_account = Account.from_key(private_key) exchanges[agent_name] = Exchange(agent_account, constants.MAINNET_API_URL, account_address=self.vault_address) logging.info(f"Initialized agent '{agent_name}' with address: {agent_account.address}") except Exception as e: logging.error(f"Failed to initialize agent '{agent_name}': {e}") return exchanges # --- REVERTED: Removed asset metadata loading --- # def _load_asset_metadata(self) -> dict: ... # --- NEW: Position state save/load methods --- def _load_opened_positions(self) -> dict: """Loads the state of currently managed positions from a JSON file.""" if not os.path.exists(self.opened_positions_file): return {} try: with open(self.opened_positions_file, 'r', encoding='utf-8') as f: return json.load(f) except (json.JSONDecodeError, IOError) as e: logging.error(f"Failed to read '{self.opened_positions_file}': {e}. Starting with empty state.", exc_info=True) return {} def _save_opened_positions(self): """Saves the current state of managed positions to a JSON file.""" try: with open(self.opened_positions_file, 'w', encoding='utf-8') as f: json.dump(self.opened_positions, f, indent=4) logging.debug(f"Successfully saved {len(self.opened_positions)} positions to '{self.opened_positions_file}'") except IOError as e: logging.error(f"Failed to write to '{self.opened_positions_file}': {e}", exc_info=True) # --- REVERTED: Removed tick rounding function --- # def _round_to_tick(self, price, tick_size): ... def run(self): """ Main execution loop. Waits for an order and updates state on success. """ logging.info("Trade Executor started. Waiting for orders...") while True: try: order = self.order_execution_queue.get() if not order: continue logging.info(f"Received order: {order}") agent_name = order['agent'] action = order['action'] coin = order['coin'] is_buy = order['is_buy'] size = order['size'] limit_px = order.get('limit_px') exchange_to_use = self.exchanges.get(agent_name) if not exchange_to_use: logging.error(f"Agent '{agent_name}' not found. Skipping order.") continue response = None if action == "market_open" or action == "market_close": reduce_only = (action == "market_close") log_action = "MARKET CLOSE" if reduce_only else "MARKET OPEN" logging.warning(f"ACTION: {log_action} {coin} {'BUY' if is_buy else 'SELL'} {size}") # --- REVERTED: Removed all slippage and rounding logic --- # The raw limit_px from the order is now used directly final_price = limit_px logging.info(f"[{agent_name}] Using raw price for {coin}: {final_price}") order_type = {"limit": {"tif": "Ioc"}} # --- REVERTED: Uses final_price (which is just limit_px) --- response = exchange_to_use.order(coin, is_buy, size, final_price, order_type, reduce_only=reduce_only) logging.info(f"Market order response: {response}") # --- NEW: STATE UPDATE ON SUCCESS --- if response.get("status") == "ok": response_data = response.get("response", {}).get("data", {}) if response_data and "statuses" in response_data: # Check if the order status contains an error if "error" not in response_data["statuses"][0]: position_key = order['position_key'] if action == "market_open": # Add to state self.opened_positions[position_key] = { "strategy": order['strategy'], "coin": coin, "side": "long" if is_buy else "short", "open_time_utc": order['open_time_utc'], "open_price": order['open_price'], "amount": order['amount'] } logging.info(f"Successfully opened position {position_key}. Saving state.") elif action == "market_close": # Remove from state if position_key in self.opened_positions: del self.opened_positions[position_key] logging.info(f"Successfully closed position {position_key}. Saving state.") else: logging.warning(f"Received close confirmation for {position_key}, but it was not in state.") self._save_opened_positions() # Save state to disk else: logging.error(f"API Error for {action}: {response_data['statuses'][0]['error']}") else: logging.error(f"Unexpected API response format: {response}") else: logging.error(f"API call failed, status: {response.get('status')}") elif action == "update_leverage": leverage = int(size) logging.warning(f"ACTION: UPDATE LEVERAGE {coin} to {leverage}x") response = exchange_to_use.update_leverage(leverage, coin) logging.info(f"Update leverage response: {response}") else: logging.warning(f"Received unknown action: {action}") except Exception as e: logging.error(f"An error occurred in the main executor loop: {e}", exc_info=True) time.sleep(1)