201 lines
10 KiB
Python
201 lines
10 KiB
Python
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.")
|
|
|