import logging import os import sys 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. """ def __init__(self, log_level: str, trade_signal_queue: multiprocessing.Queue, order_execution_queue: multiprocessing.Queue): # Note: Logging is set up by the run_position_manager function 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") # --- 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.") 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): """Helper function to put a standardized order onto the execution queue.""" 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. """ logging.info("Position Manager started. Waiting for signals...") while True: try: trade_signal = self.trade_signal_queue.get() if not trade_signal: continue 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() desired_signal = trade_signal['signal'] signal_price = trade_signal.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.") continue # --- Handle copy_trader's nested config --- 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) 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) size = params.get('size') if not size: logging.error(f"[{name}] Signal received with no 'size'. Skipping trade.") continue 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}] 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 } 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}) 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}) 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}) 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}) log_trade(strategy=name, coin=coin, action="CLOSE_SHORT", price=signal_price, size=size, signal=desired_signal) else: logging.warning(f"[{name}] Received unhandled signal '{desired_signal}'. No action taken.") except Exception as e: logging.error(f"An error occurred in the position manager loop: {e}", exc_info=True) time.sleep(1)