updated fast orders
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user