live position sync for LIMIT orders
This commit is contained in:
@ -287,37 +287,32 @@ class TradingExecutor:
|
||||
self.lock.release()
|
||||
logger.debug(f"LOCK RELEASED: {action} for {symbol}")
|
||||
|
||||
def _cancel_open_orders(self, symbol: str) -> bool:
|
||||
"""Cancel all open orders for a symbol before placing new orders"""
|
||||
def _cancel_open_orders(self, symbol: str) -> int:
|
||||
"""Cancel all open orders for a symbol and return count of cancelled orders"""
|
||||
try:
|
||||
logger.info(f"Checking for open orders to cancel for {symbol}")
|
||||
if self.simulation_mode:
|
||||
return 0
|
||||
|
||||
open_orders = self.exchange.get_open_orders(symbol)
|
||||
cancelled_count = 0
|
||||
|
||||
for order in open_orders:
|
||||
order_id = order.get('orderId')
|
||||
if order_id:
|
||||
try:
|
||||
cancel_result = self.exchange.cancel_order(symbol, str(order_id))
|
||||
if cancel_result:
|
||||
cancelled_count += 1
|
||||
logger.info(f"Cancelled order {order_id} for {symbol}")
|
||||
time.sleep(0.1) # Small delay between cancellations
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cancel order {order_id}: {e}")
|
||||
|
||||
return cancelled_count
|
||||
|
||||
if open_orders and len(open_orders) > 0:
|
||||
logger.info(f"Found {len(open_orders)} open orders for {symbol}, cancelling...")
|
||||
|
||||
for order in open_orders:
|
||||
order_id = order.get('orderId')
|
||||
if order_id:
|
||||
try:
|
||||
cancel_result = self.exchange.cancel_order(symbol, str(order_id))
|
||||
if cancel_result:
|
||||
logger.info(f"Successfully cancelled order {order_id} for {symbol}")
|
||||
else:
|
||||
logger.warning(f"Failed to cancel order {order_id} for {symbol}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error cancelling order {order_id}: {e}")
|
||||
|
||||
# Wait a moment for cancellations to process
|
||||
time.sleep(0.5)
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"No open orders found for {symbol}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking/cancelling open orders for {symbol}: {e}")
|
||||
return False
|
||||
logger.error(f"Error cancelling open orders for {symbol}: {e}")
|
||||
return 0
|
||||
|
||||
def _check_safety_conditions(self, symbol: str, action: str) -> bool:
|
||||
"""Check if it's safe to execute a trade"""
|
||||
@ -1597,4 +1592,244 @@ class TradingExecutor:
|
||||
if enabled:
|
||||
logger.info("TRADING EXECUTOR: Test mode enabled - bypassing safety checks")
|
||||
else:
|
||||
logger.info("TRADING EXECUTOR: Test mode disabled - normal safety checks active")
|
||||
logger.info("TRADING EXECUTOR: Test mode disabled - normal safety checks active")
|
||||
|
||||
def sync_position_with_mexc(self, symbol: str, desired_state: str) -> bool:
|
||||
"""Synchronize dashboard position state with actual MEXC account positions
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (e.g., 'ETH/USDT')
|
||||
desired_state: Desired position state ('NO_POSITION', 'LONG', 'SHORT')
|
||||
|
||||
Returns:
|
||||
bool: True if synchronization successful
|
||||
"""
|
||||
try:
|
||||
logger.info(f"POSITION SYNC: Starting sync for {symbol} - desired state: {desired_state}")
|
||||
|
||||
if self.simulation_mode:
|
||||
logger.info("POSITION SYNC: Simulation mode - skipping MEXC account sync")
|
||||
return True
|
||||
|
||||
# Step 1: Cancel all pending orders for the symbol
|
||||
cancelled_orders = self._cancel_open_orders(symbol)
|
||||
if cancelled_orders > 0:
|
||||
logger.info(f"POSITION SYNC: Cancelled {cancelled_orders} pending orders for {symbol}")
|
||||
time.sleep(1) # Wait for cancellations to process
|
||||
|
||||
# Step 2: Get current MEXC account balances and positions
|
||||
current_balances = self._get_mexc_account_balances()
|
||||
current_holdings = self._get_current_holdings(symbol, current_balances)
|
||||
|
||||
# Step 3: Determine current position state from MEXC account
|
||||
current_state = self._determine_position_state(symbol, current_holdings)
|
||||
logger.info(f"POSITION SYNC: Current MEXC state: {current_state}, Holdings: {current_holdings}")
|
||||
|
||||
# Step 4: If states match, no action needed
|
||||
if current_state == desired_state:
|
||||
logger.info(f"POSITION SYNC: States already match ({current_state}) - no action needed")
|
||||
return True
|
||||
|
||||
# Step 5: Execute corrective trades based on state mismatch
|
||||
return self._execute_corrective_trades(symbol, current_state, desired_state, current_holdings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"POSITION SYNC ERROR: Failed to sync {symbol}: {e}")
|
||||
import traceback
|
||||
logger.error(f"POSITION SYNC: Full traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def _get_mexc_account_balances(self) -> Dict[str, Dict[str, float]]:
|
||||
"""Get current MEXC account balances"""
|
||||
try:
|
||||
return self.exchange.get_all_balances()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get MEXC account balances: {e}")
|
||||
return {}
|
||||
|
||||
def _get_current_holdings(self, symbol: str, balances: Dict[str, Dict[str, float]]) -> Dict[str, Any]:
|
||||
"""Extract current holdings for the symbol from account balances"""
|
||||
try:
|
||||
# Parse symbol to get base and quote assets
|
||||
if '/' in symbol:
|
||||
base_asset, quote_asset = symbol.split('/')
|
||||
else:
|
||||
# Handle symbols like ETHUSDT
|
||||
if symbol.upper().endswith('USDT'):
|
||||
base_asset = symbol[:-4]
|
||||
quote_asset = 'USDT'
|
||||
elif symbol.upper().endswith('USDC'):
|
||||
base_asset = symbol[:-4]
|
||||
quote_asset = 'USDC'
|
||||
else:
|
||||
logger.error(f"Cannot parse symbol: {symbol}")
|
||||
return {'base': 0.0, 'quote': 0.0, 'base_asset': 'UNKNOWN', 'quote_asset': 'UNKNOWN'}
|
||||
|
||||
base_asset = base_asset.upper()
|
||||
quote_asset = quote_asset.upper()
|
||||
|
||||
# Get balances for base and quote assets
|
||||
base_balance = balances.get(base_asset, {}).get('total', 0.0)
|
||||
quote_balance = balances.get(quote_asset, {}).get('total', 0.0)
|
||||
|
||||
# Also check USDC if quote is USDT (MEXC uses USDC for trading)
|
||||
if quote_asset == 'USDT':
|
||||
usdc_balance = balances.get('USDC', {}).get('total', 0.0)
|
||||
quote_balance = max(quote_balance, usdc_balance)
|
||||
|
||||
return {
|
||||
'base': base_balance,
|
||||
'quote': quote_balance,
|
||||
'base_asset': base_asset, # Note: This contains string values but method returns Dict[str, float]
|
||||
'quote_asset': quote_asset # We'll handle this in the calling method
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting current holdings for {symbol}: {e}")
|
||||
return {'base': 0.0, 'quote': 0.0, 'base_asset': 'UNKNOWN', 'quote_asset': 'UNKNOWN'}
|
||||
|
||||
def _determine_position_state(self, symbol: str, holdings: Dict[str, Any]) -> str:
|
||||
"""Determine position state from current holdings"""
|
||||
try:
|
||||
base_balance = holdings.get('base', 0.0)
|
||||
quote_balance = holdings.get('quote', 0.0)
|
||||
|
||||
# Minimum balance thresholds (to ignore dust)
|
||||
min_base_threshold = 0.001 # 0.001 ETH minimum
|
||||
min_quote_threshold = 1.0 # $1 minimum
|
||||
|
||||
has_base = base_balance >= min_base_threshold
|
||||
has_quote = quote_balance >= min_quote_threshold
|
||||
|
||||
if has_base and not has_quote:
|
||||
return 'LONG' # Holding crypto asset
|
||||
elif not has_base and has_quote:
|
||||
return 'SHORT' # Holding only fiat (after selling crypto)
|
||||
elif has_base and has_quote:
|
||||
# Mixed holdings - determine which is larger
|
||||
try:
|
||||
current_price = self._get_current_price_for_sync(symbol)
|
||||
if current_price:
|
||||
base_value = base_balance * current_price
|
||||
if base_value > quote_balance * 1.5: # 50% threshold
|
||||
return 'LONG'
|
||||
else:
|
||||
return 'SHORT'
|
||||
except:
|
||||
return 'LONG' # Default to LONG if price unavailable
|
||||
else:
|
||||
return 'NO_POSITION' # No significant holdings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error determining position state: {e}")
|
||||
return 'NO_POSITION'
|
||||
|
||||
def _get_current_price_for_sync(self, symbol: str) -> Optional[float]:
|
||||
"""Get current price for position synchronization"""
|
||||
try:
|
||||
ticker = self.exchange.get_ticker(symbol)
|
||||
if ticker and 'last' in ticker:
|
||||
return float(ticker['last'])
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting current price for sync: {e}")
|
||||
return None
|
||||
|
||||
def _execute_corrective_trades(self, symbol: str, current_state: str, desired_state: str, holdings: Dict[str, float]) -> bool:
|
||||
"""Execute trades to correct position state mismatch"""
|
||||
try:
|
||||
logger.info(f"CORRECTIVE TRADE: {current_state} -> {desired_state} for {symbol}")
|
||||
|
||||
current_price = self._get_current_price_for_sync(symbol)
|
||||
if not current_price:
|
||||
logger.error("Cannot execute corrective trades without current price")
|
||||
return False
|
||||
|
||||
base_balance = holdings.get('base', 0.0)
|
||||
quote_balance = holdings.get('quote', 0.0)
|
||||
base_asset = holdings.get('base_asset', 'ETH')
|
||||
|
||||
if desired_state == 'NO_POSITION':
|
||||
# Need to sell all crypto holdings
|
||||
if base_balance > 0.001: # Minimum to avoid dust
|
||||
logger.info(f"CORRECTIVE: Selling {base_balance:.6f} {base_asset} to reach NO_POSITION")
|
||||
result = self._place_order_with_retry(symbol, 'SELL', 'LIMIT', base_balance, current_price * 0.999)
|
||||
if result:
|
||||
# Wait for order fill and update internal position tracking
|
||||
time.sleep(2)
|
||||
if symbol in self.positions:
|
||||
del self.positions[symbol]
|
||||
logger.info(f"CORRECTIVE: Successfully sold holdings for NO_POSITION")
|
||||
return True
|
||||
else:
|
||||
logger.error("CORRECTIVE: Failed to sell holdings")
|
||||
return False
|
||||
else:
|
||||
logger.info("CORRECTIVE: Already at NO_POSITION (no crypto holdings)")
|
||||
return True
|
||||
|
||||
elif desired_state == 'LONG':
|
||||
# Need to buy crypto with available quote currency
|
||||
if quote_balance < 10.0: # Minimum order value
|
||||
logger.warning(f"CORRECTIVE: Insufficient quote balance ({quote_balance:.2f}) for LONG position")
|
||||
return False
|
||||
|
||||
# Use 95% of quote balance for the trade (leaving some for fees)
|
||||
trade_amount = quote_balance * 0.95
|
||||
quantity = trade_amount / current_price
|
||||
|
||||
logger.info(f"CORRECTIVE: Buying {quantity:.6f} {base_asset} with ${trade_amount:.2f} for LONG position")
|
||||
result = self._place_order_with_retry(symbol, 'BUY', 'LIMIT', quantity, current_price * 1.001)
|
||||
if result:
|
||||
# Update internal position tracking
|
||||
time.sleep(2)
|
||||
self.positions[symbol] = Position(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
quantity=quantity,
|
||||
entry_price=current_price,
|
||||
entry_time=datetime.now(),
|
||||
order_id=result.get('orderId', f"corrective_{int(time.time())}")
|
||||
)
|
||||
logger.info(f"CORRECTIVE: Successfully established LONG position")
|
||||
return True
|
||||
else:
|
||||
logger.error("CORRECTIVE: Failed to buy for LONG position")
|
||||
return False
|
||||
|
||||
elif desired_state == 'SHORT':
|
||||
# Need to sell crypto holdings to get to cash-only position
|
||||
if base_balance > 0.001:
|
||||
logger.info(f"CORRECTIVE: Selling {base_balance:.6f} {base_asset} for SHORT position")
|
||||
result = self._place_order_with_retry(symbol, 'SELL', 'LIMIT', base_balance, current_price * 0.999)
|
||||
if result:
|
||||
# Update internal position tracking for SHORT
|
||||
time.sleep(2)
|
||||
# For spot trading, SHORT means we sold our crypto and are holding fiat
|
||||
# This is effectively being "short" the crypto asset
|
||||
self.positions[symbol] = Position(
|
||||
symbol=symbol,
|
||||
side='SHORT',
|
||||
quantity=base_balance, # Track the amount we sold
|
||||
entry_price=current_price,
|
||||
entry_time=datetime.now(),
|
||||
order_id=result.get('orderId', f"corrective_{int(time.time())}")
|
||||
)
|
||||
logger.info(f"CORRECTIVE: Successfully established SHORT position")
|
||||
return True
|
||||
else:
|
||||
logger.error("CORRECTIVE: Failed to sell for SHORT position")
|
||||
return False
|
||||
else:
|
||||
logger.info("CORRECTIVE: Already in SHORT position (holding fiat only)")
|
||||
return True
|
||||
|
||||
else:
|
||||
logger.error(f"CORRECTIVE: Unknown desired state: {desired_state}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing corrective trades: {e}")
|
||||
import traceback
|
||||
logger.error(f"CORRECTIVE: Full traceback: {traceback.format_exc()}")
|
||||
return False
|
Reference in New Issue
Block a user