size taken from monitored wallet
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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":
|
||||
|
||||
Reference in New Issue
Block a user