Files
hyper/trade_executor.py

194 lines
8.9 KiB
Python

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'],
# --- MODIFIED: Read leverage from the order ---
"leverage": order.get('leverage')
}
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)