|
@@ -125,9 +125,8 @@ class TradingEngine:
|
|
|
return "SHORT", "buy", abs(contracts)
|
|
|
|
|
|
async def execute_long_order(self, token: str, usdc_amount: float,
|
|
|
- price: Optional[float] = None,
|
|
|
+ limit_price_arg: Optional[float] = None,
|
|
|
stop_loss_price: Optional[float] = None) -> Dict[str, Any]:
|
|
|
- """Execute a long order."""
|
|
|
symbol = f"{token}/USDC:USDC"
|
|
|
|
|
|
try:
|
|
@@ -137,138 +136,168 @@ class TradingEngine:
|
|
|
|
|
|
# Get market data for price validation
|
|
|
market_data = self.get_market_data(symbol)
|
|
|
- if not market_data:
|
|
|
+ if not market_data or not market_data.get('ticker'):
|
|
|
return {"success": False, "error": f"Could not fetch market data for {token}"}
|
|
|
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
+ current_price = float(market_data['ticker'].get('last', 0.0) or 0.0)
|
|
|
if current_price <= 0:
|
|
|
- return {"success": False, "error": f"Invalid current price for {token}"}
|
|
|
+ # Allow trading if current_price is 0 but a valid limit_price_arg is provided
|
|
|
+ if not (limit_price_arg and limit_price_arg > 0):
|
|
|
+ return {"success": False, "error": f"Invalid current price ({current_price}) for {token} and no valid limit price provided."}
|
|
|
|
|
|
- # Calculate token amount
|
|
|
- if price:
|
|
|
- # Limit order - use specified price
|
|
|
- token_amount = usdc_amount / price
|
|
|
+ order_placement_price: float
|
|
|
+ token_amount: float
|
|
|
+
|
|
|
+ if limit_price_arg is not None:
|
|
|
+ # Limit order intent
|
|
|
+ if limit_price_arg <= 0:
|
|
|
+ return {"success": False, "error": "Limit price must be positive."}
|
|
|
+ order_placement_price = limit_price_arg
|
|
|
+ token_amount = usdc_amount / order_placement_price
|
|
|
+ logger.info(f"Placing LIMIT BUY order for {token_amount:.6f} {symbol} at ${order_placement_price:,.2f}")
|
|
|
+ order = self.client.place_limit_order(symbol, 'buy', token_amount, order_placement_price)
|
|
|
else:
|
|
|
- # Market order - use current price
|
|
|
- token_amount = usdc_amount / current_price
|
|
|
- price = current_price
|
|
|
-
|
|
|
- # Execute the order
|
|
|
- if price == current_price:
|
|
|
- # Market order
|
|
|
+ # Market order intent
|
|
|
+ if current_price <= 0: # Re-check specifically for market order if current_price was initially 0
|
|
|
+ return {"success": False, "error": f"Cannot place market order for {token} due to invalid current price: {current_price}"}
|
|
|
+ order_placement_price = current_price
|
|
|
+ token_amount = usdc_amount / order_placement_price
|
|
|
+ logger.info(f"Placing MARKET BUY order for {token_amount:.6f} {symbol} (approx. price ${order_placement_price:,.2f})")
|
|
|
order = self.client.place_market_order(symbol, 'buy', token_amount)
|
|
|
- else:
|
|
|
- # Limit order
|
|
|
- order = self.client.place_limit_order(symbol, 'buy', token_amount, price)
|
|
|
|
|
|
- if order:
|
|
|
- # Track the order
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- actual_price = order.get('average', price)
|
|
|
-
|
|
|
- if order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
-
|
|
|
- # Record in stats
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
- symbol, 'buy', token_amount, actual_price, order_id, "bot"
|
|
|
- )
|
|
|
-
|
|
|
- # Handle stop loss if specified
|
|
|
- if stop_loss_price and order_id != 'N/A':
|
|
|
- self.pending_stop_losses[order_id] = {
|
|
|
- 'token': token,
|
|
|
- 'stop_price': stop_loss_price,
|
|
|
- 'side': 'sell', # Exit side for long position
|
|
|
- 'amount': token_amount,
|
|
|
- 'order_type': 'stop_loss'
|
|
|
- }
|
|
|
-
|
|
|
- self._save_state()
|
|
|
-
|
|
|
- return {
|
|
|
- "success": True,
|
|
|
- "order": order,
|
|
|
- "action_type": action_type,
|
|
|
- "token_amount": token_amount,
|
|
|
- "actual_price": actual_price,
|
|
|
- "stop_loss_pending": stop_loss_price is not None
|
|
|
+ if not order:
|
|
|
+ logger.error(f"Order placement call failed for {symbol}. Client returned None or empty.")
|
|
|
+ return {"success": False, "error": "Order placement failed at client level (no order object)."}
|
|
|
+
|
|
|
+ order_id = order.get('id', 'N/A')
|
|
|
+ order_avg_fill_price = order.get('average')
|
|
|
+
|
|
|
+ # For stats, use average fill price if available, otherwise the price order was placed at.
|
|
|
+ final_price_for_stats = order_avg_fill_price if order_avg_fill_price is not None else order_placement_price
|
|
|
+
|
|
|
+ if final_price_for_stats is None:
|
|
|
+ logger.critical(f"CRITICAL: final_price_for_stats is None for order {order_id}. Order: {order}, Placement Price: {order_placement_price}, Avg Fill: {order_avg_fill_price}")
|
|
|
+ return {"success": False, "error": "Critical: Price for stats recording became None."}
|
|
|
+
|
|
|
+ logger.info(f"Order {order_id} for {symbol} processing. AvgFill: {order_avg_fill_price}, PlacementPrice: {order_placement_price}, StatsPrice: {final_price_for_stats}")
|
|
|
+
|
|
|
+ if order_id != 'N/A':
|
|
|
+ self.bot_trade_ids.add(order_id)
|
|
|
+
|
|
|
+ # Record in stats
|
|
|
+ action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
+ symbol, 'buy', token_amount, final_price_for_stats, order_id, "bot"
|
|
|
+ )
|
|
|
+
|
|
|
+ # Handle stop loss if specified
|
|
|
+ if stop_loss_price and order_id != 'N/A':
|
|
|
+ self.pending_stop_losses[order_id] = {
|
|
|
+ 'token': token,
|
|
|
+ 'stop_price': stop_loss_price,
|
|
|
+ 'side': 'sell',
|
|
|
+ 'amount': token_amount,
|
|
|
+ 'order_type': 'stop_loss'
|
|
|
}
|
|
|
- else:
|
|
|
- return {"success": False, "error": "Order execution failed"}
|
|
|
-
|
|
|
+
|
|
|
+ self._save_state()
|
|
|
+
|
|
|
+ return {
|
|
|
+ "success": True,
|
|
|
+ "order": order,
|
|
|
+ "action_type": action_type,
|
|
|
+ "token_amount": token_amount,
|
|
|
+ "actual_price": final_price_for_stats,
|
|
|
+ "stop_loss_pending": stop_loss_price is not None
|
|
|
+ }
|
|
|
+ except ZeroDivisionError as e:
|
|
|
+ logger.error(f"Error executing long order due to ZeroDivisionError (likely price issue): {e}. LimitArg: {limit_price_arg}, CurrentPrice: {current_price if 'current_price' in locals() else 'N/A'}")
|
|
|
+ return {"success": False, "error": f"Math error (division by zero), check prices: {e}"}
|
|
|
except Exception as e:
|
|
|
- logger.error(f"Error executing long order: {e}")
|
|
|
+ logger.error(f"Error executing long order: {e}", exc_info=True)
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
async def execute_short_order(self, token: str, usdc_amount: float,
|
|
|
- price: Optional[float] = None,
|
|
|
+ limit_price_arg: Optional[float] = None,
|
|
|
stop_loss_price: Optional[float] = None) -> Dict[str, Any]:
|
|
|
- """Execute a short order."""
|
|
|
symbol = f"{token}/USDC:USDC"
|
|
|
|
|
|
try:
|
|
|
- # Similar to long order but with sell side
|
|
|
if usdc_amount <= 0:
|
|
|
return {"success": False, "error": "Invalid USDC amount"}
|
|
|
|
|
|
market_data = self.get_market_data(symbol)
|
|
|
- if not market_data:
|
|
|
+ if not market_data or not market_data.get('ticker'):
|
|
|
return {"success": False, "error": f"Could not fetch market data for {token}"}
|
|
|
|
|
|
- current_price = float(market_data['ticker'].get('last', 0))
|
|
|
+ current_price = float(market_data['ticker'].get('last', 0.0) or 0.0)
|
|
|
if current_price <= 0:
|
|
|
- return {"success": False, "error": f"Invalid current price for {token}"}
|
|
|
-
|
|
|
- # Calculate token amount
|
|
|
- if price:
|
|
|
- token_amount = usdc_amount / price
|
|
|
+ if not (limit_price_arg and limit_price_arg > 0):
|
|
|
+ return {"success": False, "error": f"Invalid current price ({current_price}) for {token} and no valid limit price provided."}
|
|
|
+
|
|
|
+ order_placement_price: float
|
|
|
+ token_amount: float
|
|
|
+
|
|
|
+ if limit_price_arg is not None:
|
|
|
+ if limit_price_arg <= 0:
|
|
|
+ return {"success": False, "error": "Limit price must be positive."}
|
|
|
+ order_placement_price = limit_price_arg
|
|
|
+ token_amount = usdc_amount / order_placement_price
|
|
|
+ logger.info(f"Placing LIMIT SELL order for {token_amount:.6f} {symbol} at ${order_placement_price:,.2f}")
|
|
|
+ order = self.client.place_limit_order(symbol, 'sell', token_amount, order_placement_price)
|
|
|
else:
|
|
|
- token_amount = usdc_amount / current_price
|
|
|
- price = current_price
|
|
|
-
|
|
|
- # Execute the order
|
|
|
- if price == current_price:
|
|
|
+ if current_price <= 0:
|
|
|
+ return {"success": False, "error": f"Cannot place market order for {token} due to invalid current price: {current_price}"}
|
|
|
+ order_placement_price = current_price
|
|
|
+ token_amount = usdc_amount / order_placement_price
|
|
|
+ logger.info(f"Placing MARKET SELL order for {token_amount:.6f} {symbol} (approx. price ${order_placement_price:,.2f})")
|
|
|
order = self.client.place_market_order(symbol, 'sell', token_amount)
|
|
|
- else:
|
|
|
- order = self.client.place_limit_order(symbol, 'sell', token_amount, price)
|
|
|
|
|
|
- if order:
|
|
|
- order_id = order.get('id', 'N/A')
|
|
|
- actual_price = order.get('average', price)
|
|
|
-
|
|
|
- if order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
-
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
- symbol, 'sell', token_amount, actual_price, order_id, "bot"
|
|
|
- )
|
|
|
-
|
|
|
- # Handle stop loss if specified
|
|
|
- if stop_loss_price and order_id != 'N/A':
|
|
|
- self.pending_stop_losses[order_id] = {
|
|
|
- 'token': token,
|
|
|
- 'stop_price': stop_loss_price,
|
|
|
- 'side': 'buy', # Exit side for short position
|
|
|
- 'amount': token_amount,
|
|
|
- 'order_type': 'stop_loss'
|
|
|
- }
|
|
|
-
|
|
|
- self._save_state()
|
|
|
-
|
|
|
- return {
|
|
|
- "success": True,
|
|
|
- "order": order,
|
|
|
- "action_type": action_type,
|
|
|
- "token_amount": token_amount,
|
|
|
- "actual_price": actual_price,
|
|
|
- "stop_loss_pending": stop_loss_price is not None
|
|
|
+ if not order:
|
|
|
+ logger.error(f"Order placement call failed for {symbol}. Client returned None or empty.")
|
|
|
+ return {"success": False, "error": "Order placement failed at client level (no order object)."}
|
|
|
+
|
|
|
+ order_id = order.get('id', 'N/A')
|
|
|
+ order_avg_fill_price = order.get('average')
|
|
|
+
|
|
|
+ final_price_for_stats = order_avg_fill_price if order_avg_fill_price is not None else order_placement_price
|
|
|
+
|
|
|
+ if final_price_for_stats is None:
|
|
|
+ logger.critical(f"CRITICAL: final_price_for_stats is None for order {order_id}. Order: {order}, Placement Price: {order_placement_price}, Avg Fill: {order_avg_fill_price}")
|
|
|
+ return {"success": False, "error": "Critical: Price for stats recording became None."}
|
|
|
+
|
|
|
+ logger.info(f"Order {order_id} for {symbol} processing. AvgFill: {order_avg_fill_price}, PlacementPrice: {order_placement_price}, StatsPrice: {final_price_for_stats}")
|
|
|
+
|
|
|
+ if order_id != 'N/A':
|
|
|
+ self.bot_trade_ids.add(order_id)
|
|
|
+
|
|
|
+ action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
+ symbol, 'sell', token_amount, final_price_for_stats, order_id, "bot"
|
|
|
+ )
|
|
|
+
|
|
|
+ if stop_loss_price and order_id != 'N/A':
|
|
|
+ self.pending_stop_losses[order_id] = {
|
|
|
+ 'token': token,
|
|
|
+ 'stop_price': stop_loss_price,
|
|
|
+ 'side': 'buy', # Exit side for short
|
|
|
+ 'amount': token_amount,
|
|
|
+ 'order_type': 'stop_loss'
|
|
|
}
|
|
|
- else:
|
|
|
- return {"success": False, "error": "Order execution failed"}
|
|
|
-
|
|
|
+
|
|
|
+ self._save_state()
|
|
|
+
|
|
|
+ return {
|
|
|
+ "success": True,
|
|
|
+ "order": order,
|
|
|
+ "action_type": action_type,
|
|
|
+ "token_amount": token_amount,
|
|
|
+ "actual_price": final_price_for_stats,
|
|
|
+ "stop_loss_pending": stop_loss_price is not None
|
|
|
+ }
|
|
|
+ except ZeroDivisionError as e:
|
|
|
+ logger.error(f"Error executing short order due to ZeroDivisionError (likely price issue): {e}. LimitArg: {limit_price_arg}, CurrentPrice: {current_price if 'current_price' in locals() else 'N/A'}")
|
|
|
+ return {"success": False, "error": f"Math error (division by zero), check prices: {e}"}
|
|
|
except Exception as e:
|
|
|
- logger.error(f"Error executing short order: {e}")
|
|
|
+ logger.error(f"Error executing short order: {e}", exc_info=True)
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
async def execute_exit_order(self, token: str) -> Dict[str, Any]:
|