From 5f9109c3a9b894aa194d54f492f7adaae582a913 Mon Sep 17 00:00:00 2001 From: DiTus Date: Sun, 2 Nov 2025 22:38:31 +0100 Subject: [PATCH] size taken from monitored wallet --- position_manager.py | 176 +++++++++--------- strategies/copy_trader_strategy.py | 280 +++++++++++++++++------------ trade_executor.py | 6 +- 3 files changed, 261 insertions(+), 201 deletions(-) diff --git a/position_manager.py b/position_manager.py index 8533564..c355c29 100644 --- a/position_manager.py +++ b/position_manager.py @@ -5,16 +5,17 @@ import json import time import multiprocessing import numpy as np # Import numpy to handle np.float64 -from datetime import datetime, timezone from logging_utils import setup_logging from trade_log import log_trade class PositionManager: """ - Listens for strategy signals, READS the current position state, - and sends explicit execution orders to the TradeExecutor. - It does NOT write to the position state file. + (Stateless) Listens for EXPLICIT signals (e.g., "OPEN_LONG") from all + strategies and converts them into specific execution orders + (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): @@ -23,39 +24,34 @@ class PositionManager: self.trade_signal_queue = trade_signal_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 --- - 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.") + logging.info("Position Manager (Stateless) started.") + # --- REMOVED: _load_managed_positions method --- + # --- REMOVED: _save_managed_positions method --- + # --- REMOVED: All tick/rounding/meta logic --- - 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 {} - - # --- REMOVED: _save_opened_positions method --- - # (The TradeExecutor is now responsible for saving) - - def send_order(self, order_data: 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): """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}") self.order_execution_queue.put(order_data) def run(self): """ 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...") while True: @@ -66,103 +62,109 @@ class PositionManager: 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'] config = trade_signal['config'] params = config['parameters'] coin = trade_signal['coin'].upper() + + # --- NEW: The signal is now the explicit action --- 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): signal_price = float(signal_price) 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 - # --- 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: - # ... (omitted for brevity, this logic is correct and unchanged) ... - matching_coin_key = next((k for k in params['coins_to_copy'] if k.upper() == coin), None) + logging.info(f"[{name}] Detected 'coins_to_copy'. Entering copy_trader logic...") + 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: - coin_config = params['coins_to_copy'][matching_coin_key] - params['size'] = coin_config.get('size') - params['leverage_long'] = coin_config.get('leverage_long', 2) - params['leverage_short'] = coin_config.get('leverage_short', 2) + coin_specific_config = params['coins_to_copy'][matching_coin_key] + else: + coin_specific_config = {} + + # --- 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') - if not size: - logging.error(f"[{name}] Signal received with no 'size'. Skipping trade.") + # --- FIX: Read the size from the ROOT of the trade signal --- + size = trade_signal.get('size') + if not size or size <= 0: + logging.error(f"[{name}] Signal received with no 'size' or invalid size ({size}). Skipping trade.") continue + # --- END FIX --- leverage_long = int(params.get('leverage_long', 2)) leverage_short = int(params.get('leverage_short', 2)) + agent_name = (config.get("agent") or "default").lower() - # --- NEW: Stateful decision making --- - position_key = f"{name}_{coin}" - current_position = self.opened_positions.get(position_key) + logging.info(f"[{name}] Agent set to: {agent_name}") - logging.info(f"[{name}] Processing signal '{desired_signal}'. Current state: {current_position['side'] if current_position else 'FLAT'}") - - order_data = { - "agent": agent_name, - "coin": coin, - "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 - } + # --- REMOVED: current_position check --- + + # --- Use pure signal_price directly for the limit_px --- + limit_px = signal_price + logging.info(f"[{name}] Using pure signal price for limit_px: {limit_px}") + # --- NEW: Stateless Signal-to-Order Conversion --- + if desired_signal == "OPEN_LONG": - if current_position: - logging.info(f"[{name}] Ignoring OPEN_LONG signal, already in a position.") - continue - - 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}) + logging.warning(f"[{name}] ACTION: Opening LONG for {coin}.") + # --- REMOVED: Leverage update signal --- + self.send_order(agent_name, "market_open", coin, True, size, limit_px=limit_px) log_trade(strategy=name, coin=coin, action="OPEN_LONG", price=signal_price, size=size, signal=desired_signal) - + elif desired_signal == "OPEN_SHORT": - if current_position: - logging.info(f"[{name}] Ignoring OPEN_SHORT signal, already in a position.") - continue - - 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}) + logging.warning(f"[{name}] ACTION: Opening SHORT for {coin}.") + # --- REMOVED: Leverage update signal --- + self.send_order(agent_name, "market_open", coin, False, size, limit_px=limit_px) log_trade(strategy=name, coin=coin, action="OPEN_SHORT", price=signal_price, size=size, signal=desired_signal) elif desired_signal == "CLOSE_LONG": - if not current_position or current_position['side'] != 'long': - logging.info(f"[{name}] Ignoring CLOSE_LONG signal, not in a long position.") - continue - - logging.warning(f"[{name}] ACTION: Closing LONG position.") - self.send_order({**order_data, "action": "market_close", "is_buy": False, "size": size}) + logging.warning(f"[{name}] ACTION: Closing LONG position for {coin}.") + # A "market_close" for a LONG is a SELL order + self.send_order(agent_name, "market_close", coin, False, size, limit_px=limit_px) log_trade(strategy=name, coin=coin, action="CLOSE_LONG", price=signal_price, size=size, signal=desired_signal) elif desired_signal == "CLOSE_SHORT": - if not current_position or current_position['side'] != 'short': - logging.info(f"[{name}] Ignoring CLOSE_SHORT signal, not in a short position.") - continue - - logging.warning(f"[{name}] ACTION: Closing SHORT position.") - self.send_order({**order_data, "action": "market_close", "is_buy": True, "size": size}) + logging.warning(f"[{name}] ACTION: Closing SHORT position for {coin}.") + # A "market_close" for a SHORT is a BUY order + self.send_order(agent_name, "market_close", coin, True, size, limit_px=limit_px) 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: - 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: logging.error(f"An error occurred in the position manager loop: {e}", exc_info=True) time.sleep(1) +# This script is no longer run directly, but is called by main_app.py + diff --git a/strategies/copy_trader_strategy.py b/strategies/copy_trader_strategy.py index e2aaac4..95443bb 100644 --- a/strategies/copy_trader_strategy.py +++ b/strategies/copy_trader_strategy.py @@ -11,12 +11,16 @@ from strategies.base_strategy import BaseStrategy class CopyTraderStrategy(BaseStrategy): """ An event-driven strategy that monitors a target wallet address and - copies its trades for a specific set of allowed coins, using - per-coin size and leverage settings. + copies its trades for a specific set of allowed coins. - 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): + # --- 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) 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 - # --- MODIFIED: Load and manage its own position state --- - self.position_state_file = os.path.join("_data", f"strategy_state_{self.strategy_name}.json") - self.current_positions = self._load_position_state() + # --- REMOVED: All local state management --- + # self.position_state_file = ... + # self.current_positions = ... # --- MODIFIED: Check if shared_status is None before using it --- if self.shared_status is None: @@ -48,26 +52,9 @@ class CopyTraderStrategy(BaseStrategy): 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"Loaded positions: {self.current_positions}") - def _load_position_state(self) -> dict: - """Loads the strategy's current open positions from a file.""" - 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}") + # --- REMOVED: _load_position_state --- + # --- REMOVED: _save_position_state --- def calculate_signals(self, df): # 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): """Helper to send a formatted signal to the PositionManager.""" config = { + # --- MODIFIED: Ensure agent is read from params --- "agent": self.params.get("agent"), "parameters": trade_params } + # --- MODIFIED: Use self.trade_signal_queue (which is the queue passed in) --- self.trade_signal_queue.put({ "strategy_name": self.strategy_name, "signal": signal, # e.g., "OPEN_LONG", "CLOSE_SHORT" "coin": coin, "signal_price": price, "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}") @@ -96,23 +85,45 @@ class CopyTraderStrategy(BaseStrategy): every time the monitored address has an event. """ try: + # --- NEW: Add logging to see ALL messages --- + logging.debug(f"Received WebSocket message: {message}") + channel = message.get("channel") if channel not in ("user", "userFills", "userEvents"): + # --- NEW: Added debug logging --- + logging.debug(f"Ignoring message from unhandled channel: {channel}") return data = message.get("data") if not data: + # --- NEW: Added debug logging --- + logging.debug("Message received with no 'data' field. Ignoring.") return - fills = data.get("fills", []) - if not fills: - return - + # --- NEW: Check for user address FIRST --- user_address = data.get("user", "").lower() - - if user_address != self.target_address: + if not user_address: + logging.debug("Received message with 'data' but no 'user'. Ignoring.") 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}") for fill in fills: @@ -125,71 +136,108 @@ class CopyTraderStrategy(BaseStrategy): coin = fill.get('coin').upper() if coin in self.allowed_coins: - side = fill.get('side') price = float(fill.get('px')) - fill_size = float(fill.get('sz')) - # Get our strategy's configured trade size for this coin - coin_config = self.coins_to_copy.get(coin) - if not coin_config or not coin_config.get("size"): - logging.warning(f"No trade size specified for {coin}. Ignoring fill.") + # --- MODIFIED: Use the target's fill size --- + fill_size = float(fill.get('sz')) # Target's size + + if fill_size == 0: + logging.warning(f"Ignoring fill with size 0.") 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 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 - current_local_pos = self.current_positions.get(coin) - current_local_side = current_local_pos.get("side") if current_local_pos else None + # --- REMOVED: All stateful logic (current_local_pos, etc.) --- + # --- MODIFIED: Expanded logic to handle flip directions --- signal_sent = False - if side == "B": # Target bought - 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.") + dashboard_signal = "" + 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: # Update dashboard status - self.current_signal = f"{side} @ {coin}" + self.current_signal = dashboard_signal # Show the action self.signal_price = price 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_position_state() # For our internal tracking logging.info(f"Source trade logged: {json.dumps(fill)}") + else: + logging.info(f"[{coin}] Ignoring unhandled fill direction: {fill_direction}") else: logging.info(f"Ignoring fill for unmonitored coin: {coin}") @@ -203,9 +251,12 @@ class CopyTraderStrategy(BaseStrategy): try: logging.info("Connecting to Hyperliquid WebSocket...") self.info = Info(constants.MAINNET_API_URL, skip_ws=False) + + # --- MODIFIED: Reverted to 'userFills' as requested --- subscription = {"type": "userFills", "user": self.target_address} self.info.subscribe(subscription, self.on_fill_message) logging.info(f"Subscribed to 'userFills' for target address: {self.target_address}") + return True except Exception as 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 persistent WebSocket connection and runs a watchdog to ensure it stays connected. - - It also catches KeyboardInterrupt to gracefully shut down positions. """ try: if not self._connect_and_subscribe(): @@ -226,6 +275,40 @@ class CopyTraderStrategy(BaseStrategy): time.sleep(60) 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 self._save_status() @@ -253,36 +336,9 @@ class CopyTraderStrategy(BaseStrategy): except Exception as e: logging.error(f"An error occurred in the watchdog loop: {e}", exc_info=True) - except KeyboardInterrupt: # --- THIS IS THE GRACEFUL SHUTDOWN LOGIC --- - logging.warning(f"Shutdown signal received. Closing all open positions for '{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 KeyboardInterrupt: + # --- MODIFIED: No positions to close, just exit --- + logging.warning(f"Shutdown signal received. Exiting strategy '{self.strategy_name}'.") except Exception as e: logging.error(f"An unhandled error occurred in run_event_loop: {e}", exc_info=True) diff --git a/trade_executor.py b/trade_executor.py index 2de81c7..44c6e13 100644 --- a/trade_executor.py +++ b/trade_executor.py @@ -142,7 +142,7 @@ class TradeExecutor: # --- NEW: STATE UPDATE ON SUCCESS --- 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: # Check if the order status contains an error if "error" not in response_data["statuses"][0]: @@ -155,7 +155,9 @@ class TradeExecutor: "side": "long" if is_buy else "short", "open_time_utc": order['open_time_utc'], "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.") elif action == "market_close":