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 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