updated fast orders

This commit is contained in:
2025-11-02 19:56:40 +01:00
parent 93363750ae
commit d650bb5fe2
6 changed files with 932 additions and 354 deletions

View File

@ -4,6 +4,7 @@ import os
import sys
import json
import time
# --- REVERTED: Removed math import ---
from datetime import datetime
import multiprocessing
@ -14,48 +15,44 @@ 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 a shared queue for strategy signals and executes trades.
This script is now a dedicated, event-driven consumer.
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, 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
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.")
# --- 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.")
sys.exit(1)
# --- 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.")
# --- 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.")
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.")
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 by scanning for environment variables.
"""
# ... (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():
@ -73,179 +70,122 @@ class TradeExecutor:
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: ...
def _load_managed_positions(self) -> dict:
"""Loads the state of which strategy manages which position."""
# Prefer shared in-memory state when available
# --- 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:
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.")
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 {}
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 from file.")
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."""
def _save_opened_positions(self):
"""Saves the current state of managed positions to a JSON file."""
try:
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)
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 save managed positions state: {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. Blocks and waits for a signal from the queue.
Main execution loop. Waits for an order and updates state on success.
"""
logging.info("Trade Executor started. Waiting for signals...")
logging.info("Trade Executor started. Waiting for orders...")
while True:
try:
trade_signal = self.trade_signal_queue.get()
if not trade_signal:
order = self.order_execution_queue.get()
if not order:
continue
logging.info(f"Received signal: {trade_signal}")
logging.info(f"Received order: {order}")
# 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
# 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
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
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 = order['agent']
action = order['action']
coin = order['coin']
is_buy = order['is_buy']
size = order['size']
limit_px = order.get('limit_px')
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.")
logging.error(f"Agent '{agent_name}' not found. Skipping order.")
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)
response = None
self._save_managed_positions()
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']
}
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)
# This script is no longer run directly, but is called by main_app.py