size taken from monitored wallet

This commit is contained in:
2025-11-02 22:38:31 +01:00
parent d650bb5fe2
commit 5f9109c3a9
3 changed files with 261 additions and 201 deletions

View File

@ -5,16 +5,17 @@ import json
import time import time
import multiprocessing import multiprocessing
import numpy as np # Import numpy to handle np.float64 import numpy as np # Import numpy to handle np.float64
from datetime import datetime, timezone
from logging_utils import setup_logging from logging_utils import setup_logging
from trade_log import log_trade from trade_log import log_trade
class PositionManager: class PositionManager:
""" """
Listens for strategy signals, READS the current position state, (Stateless) Listens for EXPLICIT signals (e.g., "OPEN_LONG") from all
and sends explicit execution orders to the TradeExecutor. strategies and converts them into specific execution orders
It does NOT write to the position state file. (e.g., "market_open") for the TradeExecutor.
It holds NO position state.
""" """
def __init__(self, log_level: str, trade_signal_queue: multiprocessing.Queue, order_execution_queue: multiprocessing.Queue): def __init__(self, log_level: str, trade_signal_queue: multiprocessing.Queue, order_execution_queue: multiprocessing.Queue):
@ -23,39 +24,34 @@ class PositionManager:
self.trade_signal_queue = trade_signal_queue self.trade_signal_queue = trade_signal_queue
self.order_execution_queue = order_execution_queue self.order_execution_queue = order_execution_queue
self.opened_positions_file = os.path.join("_data", "opened_positions.json") # --- REMOVED: All state management ---
# --- MODIFIED: Load state, but will not save it --- logging.info("Position Manager (Stateless) started.")
self.opened_positions = self._load_opened_positions()
if self.opened_positions:
logging.info(f"Position Manager started. Loaded {len(self.opened_positions)} open positions (read-only).")
else:
logging.info("Position Manager started. No initial positions found.")
# --- REMOVED: _load_managed_positions method ---
# --- REMOVED: _save_managed_positions method ---
# --- REMOVED: All tick/rounding/meta logic ---
def _load_opened_positions(self) -> dict: def send_order(self, agent: str, action: str, coin: str, is_buy: bool, size: float, reduce_only: bool = False, limit_px=None, sl_px=None, tp_px=None):
"""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 {}
# --- REMOVED: _save_opened_positions method ---
# (The TradeExecutor is now responsible for saving)
def send_order(self, order_data: dict):
"""Helper function to put a standardized order onto the execution queue.""" """Helper function to put a standardized order onto the execution queue."""
order_data = {
"agent": agent,
"action": action,
"coin": coin,
"is_buy": is_buy,
"size": size,
"reduce_only": reduce_only,
"limit_px": limit_px,
"sl_px": sl_px,
"tp_px": tp_px,
}
logging.info(f"Sending order to executor: {order_data}") logging.info(f"Sending order to executor: {order_data}")
self.order_execution_queue.put(order_data) self.order_execution_queue.put(order_data)
def run(self): def run(self):
""" """
Main execution loop. Blocks and waits for a signal from the queue. Main execution loop. Blocks and waits for a signal from the queue.
Converts strategy signals into execution orders based on current state. Converts explicit strategy signals into execution orders.
""" """
logging.info("Position Manager started. Waiting for signals...") logging.info("Position Manager started. Waiting for signals...")
while True: while True:
@ -66,103 +62,109 @@ class PositionManager:
logging.info(f"Received signal: {trade_signal}") logging.info(f"Received signal: {trade_signal}")
# --- NEW: Reload the position state on every signal ---
# This ensures we have the most up-to-date state from the Executor
self.opened_positions = self._load_opened_positions()
name = trade_signal['strategy_name'] name = trade_signal['strategy_name']
config = trade_signal['config'] config = trade_signal['config']
params = config['parameters'] params = config['parameters']
coin = trade_signal['coin'].upper() coin = trade_signal['coin'].upper()
# --- NEW: The signal is now the explicit action ---
desired_signal = trade_signal['signal'] desired_signal = trade_signal['signal']
signal_price = trade_signal.get('signal_price') status = trade_signal
signal_price = status.get('signal_price')
if isinstance(signal_price, np.float64): if isinstance(signal_price, np.float64):
signal_price = float(signal_price) signal_price = float(signal_price)
if not signal_price or signal_price <= 0: if not signal_price or signal_price <= 0:
logging.warning(f"[{name}] Signal received with invalid price ({signal_price}). Skipping.") logging.warning(f"[{name}] Signal received with invalid or missing price ({signal_price}). Skipping.")
continue continue
# --- Handle copy_trader's nested config --- # --- This logic is still needed for copy_trader's nested config ---
# --- But ONLY for finding leverage, not size ---
if 'coins_to_copy' in params: if 'coins_to_copy' in params:
# ... (omitted for brevity, this logic is correct and unchanged) ... logging.info(f"[{name}] Detected 'coins_to_copy'. Entering copy_trader logic...")
matching_coin_key = next((k for k in params['coins_to_copy'] if k.upper() == coin), None) matching_coin_key = None
for key in params['coins_to_copy'].keys():
if key.upper() == coin:
matching_coin_key = key
break
if matching_coin_key: if matching_coin_key:
coin_config = params['coins_to_copy'][matching_coin_key] coin_specific_config = params['coins_to_copy'][matching_coin_key]
params['size'] = coin_config.get('size') else:
params['leverage_long'] = coin_config.get('leverage_long', 2) coin_specific_config = {}
params['leverage_short'] = coin_config.get('leverage_short', 2)
# --- REMOVED: size = coin_specific_config.get('size') ---
params['leverage_long'] = coin_specific_config.get('leverage_long', 2)
params['leverage_short'] = coin_specific_config.get('leverage_short', 2)
size = params.get('size') # --- FIX: Read the size from the ROOT of the trade signal ---
if not size: size = trade_signal.get('size')
logging.error(f"[{name}] Signal received with no 'size'. Skipping trade.") if not size or size <= 0:
logging.error(f"[{name}] Signal received with no 'size' or invalid size ({size}). Skipping trade.")
continue continue
# --- END FIX ---
leverage_long = int(params.get('leverage_long', 2)) leverage_long = int(params.get('leverage_long', 2))
leverage_short = int(params.get('leverage_short', 2)) leverage_short = int(params.get('leverage_short', 2))
agent_name = (config.get("agent") or "default").lower() agent_name = (config.get("agent") or "default").lower()
# --- NEW: Stateful decision making --- logging.info(f"[{name}] Agent set to: {agent_name}")
position_key = f"{name}_{coin}"
current_position = self.opened_positions.get(position_key)
logging.info(f"[{name}] Processing signal '{desired_signal}'. Current state: {current_position['side'] if current_position else 'FLAT'}") # --- REMOVED: current_position check ---
order_data = { # --- Use pure signal_price directly for the limit_px ---
"agent": agent_name, limit_px = signal_price
"coin": coin, logging.info(f"[{name}] Using pure signal price for limit_px: {limit_px}")
"limit_px": signal_price,
# --- NEW: Pass all context to the executor ---
"strategy": name,
"position_key": position_key,
"open_price": signal_price,
"open_time_utc": datetime.now(timezone.utc).isoformat(),
"amount": size
}
# --- NEW: Stateless Signal-to-Order Conversion ---
if desired_signal == "OPEN_LONG": if desired_signal == "OPEN_LONG":
if current_position: logging.warning(f"[{name}] ACTION: Opening LONG for {coin}.")
logging.info(f"[{name}] Ignoring OPEN_LONG signal, already in a position.") # --- REMOVED: Leverage update signal ---
continue self.send_order(agent_name, "market_open", coin, True, size, limit_px=limit_px)
logging.warning(f"[{name}] ACTION: Setting leverage to {leverage_long}x and opening LONG.")
self.send_order({**order_data, "action": "update_leverage", "is_buy": True, "size": leverage_long})
self.send_order({**order_data, "action": "market_open", "is_buy": True, "size": size})
log_trade(strategy=name, coin=coin, action="OPEN_LONG", price=signal_price, size=size, signal=desired_signal) log_trade(strategy=name, coin=coin, action="OPEN_LONG", price=signal_price, size=size, signal=desired_signal)
elif desired_signal == "OPEN_SHORT": elif desired_signal == "OPEN_SHORT":
if current_position: logging.warning(f"[{name}] ACTION: Opening SHORT for {coin}.")
logging.info(f"[{name}] Ignoring OPEN_SHORT signal, already in a position.") # --- REMOVED: Leverage update signal ---
continue self.send_order(agent_name, "market_open", coin, False, size, limit_px=limit_px)
logging.warning(f"[{name}] ACTION: Setting leverage to {leverage_short}x and opening SHORT.")
self.send_order({**order_data, "action": "update_leverage", "is_buy": False, "size": leverage_short})
self.send_order({**order_data, "action": "market_open", "is_buy": False, "size": size})
log_trade(strategy=name, coin=coin, action="OPEN_SHORT", price=signal_price, size=size, signal=desired_signal) log_trade(strategy=name, coin=coin, action="OPEN_SHORT", price=signal_price, size=size, signal=desired_signal)
elif desired_signal == "CLOSE_LONG": elif desired_signal == "CLOSE_LONG":
if not current_position or current_position['side'] != 'long': logging.warning(f"[{name}] ACTION: Closing LONG position for {coin}.")
logging.info(f"[{name}] Ignoring CLOSE_LONG signal, not in a long position.") # A "market_close" for a LONG is a SELL order
continue self.send_order(agent_name, "market_close", coin, False, size, limit_px=limit_px)
logging.warning(f"[{name}] ACTION: Closing LONG position.")
self.send_order({**order_data, "action": "market_close", "is_buy": False, "size": size})
log_trade(strategy=name, coin=coin, action="CLOSE_LONG", price=signal_price, size=size, signal=desired_signal) log_trade(strategy=name, coin=coin, action="CLOSE_LONG", price=signal_price, size=size, signal=desired_signal)
elif desired_signal == "CLOSE_SHORT": elif desired_signal == "CLOSE_SHORT":
if not current_position or current_position['side'] != 'short': logging.warning(f"[{name}] ACTION: Closing SHORT position for {coin}.")
logging.info(f"[{name}] Ignoring CLOSE_SHORT signal, not in a short position.") # A "market_close" for a SHORT is a BUY order
continue self.send_order(agent_name, "market_close", coin, True, size, limit_px=limit_px)
logging.warning(f"[{name}] ACTION: Closing SHORT position.")
self.send_order({**order_data, "action": "market_close", "is_buy": True, "size": size})
log_trade(strategy=name, coin=coin, action="CLOSE_SHORT", price=signal_price, size=size, signal=desired_signal) log_trade(strategy=name, coin=coin, action="CLOSE_SHORT", price=signal_price, size=size, signal=desired_signal)
# --- NEW: Handle leverage update signals ---
elif desired_signal == "UPDATE_LEVERAGE_LONG":
logging.warning(f"[{name}] ACTION: Updating LONG leverage for {coin} to {size}x")
# 'size' field holds the leverage value for this signal
self.send_order(agent_name, "update_leverage", coin, True, size)
elif desired_signal == "UPDATE_LEVERAGE_SHORT":
logging.warning(f"[{name}] ACTION: Updating SHORT leverage for {coin} to {size}x")
# 'size' field holds the leverage value for this signal
self.send_order(agent_name, "update_leverage", coin, False, size)
else: else:
logging.warning(f"[{name}] Received unhandled signal '{desired_signal}'. No action taken.") logging.warning(f"[{name}] Received unknown signal '{desired_signal}'. No action taken.")
# --- REMOVED: _save_managed_positions() ---
except Exception as e: except Exception as e:
logging.error(f"An error occurred in the position manager loop: {e}", exc_info=True) logging.error(f"An error occurred in the position manager loop: {e}", exc_info=True)
time.sleep(1) time.sleep(1)
# This script is no longer run directly, but is called by main_app.py

View File

@ -11,12 +11,16 @@ from strategies.base_strategy import BaseStrategy
class CopyTraderStrategy(BaseStrategy): class CopyTraderStrategy(BaseStrategy):
""" """
An event-driven strategy that monitors a target wallet address and An event-driven strategy that monitors a target wallet address and
copies its trades for a specific set of allowed coins, using copies its trades for a specific set of allowed coins.
per-coin size and leverage settings.
This strategy is STATEFUL and tracks its own positions. This strategy is STATELESS. It translates a target's fill direction
(e.g., "Open Long") directly into an explicit signal
(e.g., "OPEN_LONG") for the PositionManager.
""" """
def __init__(self, strategy_name: str, params: dict, trade_signal_queue, shared_status: dict = None): def __init__(self, strategy_name: str, params: dict, trade_signal_queue, shared_status: dict = None):
# --- MODIFIED: Pass the correct queue to the parent ---
# The event-driven copy trader should send orders to the order_execution_queue
# We will assume the queue passed in is the correct one (as setup in main_app.py)
super().__init__(strategy_name, params, trade_signal_queue, shared_status) super().__init__(strategy_name, params, trade_signal_queue, shared_status)
self.target_address = self.params.get("target_address", "").lower() self.target_address = self.params.get("target_address", "").lower()
@ -33,9 +37,9 @@ class CopyTraderStrategy(BaseStrategy):
self.info = None # Will be initialized in the run loop self.info = None # Will be initialized in the run loop
# --- MODIFIED: Load and manage its own position state --- # --- REMOVED: All local state management ---
self.position_state_file = os.path.join("_data", f"strategy_state_{self.strategy_name}.json") # self.position_state_file = ...
self.current_positions = self._load_position_state() # self.current_positions = ...
# --- MODIFIED: Check if shared_status is None before using it --- # --- MODIFIED: Check if shared_status is None before using it ---
if self.shared_status is None: if self.shared_status is None:
@ -48,26 +52,9 @@ class CopyTraderStrategy(BaseStrategy):
self.start_time_utc = datetime.now(timezone.utc) self.start_time_utc = datetime.now(timezone.utc)
logging.info(f"Strategy initialized. Ignoring all trades before {self.start_time_utc.isoformat()}") logging.info(f"Strategy initialized. Ignoring all trades before {self.start_time_utc.isoformat()}")
logging.info(f"Loaded positions: {self.current_positions}")
def _load_position_state(self) -> dict: # --- REMOVED: _load_position_state ---
"""Loads the strategy's current open positions from a file.""" # --- REMOVED: _save_position_state ---
if os.path.exists(self.position_state_file):
try:
with open(self.position_state_file, 'r') as f:
logging.info(f"Loading existing position state from {self.position_state_file}")
return json.load(f)
except (IOError, json.JSONDecodeError):
logging.warning(f"Could not read position state file {self.position_state_file}. Starting fresh.")
return {} # { "ETH": {"side": "long", "size": 0.01, "entry": 3000}, ... }
def _save_position_state(self):
"""Saves the strategy's current open positions to a file."""
try:
with open(self.position_state_file, 'w') as f:
json.dump(self.current_positions, f, indent=4)
except IOError as e:
logging.error(f"Failed to save position state: {e}")
def calculate_signals(self, df): def calculate_signals(self, df):
# This strategy is event-driven, so it does not use polling-based signal calculation. # This strategy is event-driven, so it does not use polling-based signal calculation.
@ -76,17 +63,19 @@ class CopyTraderStrategy(BaseStrategy):
def send_explicit_signal(self, signal: str, coin: str, price: float, trade_params: dict, size: float): def send_explicit_signal(self, signal: str, coin: str, price: float, trade_params: dict, size: float):
"""Helper to send a formatted signal to the PositionManager.""" """Helper to send a formatted signal to the PositionManager."""
config = { config = {
# --- MODIFIED: Ensure agent is read from params ---
"agent": self.params.get("agent"), "agent": self.params.get("agent"),
"parameters": trade_params "parameters": trade_params
} }
# --- MODIFIED: Use self.trade_signal_queue (which is the queue passed in) ---
self.trade_signal_queue.put({ self.trade_signal_queue.put({
"strategy_name": self.strategy_name, "strategy_name": self.strategy_name,
"signal": signal, # e.g., "OPEN_LONG", "CLOSE_SHORT" "signal": signal, # e.g., "OPEN_LONG", "CLOSE_SHORT"
"coin": coin, "coin": coin,
"signal_price": price, "signal_price": price,
"config": config, "config": config,
"size": size # Explicitly pass size "size": size # Explicitly pass size (or leverage for leverage updates)
}) })
logging.info(f"Explicit signal SENT: {signal} {coin} @ {price}, Size: {size}") logging.info(f"Explicit signal SENT: {signal} {coin} @ {price}, Size: {size}")
@ -96,23 +85,45 @@ class CopyTraderStrategy(BaseStrategy):
every time the monitored address has an event. every time the monitored address has an event.
""" """
try: try:
# --- NEW: Add logging to see ALL messages ---
logging.debug(f"Received WebSocket message: {message}")
channel = message.get("channel") channel = message.get("channel")
if channel not in ("user", "userFills", "userEvents"): if channel not in ("user", "userFills", "userEvents"):
# --- NEW: Added debug logging ---
logging.debug(f"Ignoring message from unhandled channel: {channel}")
return return
data = message.get("data") data = message.get("data")
if not data: if not data:
# --- NEW: Added debug logging ---
logging.debug("Message received with no 'data' field. Ignoring.")
return return
fills = data.get("fills", []) # --- NEW: Check for user address FIRST ---
if not fills:
return
user_address = data.get("user", "").lower() user_address = data.get("user", "").lower()
if not user_address:
if user_address != self.target_address: logging.debug("Received message with 'data' but no 'user'. Ignoring.")
return return
# --- MODIFIED: Check for 'fills' vs. other event types ---
# This check is still valid for userFills
if "fills" not in data or not data.get("fills"):
# This is a userEvent, but not a fill (e.g., order placement, cancel, withdrawal)
event_type = data.get("type") # e.g., 'order', 'cancel', 'withdrawal'
if event_type:
logging.debug(f"Received non-fill user event: '{event_type}'. Ignoring.")
else:
logging.debug(f"Received 'data' message with no 'fills'. Ignoring.")
return
# --- This line is now safe to run ---
if user_address != self.target_address:
# This shouldn't happen if the subscription is correct, but good to check
logging.warning(f"Received fill for wrong user: {user_address}")
return
fills = data.get("fills")
logging.debug(f"Received {len(fills)} fill(s) for user {user_address}") logging.debug(f"Received {len(fills)} fill(s) for user {user_address}")
for fill in fills: for fill in fills:
@ -125,71 +136,108 @@ class CopyTraderStrategy(BaseStrategy):
coin = fill.get('coin').upper() coin = fill.get('coin').upper()
if coin in self.allowed_coins: if coin in self.allowed_coins:
side = fill.get('side')
price = float(fill.get('px')) price = float(fill.get('px'))
fill_size = float(fill.get('sz'))
# Get our strategy's configured trade size for this coin # --- MODIFIED: Use the target's fill size ---
coin_config = self.coins_to_copy.get(coin) fill_size = float(fill.get('sz')) # Target's size
if not coin_config or not coin_config.get("size"):
logging.warning(f"No trade size specified for {coin}. Ignoring fill.") if fill_size == 0:
logging.warning(f"Ignoring fill with size 0.")
continue continue
strategy_trade_size = coin_config.get("size")
# --- NEW: Get the fill direction ---
# "dir": "Open Long", "Close Long", "Open Short", "Close Short"
fill_direction = fill.get("dir")
# --- NEW: Get startPosition to calculate flip sizes ---
start_pos_size = float(fill.get('startPosition', 0.0))
if not fill_direction:
logging.warning(f"Fill message missing 'dir'. Ignoring fill: {fill}")
continue
# Get our strategy's configured leverage for this coin
coin_config = self.coins_to_copy.get(coin)
# --- REMOVED: Check for coin_config.get("size") ---
# --- REMOVED: strategy_trade_size = coin_config.get("size") ---
# Prepare config for the signal # Prepare config for the signal
trade_params = self.params.copy() trade_params = self.params.copy()
trade_params.update(coin_config) if coin_config:
trade_params.update(coin_config)
# Get our current position state for this coin # --- REMOVED: All stateful logic (current_local_pos, etc.) ---
current_local_pos = self.current_positions.get(coin)
current_local_side = current_local_pos.get("side") if current_local_pos else None
# --- MODIFIED: Expanded logic to handle flip directions ---
signal_sent = False signal_sent = False
if side == "B": # Target bought dashboard_signal = ""
if current_local_side == "short":
# Flip: Close short, then open long
logging.warning(f"[{coin}] Target BOUGHT, we are SHORT. Flipping to LONG.")
self.send_explicit_signal("CLOSE_SHORT", coin, price, trade_params, current_local_pos.get("size"))
self.send_explicit_signal("OPEN_LONG", coin, price, trade_params, strategy_trade_size)
self.current_positions[coin] = {"side": "long", "size": strategy_trade_size, "entry": price}
signal_sent = True
elif current_local_side is None:
# New: Open long
logging.warning(f"[{coin}] Target BOUGHT, we are FLAT. Opening LONG.")
self.send_explicit_signal("OPEN_LONG", coin, price, trade_params, strategy_trade_size)
self.current_positions[coin] = {"side": "long", "size": strategy_trade_size, "entry": price}
signal_sent = True
else: # We are already long
logging.info(f"[{coin}] Target BOUGHT, we are already LONG. Ignoring.")
elif side == "A": # Target sold
if current_local_side == "long":
# Flip: Close long, then open short
logging.warning(f"[{coin}] Target SOLD, we are LONG. Flipping to SHORT.")
self.send_explicit_signal("CLOSE_LONG", coin, price, trade_params, current_local_pos.get("size"))
self.send_explicit_signal("OPEN_SHORT", coin, price, trade_params, strategy_trade_size)
self.current_positions[coin] = {"side": "short", "size": strategy_trade_size, "entry": price}
signal_sent = True
elif current_local_side is None:
# New: Open short
logging.warning(f"[{coin}] Target SOLD, we are FLAT. Opening SHORT.")
self.send_explicit_signal("OPEN_SHORT", coin, price, trade_params, strategy_trade_size)
self.current_positions[coin] = {"side": "short", "size": strategy_trade_size, "entry": price}
signal_sent = True
else: # We are already short
logging.info(f"[{coin}] Target SOLD, we are already SHORT. Ignoring.")
if fill_direction == "Open Long":
logging.warning(f"[{coin}] Target action: {fill_direction}. Sending signal: OPEN_LONG")
self.send_explicit_signal("OPEN_LONG", coin, price, trade_params, fill_size)
signal_sent = True
dashboard_signal = "OPEN_LONG"
elif fill_direction == "Close Long":
logging.warning(f"[{coin}] Target action: {fill_direction}. Sending signal: CLOSE_LONG")
self.send_explicit_signal("CLOSE_LONG", coin, price, trade_params, fill_size)
signal_sent = True
dashboard_signal = "CLOSE_LONG"
elif fill_direction == "Open Short":
logging.warning(f"[{coin}] Target action: {fill_direction}. Sending signal: OPEN_SHORT")
self.send_explicit_signal("OPEN_SHORT", coin, price, trade_params, fill_size)
signal_sent = True
dashboard_signal = "OPEN_SHORT"
elif fill_direction == "Close Short":
logging.warning(f"[{coin}] Target action: {fill_direction}. Sending signal: CLOSE_SHORT")
self.send_explicit_signal("CLOSE_SHORT", coin, price, trade_params, fill_size)
signal_sent = True
dashboard_signal = "CLOSE_SHORT"
elif fill_direction == "Short > Long":
logging.warning(f"[{coin}] Target action: {fill_direction}. Sending CLOSE_SHORT then OPEN_LONG.")
close_size = abs(start_pos_size)
open_size = fill_size - close_size
if close_size > 0:
self.send_explicit_signal("CLOSE_SHORT", coin, price, trade_params, close_size)
if open_size > 0:
self.send_explicit_signal("OPEN_LONG", coin, price, trade_params, open_size)
signal_sent = True
dashboard_signal = "FLIP_TO_LONG"
elif fill_direction == "Long > Short":
logging.warning(f"[{coin}] Target action: {fill_direction}. Sending CLOSE_LONG then OPEN_SHORT.")
close_size = abs(start_pos_size)
open_size = fill_size - close_size
if close_size > 0:
self.send_explicit_signal("CLOSE_LONG", coin, price, trade_params, close_size)
if open_size > 0:
self.send_explicit_signal("OPEN_SHORT", coin, price, trade_params, open_size)
signal_sent = True
dashboard_signal = "FLIP_TO_SHORT"
if signal_sent: if signal_sent:
# Update dashboard status # Update dashboard status
self.current_signal = f"{side} @ {coin}" self.current_signal = dashboard_signal # Show the action
self.signal_price = price self.signal_price = price
self.last_signal_change_utc = trade_time.isoformat() self.last_signal_change_utc = trade_time.isoformat()
# --- MODIFIED: Save BOTH status files --- self.coin = coin # Update coin for dashboard
self.size = fill_size # Update size for dashboard
self._save_status() # For dashboard self._save_status() # For dashboard
self._save_position_state() # For our internal tracking
logging.info(f"Source trade logged: {json.dumps(fill)}") logging.info(f"Source trade logged: {json.dumps(fill)}")
else:
logging.info(f"[{coin}] Ignoring unhandled fill direction: {fill_direction}")
else: else:
logging.info(f"Ignoring fill for unmonitored coin: {coin}") logging.info(f"Ignoring fill for unmonitored coin: {coin}")
@ -203,9 +251,12 @@ class CopyTraderStrategy(BaseStrategy):
try: try:
logging.info("Connecting to Hyperliquid WebSocket...") logging.info("Connecting to Hyperliquid WebSocket...")
self.info = Info(constants.MAINNET_API_URL, skip_ws=False) self.info = Info(constants.MAINNET_API_URL, skip_ws=False)
# --- MODIFIED: Reverted to 'userFills' as requested ---
subscription = {"type": "userFills", "user": self.target_address} subscription = {"type": "userFills", "user": self.target_address}
self.info.subscribe(subscription, self.on_fill_message) self.info.subscribe(subscription, self.on_fill_message)
logging.info(f"Subscribed to 'userFills' for target address: {self.target_address}") logging.info(f"Subscribed to 'userFills' for target address: {self.target_address}")
return True return True
except Exception as e: except Exception as e:
logging.error(f"Failed to connect or subscribe: {e}") logging.error(f"Failed to connect or subscribe: {e}")
@ -217,8 +268,6 @@ class CopyTraderStrategy(BaseStrategy):
This method overrides the default polling loop. It establishes a This method overrides the default polling loop. It establishes a
persistent WebSocket connection and runs a watchdog to ensure persistent WebSocket connection and runs a watchdog to ensure
it stays connected. it stays connected.
It also catches KeyboardInterrupt to gracefully shut down positions.
""" """
try: try:
if not self._connect_and_subscribe(): if not self._connect_and_subscribe():
@ -226,6 +275,40 @@ class CopyTraderStrategy(BaseStrategy):
time.sleep(60) time.sleep(60)
return return
# --- MODIFIED: Add a small delay to ensure Info object is ready for REST calls ---
logging.info("Connection established. Waiting 2 seconds for Info client to be ready...")
time.sleep(2)
# --- END MODIFICATION ---
# --- NEW: Set initial leverage for all monitored coins ---
logging.info("Setting initial leverage for all monitored coins...")
try:
all_mids = self.info.all_mids()
for coin_key, coin_config in self.coins_to_copy.items():
coin = coin_key.upper()
# Use a failsafe price of 1.0 if coin not in mids (e.g., new listing)
current_price = float(all_mids.get(coin, 1.0))
leverage_long = coin_config.get('leverage_long', 2)
leverage_short = coin_config.get('leverage_short', 2)
# Prepare config for the signal
trade_params = self.params.copy()
trade_params.update(coin_config)
# Send LONG leverage update
# The 'size' param is used to pass the leverage value for this signal type
self.send_explicit_signal("UPDATE_LEVERAGE_LONG", coin, current_price, trade_params, leverage_long)
# Send SHORT leverage update
self.send_explicit_signal("UPDATE_LEVERAGE_SHORT", coin, current_price, trade_params, leverage_short)
logging.info(f"Sent initial leverage signals for {coin} (Long: {leverage_long}x, Short: {leverage_short}x)")
except Exception as e:
logging.error(f"Failed to set initial leverage: {e}", exc_info=True)
# --- END NEW LEVERAGE LOGIC ---
# Save the initial "WAIT" status # Save the initial "WAIT" status
self._save_status() self._save_status()
@ -253,36 +336,9 @@ class CopyTraderStrategy(BaseStrategy):
except Exception as e: except Exception as e:
logging.error(f"An error occurred in the watchdog loop: {e}", exc_info=True) logging.error(f"An error occurred in the watchdog loop: {e}", exc_info=True)
except KeyboardInterrupt: # --- THIS IS THE GRACEFUL SHUTDOWN LOGIC --- except KeyboardInterrupt:
logging.warning(f"Shutdown signal received. Closing all open positions for '{self.strategy_name}'...") # --- MODIFIED: No positions to close, just exit ---
logging.warning(f"Shutdown signal received. Exiting strategy '{self.strategy_name}'.")
# Use a copy of the items to avoid runtime modification errors
for coin, position in list(self.current_positions.items()):
current_side = position.get("side")
trade_size = position.get("size")
if not current_side or not trade_size:
continue
# Find the config for this coin
coin_config = self.coins_to_copy.get(coin.upper(), {})
trade_params = self.params.copy()
trade_params.update(coin_config)
# Use the last entry price as a placeholder for the market close order
price = position.get("entry", 1) # Use 1 as a failsafe
if current_side == "long":
logging.warning(f"Sending CLOSE_LONG for {coin}, {price}, {trade_size}...")
#self.send_explicit_signal("CLOSE_LONG", coin, price, trade_params, trade_size)
#del self.current_positions[coin] # Assume it will close
elif current_side == "short":
logging.warning(f"Sending CLOSE_SHORT for {coin}, {price}, {trade_size} ...")
#self.send_explicit_signal("CLOSE_SHORT", coin, price, trade_params, trade_size)
#del self.current_positions[coin] # Assume it will close
self._save_position_state() # Save the new empty state
logging.info("All closing signals sent. Exiting strategy.")
except Exception as e: except Exception as e:
logging.error(f"An unhandled error occurred in run_event_loop: {e}", exc_info=True) logging.error(f"An unhandled error occurred in run_event_loop: {e}", exc_info=True)

View File

@ -142,7 +142,7 @@ class TradeExecutor:
# --- NEW: STATE UPDATE ON SUCCESS --- # --- NEW: STATE UPDATE ON SUCCESS ---
if response.get("status") == "ok": if response.get("status") == "ok":
response_data = response.get("response", {}).get("data", {}) response_data = response.get("response", {},).get("data", {})
if response_data and "statuses" in response_data: if response_data and "statuses" in response_data:
# Check if the order status contains an error # Check if the order status contains an error
if "error" not in response_data["statuses"][0]: if "error" not in response_data["statuses"][0]:
@ -155,7 +155,9 @@ class TradeExecutor:
"side": "long" if is_buy else "short", "side": "long" if is_buy else "short",
"open_time_utc": order['open_time_utc'], "open_time_utc": order['open_time_utc'],
"open_price": order['open_price'], "open_price": order['open_price'],
"amount": order['amount'] "amount": order['amount'],
# --- MODIFIED: Read leverage from the order ---
"leverage": order.get('leverage')
} }
logging.info(f"Successfully opened position {position_key}. Saving state.") logging.info(f"Successfully opened position {position_key}. Saving state.")
elif action == "market_close": elif action == "market_close":