fixes, old way to handle strategies

This commit is contained in:
2025-10-27 21:54:33 +01:00
parent 541a71d2a6
commit 93363750ae
9 changed files with 1063 additions and 203 deletions

View File

@ -5,6 +5,7 @@ import sys
import json
import time
from datetime import datetime
import multiprocessing
from eth_account import Account
from hyperliquid.exchange import Exchange
@ -20,40 +21,41 @@ 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.
Monitors a shared queue for strategy signals and executes trades.
This script is now a dedicated, event-driven consumer.
"""
def __init__(self, log_level: str):
def __init__(self, log_level: str, trade_signal_queue: multiprocessing.Queue, shared_executor_status: dict = None):
setup_logging(log_level, 'TradeExecutor')
self.trade_signal_queue = trade_signal_queue
# Optional Manager.dict() to store live managed positions and other executor status
self.shared_executor_status = shared_executor_status
self.vault_address = os.environ.get("MAIN_WALLET_ADDRESS")
if not self.vault_address:
logging.error("MAIN_WALLET_ADDRESS not set.")
sys.exit(1)
# --- FIX: Raise an exception instead of sys.exit() ---
# This allows the main_app process manager to catch and log the error.
raise ValueError("MAIN_WALLET_ADDRESS not set in environment.")
# --- FIX: Corrected constant name from MAIN_NET_API_URL to MAINNET_API_URL ---
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)
# --- FIX: Raise an exception instead of sys.exit() ---
raise ValueError("No trading agents found in .env file. Check AGENT_PRIVATE_KEY or _AGENT_PK vars.")
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()
logging.info(f"TradeExecutor initialized. Agents available: {list(self.exchanges.keys())}")
def _load_agents(self) -> dict:
"""Discovers and initializes agents from environment variables."""
"""
Discovers and initializes agents by scanning for environment variables.
"""
exchanges = {}
logging.info("Discovering agents from environment variables...")
for env_var, private_key in os.environ.items():
@ -74,10 +76,20 @@ class TradeExecutor:
def _load_managed_positions(self) -> dict:
"""Loads the state of which strategy manages which position."""
# Prefer shared in-memory state when available
try:
if self.shared_executor_status is not None:
mgr = self.shared_executor_status.get('managed_positions') if isinstance(self.shared_executor_status, dict) else None
if mgr:
logging.info("Loading managed positions from shared executor status.")
return dict(mgr)
except Exception:
logging.debug("Unable to read managed positions from shared status. Falling back to file.")
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.")
logging.info("Loading existing managed positions state from file.")
return json.load(f)
except (IOError, json.JSONDecodeError):
logging.warning("Could not read managed positions file. Starting fresh.")
@ -86,115 +98,154 @@ class TradeExecutor:
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)
if self.shared_executor_status is not None:
try:
# store under a known key
self.shared_executor_status['managed_positions'] = dict(self.managed_positions)
except Exception:
# fallback: try direct assignment
self.shared_executor_status['managed_positions'] = self.managed_positions
else:
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...")
"""
Main execution loop. Blocks and waits for a signal from the queue.
"""
logging.info("Trade Executor started. Waiting for signals...")
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.")
trade_signal = self.trade_signal_queue.get()
if not trade_signal:
continue
logging.info(f"Received signal: {trade_signal}")
# Basic validation and debug information to help trace gaps
if 'config' not in trade_signal:
logging.error(f"Signal missing 'config' key. Ignoring: {trade_signal}")
continue
if 'strategy_name' not in trade_signal:
logging.error(f"Signal missing 'strategy_name' key. Ignoring: {trade_signal}")
continue
# Special command handling
if isinstance(trade_signal, dict) and trade_signal.get('_cmd') == 'CLOSE_ALL':
target_agent = trade_signal.get('agent')
logging.warning(f"Received CLOSE_ALL command for agent: {target_agent}")
if not target_agent:
logging.error("CLOSE_ALL command missing 'agent' field. Ignoring.")
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)
# Iterate managed positions and close those opened by the target agent
to_close = [s for s, v in self.managed_positions.items() if v.get('agent') == target_agent]
if not to_close:
logging.info(f"No managed positions found for agent '{target_agent}'.")
continue
elif current_position['side'] == 'short':
if not all([size, leverage_long]):
logging.error(f"[{name}] 'size' or 'leverage_long' not defined. Skipping.")
continue
for sname in to_close:
pos = self.managed_positions.get(sname)
if not pos:
continue
coin = pos.get('coin')
side = pos.get('side')
size = pos.get('size')
# Determine is_buy to neutralize the position
is_buy = True if side == 'short' else False
logging.warning(f"[CLOSE_ALL] Closing {side} position for strategy {sname}, coin {coin}, size {size}")
try:
# Use the agent's exchange if available
exch = self.exchanges.get(target_agent)
if exch:
exch.market_open(coin, is_buy, size, None, 0.01)
else:
logging.error(f"Exchange object for agent '{target_agent}' not found. Skipping live close for {sname}.")
except Exception as e:
logging.error(f"Error closing position for {sname}: {e}")
# remove from managed positions regardless to avoid stuck state
try:
del self.managed_positions[sname]
except KeyError:
pass
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()
logging.info(f"CLOSE_ALL for agent '{target_agent}' completed.")
continue
name = trade_signal['strategy_name']
config = trade_signal['config']
params = config.get('parameters', {})
coin = trade_signal['coin']
desired_signal = trade_signal['signal']
status = trade_signal
size = params.get('size')
if size is None:
logging.error(f"[{name}] No 'size' in parameters: {params}. Skipping.")
continue
leverage_long = int(params.get('leverage_long', 2))
leverage_short = int(params.get('leverage_short', 2))
current_position = self.managed_positions.get(name)
agent_name = (config.get("agent") or "default").lower()
exchange_to_use = self.exchanges.get(agent_name)
if not exchange_to_use:
logging.error(f"[{name}] Agent '{agent_name}' not found. Available agents: {list(self.exchanges.keys())}. Skipping trade.")
continue
# --- State Machine Logic (now runs instantly on signal) ---
if desired_signal == "BUY" or desired_signal == "INIT_BUY":
if not current_position:
logging.warning(f"[{name}] ACTION: Setting leverage to {leverage_long}x and opening LONG for {coin}.")
exchange_to_use.update_leverage(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':
logging.warning(f"[{name}] ACTION: Closing SHORT and opening LONG for {coin} with {leverage_long}x leverage.")
exchange_to_use.update_leverage(leverage_long, coin)
# 1. Close the short by buying back (this is a market_open, but is_buy=True)
exchange_to_use.market_open(coin, True, current_position['size'], None, 0.01)
log_trade(strategy=name, coin=coin, action="CLOSE_SHORT", price=status.get('signal_price', 0), size=current_position['size'], signal=desired_signal)
# 2. Open the new long
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 desired_signal == "SELL" or desired_signal == "INIT_SELL":
if not current_position:
logging.warning(f"[{name}] ACTION: Setting leverage to {leverage_short}x and opening SHORT for {coin}.")
exchange_to_use.update_leverage(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':
logging.warning(f"[{name}] ACTION: Closing LONG and opening SHORT for {coin} with {leverage_short}x leverage.")
exchange_to_use.update_leverage(leverage_short, coin)
# 1. Close the long by selling
exchange_to_use.market_open(coin, False, current_position['size'], None, 0.01)
log_trade(strategy=name, coin=coin, action="CLOSE_LONG", price=status.get('signal_price', 0), size=current_position['size'], signal=desired_signal)
# 2. Open the new short
exchange_to_use.market_open(coin, False, size, None, 0.01)
self.managed_positions[name] = {"coin": coin, "side": "short", "size": size}
# --- FIX: Corrected typo from 'signal.desired_signal' to 'signal=desired_signal' ---
log_trade(strategy=name, coin=coin, action="OPEN_SHORT", 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}")
logging.error(f"An error occurred in the main executor loop: {e}", exc_info=True)
time.sleep(1)
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.")
# This script is no longer run directly, but is called by main_app.py