194 lines
8.9 KiB
Python
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)
|
|
|