|
@@ -42,6 +42,7 @@ class CopyTrade:
|
|
|
original_trade_hash: str
|
|
|
target_trader_address: str
|
|
|
timestamp: int
|
|
|
+ entry_price: Optional[float] = None # Target trader's entry price for better copying
|
|
|
|
|
|
|
|
|
class CopyTradingMonitor:
|
|
@@ -246,6 +247,12 @@ class CopyTradingMonitor:
|
|
|
except Exception as e:
|
|
|
self.logger.error(f"Error updating our positions: {e}")
|
|
|
|
|
|
+ # Monitor for stop losses and take profits from target trader
|
|
|
+ try:
|
|
|
+ await self._monitor_stop_losses_and_take_profits()
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"Error monitoring stop losses and take profits: {e}")
|
|
|
+
|
|
|
# Update last check timestamp (async version)
|
|
|
await self.state_manager.update_last_check_async()
|
|
|
|
|
@@ -343,7 +350,8 @@ class CopyTradingMonitor:
|
|
|
leverage=new_pos.leverage,
|
|
|
original_trade_hash=trade_id,
|
|
|
target_trader_address=self.target_address,
|
|
|
- timestamp=new_pos.timestamp
|
|
|
+ timestamp=new_pos.timestamp,
|
|
|
+ entry_price=new_pos.entry_price
|
|
|
))
|
|
|
|
|
|
# Check for position reductions
|
|
@@ -361,7 +369,8 @@ class CopyTradingMonitor:
|
|
|
leverage=new_pos.leverage,
|
|
|
original_trade_hash=trade_id,
|
|
|
target_trader_address=self.target_address,
|
|
|
- timestamp=new_pos.timestamp
|
|
|
+ timestamp=new_pos.timestamp,
|
|
|
+ entry_price=new_pos.entry_price
|
|
|
))
|
|
|
self.logger.info(f"📉 Detected position decrease: {action} {size_decrease} {coin}")
|
|
|
|
|
@@ -385,7 +394,8 @@ class CopyTradingMonitor:
|
|
|
leverage=tracked_pos['leverage'],
|
|
|
original_trade_hash=trade_id,
|
|
|
target_trader_address=self.target_address,
|
|
|
- timestamp=int(time.time() * 1000)
|
|
|
+ timestamp=int(time.time() * 1000),
|
|
|
+ entry_price=tracked_pos.get('entry_price')
|
|
|
))
|
|
|
self.logger.info(f"❌ Detected position closure: {action} {tracked_pos['size']} {coin}")
|
|
|
|
|
@@ -439,7 +449,12 @@ class CopyTradingMonitor:
|
|
|
await asyncio.sleep(self.execution_delay)
|
|
|
|
|
|
# Execute the trade
|
|
|
- success = await self._execute_hyperliquid_trade(trade, position_calc, leverage)
|
|
|
+ if trade.entry_price and 'open' in trade.action:
|
|
|
+ # Use limit order at target's entry price for opening positions
|
|
|
+ success = await self._execute_limit_order_at_target_price(trade, position_calc, leverage, trade.entry_price)
|
|
|
+ else:
|
|
|
+ # Use market order for closes, reduces, or when entry_price not available
|
|
|
+ success = await self._execute_hyperliquid_trade(trade, position_calc, leverage)
|
|
|
|
|
|
# Mark trade as copied (whether successful or not to avoid retrying)
|
|
|
await self.state_manager.add_copied_trade_async(trade.original_trade_hash)
|
|
@@ -628,8 +643,277 @@ class CopyTradingMonitor:
|
|
|
self.logger.error(f"Error getting target account balance: {e}")
|
|
|
return 0.0
|
|
|
|
|
|
+ async def _execute_limit_order_at_target_price(self, trade: CopyTrade, position_calc: Dict[str, float], leverage: float, target_entry_price: float) -> bool:
|
|
|
+ """Execute trade using limit order at target trader's entry price for better accuracy"""
|
|
|
+ try:
|
|
|
+ # Determine if this is a buy or sell order
|
|
|
+ is_buy = 'long' in trade.action or ('close' in trade.action and 'short' in trade.action)
|
|
|
+ side = 'buy' if is_buy else 'sell'
|
|
|
+
|
|
|
+ # Extract values from position calculation
|
|
|
+ token_amount = position_calc['token_amount']
|
|
|
+ margin_used = position_calc['margin_to_use']
|
|
|
+ position_value = position_calc['position_value']
|
|
|
+
|
|
|
+ symbol = f"{trade.coin}/USDC:USDC"
|
|
|
+
|
|
|
+ self.logger.info(f"📍 Executing {trade.action} for {trade.coin} with LIMIT order:")
|
|
|
+ self.logger.info(f" 📊 Side: {side}")
|
|
|
+ self.logger.info(f" 🎯 Target Entry Price: ${target_entry_price:.4f}")
|
|
|
+ self.logger.info(f" 🪙 Token Amount: {token_amount:.6f} {trade.coin}")
|
|
|
+ self.logger.info(f" 💳 Margin: ${margin_used:.2f}")
|
|
|
+ self.logger.info(f" 🏦 Position Value: ${position_value:.2f}")
|
|
|
+ self.logger.info(f" ⚖️ Leverage: {leverage}x")
|
|
|
+
|
|
|
+ # Set leverage before placing the order
|
|
|
+ self.logger.info(f"⚖️ Setting leverage to {leverage}x for {trade.coin}")
|
|
|
+ leverage_result, leverage_error = await asyncio.to_thread(
|
|
|
+ self.client.set_leverage,
|
|
|
+ leverage=int(leverage),
|
|
|
+ symbol=symbol
|
|
|
+ )
|
|
|
+ if leverage_error:
|
|
|
+ self.logger.warning(f"⚠️ Failed to set leverage to {leverage}x for {trade.coin}: {leverage_error}")
|
|
|
+ else:
|
|
|
+ self.logger.info(f"✅ Successfully set leverage to {leverage}x for {trade.coin}")
|
|
|
+
|
|
|
+ # Place limit order at target trader's entry price
|
|
|
+ result, error = await asyncio.to_thread(
|
|
|
+ self.client.place_limit_order,
|
|
|
+ symbol=symbol,
|
|
|
+ side=side,
|
|
|
+ amount=token_amount,
|
|
|
+ price=target_entry_price
|
|
|
+ )
|
|
|
+
|
|
|
+ if error:
|
|
|
+ self.logger.error(f"❌ Limit order failed: {error}")
|
|
|
+ # Fallback to market order if limit order fails
|
|
|
+ self.logger.info("🔄 Falling back to market order...")
|
|
|
+ return await self._execute_hyperliquid_trade(trade, position_calc, leverage)
|
|
|
+
|
|
|
+ if result:
|
|
|
+ self.logger.info(f"✅ Successfully placed LIMIT order at target price: ${target_entry_price:.4f}")
|
|
|
+ self.logger.info(f" 🪙 {token_amount:.6f} {trade.coin} (${position_value:.2f} value, ${margin_used:.2f} margin)")
|
|
|
+ return True
|
|
|
+ else:
|
|
|
+ self.logger.error(f"❌ Failed to place limit order - no result returned")
|
|
|
+ return False
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"❌ Error executing limit order at target price: {e}")
|
|
|
+ # Fallback to market order
|
|
|
+ self.logger.info("🔄 Falling back to market order due to error...")
|
|
|
+ return await self._execute_hyperliquid_trade(trade, position_calc, leverage)
|
|
|
+
|
|
|
+ async def _get_target_trader_open_orders(self) -> Optional[List[Dict[str, Any]]]:
|
|
|
+ """Attempt to get target trader's open orders (may not be available for external addresses)"""
|
|
|
+ try:
|
|
|
+ # Note: This likely won't work for external addresses due to API limitations
|
|
|
+ # But we can try anyway for completeness
|
|
|
+ timeout = aiohttp.ClientTimeout(total=5.0)
|
|
|
+ async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
|
+ payload = {
|
|
|
+ "type": "openOrders",
|
|
|
+ "user": self.target_address
|
|
|
+ }
|
|
|
+
|
|
|
+ async with session.post(self.info_url, json=payload) as response:
|
|
|
+ if response.status == 200:
|
|
|
+ data = await response.json()
|
|
|
+ if data and isinstance(data, list):
|
|
|
+ self.logger.info(f"📋 Found {len(data)} open orders for target trader")
|
|
|
+ return data
|
|
|
+ else:
|
|
|
+ self.logger.debug("📋 No open orders found for target trader")
|
|
|
+ return []
|
|
|
+ else:
|
|
|
+ self.logger.debug(f"❌ Failed to fetch target trader open orders: {response.status}")
|
|
|
+ return None
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.debug(f"❌ Error fetching target trader open orders: {e}")
|
|
|
+ return None
|
|
|
+
|
|
|
+ async def _monitor_stop_losses_and_take_profits(self):
|
|
|
+ """Monitor for stop loss and take profit orders from target trader"""
|
|
|
+ try:
|
|
|
+ # Get target trader's open orders (if available)
|
|
|
+ open_orders = await self._get_target_trader_open_orders()
|
|
|
+
|
|
|
+ if not open_orders:
|
|
|
+ return
|
|
|
+
|
|
|
+ # Look for stop loss and take profit orders
|
|
|
+ for order in open_orders:
|
|
|
+ order_type = order.get('type', '').lower()
|
|
|
+ symbol = order.get('coin', '')
|
|
|
+ trigger_price = order.get('triggerPx')
|
|
|
+ order_side = order.get('side', '').lower()
|
|
|
+ order_size = order.get('sz', 0)
|
|
|
+
|
|
|
+ if not symbol or not trigger_price:
|
|
|
+ continue
|
|
|
+
|
|
|
+ if 'stop' in order_type or 'sl' in order_type:
|
|
|
+ # Found a stop loss order
|
|
|
+ self.logger.info(f"🛑 Target trader has stop loss for {symbol} at ${trigger_price}")
|
|
|
+ await self._copy_stop_loss_order(symbol, trigger_price, order_side, order_size)
|
|
|
+
|
|
|
+ elif 'take' in order_type or 'tp' in order_type:
|
|
|
+ # Found a take profit order
|
|
|
+ self.logger.info(f"🎯 Target trader has take profit for {symbol} at ${trigger_price}")
|
|
|
+ await self._copy_take_profit_order(symbol, trigger_price, order_side, order_size)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"❌ Error monitoring stop losses and take profits: {e}")
|
|
|
+
|
|
|
+ async def _copy_stop_loss_order(self, symbol: str, trigger_price: float, order_side: str, target_order_size: float):
|
|
|
+ """Copy a stop loss order from the target trader"""
|
|
|
+ try:
|
|
|
+ # Convert symbol format if needed (e.g., ETH -> ETH/USDC:USDC)
|
|
|
+ trading_symbol = f"{symbol}/USDC:USDC" if "/" not in symbol else symbol
|
|
|
+
|
|
|
+ # Check if we have a position in this symbol
|
|
|
+ our_positions = await asyncio.to_thread(self.client.get_positions, symbol=trading_symbol)
|
|
|
+ if not our_positions:
|
|
|
+ self.logger.debug(f"⚠️ No position found for {symbol} - skipping stop loss copy")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Find our position
|
|
|
+ our_position = None
|
|
|
+ for pos in our_positions:
|
|
|
+ if pos.get('symbol') == trading_symbol and float(pos.get('contracts', 0)) != 0:
|
|
|
+ our_position = pos
|
|
|
+ break
|
|
|
+
|
|
|
+ if not our_position:
|
|
|
+ self.logger.debug(f"⚠️ No open position found for {symbol} - skipping stop loss copy")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Calculate our order size based on position ratio
|
|
|
+ our_position_size = abs(float(our_position.get('contracts', 0)))
|
|
|
+ our_position_side = 'long' if float(our_position.get('contracts', 0)) > 0 else 'short'
|
|
|
+
|
|
|
+ # Determine stop loss side (opposite of position)
|
|
|
+ stop_side = 'sell' if our_position_side == 'long' else 'buy'
|
|
|
+
|
|
|
+ # Check if we already have a stop loss for this position
|
|
|
+ existing_orders = await asyncio.to_thread(self.client.get_open_orders, trading_symbol)
|
|
|
+ if existing_orders:
|
|
|
+ for existing_order in existing_orders:
|
|
|
+ if ('stop' in existing_order.get('type', '').lower() or
|
|
|
+ 'sl' in existing_order.get('type', '').lower()):
|
|
|
+ self.logger.debug(f"📋 Stop loss already exists for {symbol} - skipping")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Place the stop loss order
|
|
|
+ self.logger.info(f"🛑 Placing stop loss for {symbol}: {stop_side} {our_position_size:.6f} @ ${trigger_price}")
|
|
|
+
|
|
|
+ result, error = await asyncio.to_thread(
|
|
|
+ self.client.place_stop_loss_order,
|
|
|
+ symbol=trading_symbol,
|
|
|
+ side=stop_side,
|
|
|
+ amount=our_position_size,
|
|
|
+ stop_price_arg=float(trigger_price)
|
|
|
+ )
|
|
|
+
|
|
|
+ if error:
|
|
|
+ self.logger.error(f"❌ Failed to place stop loss for {symbol}: {error}")
|
|
|
+ else:
|
|
|
+ self.logger.info(f"✅ Successfully copied stop loss for {symbol}")
|
|
|
+
|
|
|
+ # Send notification
|
|
|
+ if self.notifications_enabled:
|
|
|
+ await self.notification_manager.send_generic_notification(
|
|
|
+ f"🛑 Stop Loss Copied\n"
|
|
|
+ f"Symbol: {symbol}\n"
|
|
|
+ f"Trigger: ${trigger_price}\n"
|
|
|
+ f"Size: {our_position_size:.6f}\n"
|
|
|
+ f"Side: {stop_side}"
|
|
|
+ )
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"❌ Error copying stop loss for {symbol}: {e}")
|
|
|
+
|
|
|
+ async def _copy_take_profit_order(self, symbol: str, trigger_price: float, order_side: str, target_order_size: float):
|
|
|
+ """Copy a take profit order from the target trader"""
|
|
|
+ try:
|
|
|
+ # Convert symbol format if needed (e.g., ETH -> ETH/USDC:USDC)
|
|
|
+ trading_symbol = f"{symbol}/USDC:USDC" if "/" not in symbol else symbol
|
|
|
+
|
|
|
+ # Check if we have a position in this symbol
|
|
|
+ our_positions = await asyncio.to_thread(self.client.get_positions, symbol=trading_symbol)
|
|
|
+ if not our_positions:
|
|
|
+ self.logger.debug(f"⚠️ No position found for {symbol} - skipping take profit copy")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Find our position
|
|
|
+ our_position = None
|
|
|
+ for pos in our_positions:
|
|
|
+ if pos.get('symbol') == trading_symbol and float(pos.get('contracts', 0)) != 0:
|
|
|
+ our_position = pos
|
|
|
+ break
|
|
|
+
|
|
|
+ if not our_position:
|
|
|
+ self.logger.debug(f"⚠️ No open position found for {symbol} - skipping take profit copy")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Calculate our order size based on position ratio
|
|
|
+ our_position_size = abs(float(our_position.get('contracts', 0)))
|
|
|
+ our_position_side = 'long' if float(our_position.get('contracts', 0)) > 0 else 'short'
|
|
|
+
|
|
|
+ # Determine take profit side (opposite of position)
|
|
|
+ tp_side = 'sell' if our_position_side == 'long' else 'buy'
|
|
|
+
|
|
|
+ # Check if we already have a take profit for this position
|
|
|
+ existing_orders = await asyncio.to_thread(self.client.get_open_orders, trading_symbol)
|
|
|
+ if existing_orders:
|
|
|
+ for existing_order in existing_orders:
|
|
|
+ if ('take' in existing_order.get('type', '').lower() or
|
|
|
+ 'tp' in existing_order.get('type', '').lower() or
|
|
|
+ existing_order.get('side') == tp_side):
|
|
|
+ # Check if it's a limit order in profit direction
|
|
|
+ current_price = float(our_position.get('markPrice', 0))
|
|
|
+ order_price = float(existing_order.get('price', 0))
|
|
|
+
|
|
|
+ # Skip if we already have a profitable exit order
|
|
|
+ if ((our_position_side == 'long' and order_price > current_price) or
|
|
|
+ (our_position_side == 'short' and order_price < current_price)):
|
|
|
+ self.logger.debug(f"📋 Take profit already exists for {symbol} - skipping")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Place the take profit order (using limit order)
|
|
|
+ self.logger.info(f"🎯 Placing take profit for {symbol}: {tp_side} {our_position_size:.6f} @ ${trigger_price}")
|
|
|
+
|
|
|
+ result, error = await asyncio.to_thread(
|
|
|
+ self.client.place_take_profit_order,
|
|
|
+ symbol=trading_symbol,
|
|
|
+ side=tp_side,
|
|
|
+ amount=our_position_size,
|
|
|
+ take_profit_price_arg=float(trigger_price)
|
|
|
+ )
|
|
|
+
|
|
|
+ if error:
|
|
|
+ self.logger.error(f"❌ Failed to place take profit for {symbol}: {error}")
|
|
|
+ else:
|
|
|
+ self.logger.info(f"✅ Successfully copied take profit for {symbol}")
|
|
|
+
|
|
|
+ # Send notification
|
|
|
+ if self.notifications_enabled:
|
|
|
+ await self.notification_manager.send_generic_notification(
|
|
|
+ f"🎯 Take Profit Copied\n"
|
|
|
+ f"Symbol: {symbol}\n"
|
|
|
+ f"Target: ${trigger_price}\n"
|
|
|
+ f"Size: {our_position_size:.6f}\n"
|
|
|
+ f"Side: {tp_side}"
|
|
|
+ )
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"❌ Error copying take profit for {symbol}: {e}")
|
|
|
+
|
|
|
async def _execute_hyperliquid_trade(self, trade: CopyTrade, position_calc: Dict[str, float], leverage: float) -> bool:
|
|
|
- """Execute trade on Hyperliquid"""
|
|
|
+ """Execute trade on Hyperliquid using market orders (fallback method)"""
|
|
|
try:
|
|
|
# Determine if this is a buy or sell order
|
|
|
is_buy = 'long' in trade.action or ('close' in trade.action and 'short' in trade.action)
|
|
@@ -640,7 +924,7 @@ class CopyTradingMonitor:
|
|
|
margin_used = position_calc['margin_to_use']
|
|
|
position_value = position_calc['position_value']
|
|
|
|
|
|
- self.logger.info(f"🔄 Executing {trade.action} for {trade.coin}:")
|
|
|
+ self.logger.info(f"🔄 Executing {trade.action} for {trade.coin} with MARKET order:")
|
|
|
self.logger.info(f" 📊 Side: {side}")
|
|
|
self.logger.info(f" 🪙 Token Amount: {token_amount:.6f} {trade.coin}")
|
|
|
self.logger.info(f" 💳 Margin: ${margin_used:.2f}")
|
|
@@ -661,7 +945,6 @@ class CopyTradingMonitor:
|
|
|
)
|
|
|
if leverage_error:
|
|
|
self.logger.warning(f"⚠️ Failed to set leverage to {leverage}x for {trade.coin}: {leverage_error}")
|
|
|
- # Continue anyway - leverage might already be set or order might still work
|
|
|
else:
|
|
|
self.logger.info(f"✅ Successfully set leverage to {leverage}x for {trade.coin}")
|
|
|
|
|
@@ -777,7 +1060,7 @@ class CopyTradingMonitor:
|
|
|
except Exception as e:
|
|
|
self.logger.error(f"❌ Error executing Hyperliquid trade: {e}")
|
|
|
return False
|
|
|
-
|
|
|
+
|
|
|
async def sync_positions(self):
|
|
|
"""Sync our current positions with tracking"""
|
|
|
try:
|