live position sync for LIMIT orders

This commit is contained in:
Dobromir Popov
2025-07-14 14:50:30 +03:00
parent f861559319
commit d53a2ba75d
5 changed files with 822 additions and 51 deletions

View File

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