import argparse import logging import os import sys import json import time from datetime import datetime 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 from trade_log import log_trade # Load environment variables from a .env file load_dotenv() class TradeExecutor: """ Monitors strategy signals and executes trades using a multi-agent, multi-strategy position management system. Each strategy's position is tracked independently. """ def __init__(self, log_level: str): setup_logging(log_level, 'TradeExecutor') 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) strategy_config_path = os.path.join("_data", "strategies.json") try: with open(strategy_config_path, 'r') as f: self.strategy_configs = {name: config for name, config in json.load(f).items() if config.get("enabled")} logging.info(f"Loaded {len(self.strategy_configs)} enabled strategies.") except (FileNotFoundError, json.JSONDecodeError) as e: logging.error(f"Could not load strategies from '{strategy_config_path}': {e}") sys.exit(1) self.status_file_path = os.path.join("_logs", "trade_executor_status.json") self.managed_positions_path = os.path.join("_data", "executor_managed_positions.json") self.managed_positions = self._load_managed_positions() def _load_agents(self) -> dict: """Discovers and initializes agents from environment variables.""" 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 def _load_managed_positions(self) -> dict: """Loads the state of which strategy manages which position.""" if os.path.exists(self.managed_positions_path): try: with open(self.managed_positions_path, 'r') as f: logging.info("Loading existing managed positions state.") return json.load(f) except (IOError, json.JSONDecodeError): logging.warning("Could not read managed positions file. Starting fresh.") return {} def _save_managed_positions(self): """Saves the current state of managed positions.""" try: with open(self.managed_positions_path, 'w') as f: json.dump(self.managed_positions, f, indent=4) except IOError as e: logging.error(f"Failed to save managed positions state: {e}") def _save_executor_status(self, perpetuals_state, spot_state, all_market_contexts): """Saves the current balances and open positions to a live status file.""" # This function is correct and does not need changes. pass def run(self): """The main execution loop with advanced position management.""" logging.info("Starting Trade Executor loop...") while True: try: perpetuals_state = self.info.user_state(self.vault_address) open_positions_api = {pos['position'].get('coin'): pos['position'] for pos in perpetuals_state.get('assetPositions', []) if float(pos.get('position', {}).get('szi', 0)) != 0} for name, config in self.strategy_configs.items(): coin = config['parameters'].get('coin') size = config['parameters'].get('size') # --- ADDED: Load leverage parameters from config --- leverage_long = config['parameters'].get('leverage_long') leverage_short = config['parameters'].get('leverage_short') status_file = os.path.join("_data", f"strategy_status_{name}.json") if not os.path.exists(status_file): continue with open(status_file, 'r') as f: status = json.load(f) desired_signal = status.get('current_signal') current_position = self.managed_positions.get(name) agent_name = config.get("agent", "default").lower() exchange_to_use = self.exchanges.get(agent_name) if not exchange_to_use: logging.error(f"[{name}] Agent '{agent_name}' not found. Skipping trade.") continue # --- State Machine Logic with Configurable Leverage --- if desired_signal == "BUY": if not current_position: if not all([size, leverage_long]): logging.error(f"[{name}] 'size' or 'leverage_long' not defined. Skipping.") continue logging.warning(f"[{name}] ACTION: Open LONG for {coin} with {leverage_long}x leverage.") exchange_to_use.update_leverage(int(leverage_long), coin) exchange_to_use.market_open(coin, True, size, None, 0.01) self.managed_positions[name] = {"coin": coin, "side": "long", "size": size} log_trade(strategy=name, coin=coin, action="OPEN_LONG", price=status.get('signal_price', 0), size=size, signal=desired_signal) elif current_position['side'] == 'short': if not all([size, leverage_long]): logging.error(f"[{name}] 'size' or 'leverage_long' not defined. Skipping.") continue logging.warning(f"[{name}] ACTION: Close SHORT and open LONG for {coin} with {leverage_long}x leverage.") exchange_to_use.update_leverage(int(leverage_long), coin) exchange_to_use.market_open(coin, True, current_position['size'] + size, None, 0.01) self.managed_positions[name] = {"coin": coin, "side": "long", "size": size} log_trade(strategy=name, coin=coin, action="CLOSE_SHORT_&_REVERSE", price=status.get('signal_price', 0), size=size, signal=desired_signal) elif desired_signal == "SELL": if not current_position: if not all([size, leverage_short]): logging.error(f"[{name}] 'size' or 'leverage_short' not defined. Skipping.") continue logging.warning(f"[{name}] ACTION: Open SHORT for {coin} with {leverage_short}x leverage.") exchange_to_use.update_leverage(int(leverage_short), coin) exchange_to_use.market_open(coin, False, size, None, 0.01) self.managed_positions[name] = {"coin": coin, "side": "short", "size": size} log_trade(strategy=name, coin=coin, action="OPEN_SHORT", price=status.get('signal_price', 0), size=size, signal=desired_signal) elif current_position['side'] == 'long': if not all([size, leverage_short]): logging.error(f"[{name}] 'size' or 'leverage_short' not defined. Skipping.") continue logging.warning(f"[{name}] ACTION: Close LONG and open SHORT for {coin} with {leverage_short}x leverage.") exchange_to_use.update_leverage(int(leverage_short), coin) exchange_to_use.market_open(coin, False, current_position['size'] + size, None, 0.01) self.managed_positions[name] = {"coin": coin, "side": "short", "size": size} log_trade(strategy=name, coin=coin, action="CLOSE_LONG_&_REVERSE", price=status.get('signal_price', 0), size=size, signal=desired_signal) elif desired_signal == "FLAT": if current_position: logging.warning(f"[{name}] ACTION: Close {current_position['side']} position for {coin}.") is_buy = current_position['side'] == 'short' exchange_to_use.market_open(coin, is_buy, current_position['size'], None, 0.01) del self.managed_positions[name] log_trade(strategy=name, coin=coin, action=f"CLOSE_{current_position['side'].upper()}", price=status.get('signal_price', 0), size=current_position['size'], signal=desired_signal) self._save_managed_positions() except Exception as e: logging.error(f"An error occurred in the main executor loop: {e}") time.sleep(15) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run the Trade Executor.") parser.add_argument("--log-level", default="normal", choices=['off', 'normal', 'debug']) args = parser.parse_args() executor = TradeExecutor(log_level=args.log_level) try: executor.run() except KeyboardInterrupt: logging.info("Trade Executor stopped.")