169 lines
8.3 KiB
Python
169 lines
8.3 KiB
Python
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)
|
|
|