limit max positions

This commit is contained in:
Dobromir Popov
2025-07-15 02:27:33 +03:00
parent 439611cf88
commit 0b07825be0
4 changed files with 537 additions and 90 deletions

View File

@ -143,6 +143,10 @@ class TradingExecutor:
self.max_open_orders = 2 # Maximum number of open orders allowed
self.open_orders_count = 0 # Current count of open orders
# Rate limiting for open orders sync (30 seconds)
self.last_open_orders_sync = datetime.min
self.open_orders_sync_interval = 30 # seconds
# Trading symbols
self.symbols = self.config.get('symbols', ['ETH/USDT', 'BTC/USDT'])
@ -332,21 +336,42 @@ class TradingExecutor:
logger.debug(f"LOCK RELEASED: {action} for {symbol}")
def _get_open_orders_count(self) -> int:
"""Get current count of open orders across all symbols"""
"""Get current count of open orders across all symbols with rate limiting"""
try:
if self.simulation_mode:
return 0
# Check if enough time has passed since last sync
current_time = datetime.now()
time_since_last_sync = (current_time - self.last_open_orders_sync).total_seconds()
if time_since_last_sync < self.open_orders_sync_interval:
# Return cached count if within rate limit
logger.debug(f"Using cached open orders count ({self.open_orders_count}) - rate limited")
return self.open_orders_count
# Update last sync time
self.last_open_orders_sync = current_time
total_open_orders = 0
for symbol in self.symbols:
open_orders = self.exchange.get_open_orders(symbol)
total_open_orders += len(open_orders)
try:
open_orders = self.exchange.get_open_orders(symbol)
total_open_orders += len(open_orders)
except Exception as e:
logger.warning(f"Error getting open orders for {symbol}: {e}")
# Continue with other symbols
continue
# Update cached count
self.open_orders_count = total_open_orders
logger.debug(f"Updated open orders count: {total_open_orders}")
return total_open_orders
except Exception as e:
logger.error(f"Error getting open orders count: {e}")
return 0
return self.open_orders_count # Return cached value on error
def _can_place_new_order(self) -> bool:
"""Check if we can place a new order based on open order limit"""
@ -359,7 +384,7 @@ class TradingExecutor:
return can_place
def sync_open_orders(self) -> Dict[str, Any]:
"""Synchronize open orders with exchange and update internal state
"""Synchronize open orders with exchange and update internal state with rate limiting
Returns:
dict: Sync result with status and order details
@ -373,6 +398,24 @@ class TradingExecutor:
'count': 0
}
# Check rate limiting
current_time = datetime.now()
time_since_last_sync = (current_time - self.last_open_orders_sync).total_seconds()
if time_since_last_sync < self.open_orders_sync_interval:
# Return cached result if within rate limit
logger.debug(f"Open order sync rate limited - using cached data")
return {
'status': 'rate_limited',
'message': f'Rate limited - last sync was {time_since_last_sync:.1f}s ago',
'orders': [],
'count': self.open_orders_count,
'cached': True
}
# Update last sync time
self.last_open_orders_sync = current_time
sync_result = {
'status': 'started',
'orders': [],
@ -408,7 +451,7 @@ class TradingExecutor:
except Exception as e:
error_msg = f"Error syncing orders for {symbol}: {e}"
logger.error(error_msg)
logger.warning(error_msg) # Changed to warning since this is expected with rate limits
sync_result['errors'].append(error_msg)
# Update internal state
@ -505,11 +548,30 @@ class TradingExecutor:
logger.warning(f"Maximum concurrent positions reached: {len(self.positions)}")
return False
# SYNC OPEN ORDERS BEFORE CHECKING LIMITS
# This ensures we have accurate position data before making decisions
if not self.simulation_mode:
try:
logger.debug(f"Syncing open orders before trade execution for {symbol}")
sync_result = self.sync_open_orders()
if sync_result.get('status') == 'success':
logger.debug(f"Open orders synced successfully: {sync_result.get('count', 0)} orders")
else:
logger.warning(f"Open orders sync failed: {sync_result.get('message', 'Unknown error')}")
except Exception as e:
logger.warning(f"Error syncing open orders: {e}")
# Check open order limit
if not self._can_place_new_order():
logger.warning(f"Maximum open orders reached: {self._get_open_orders_count()}/{self.max_open_orders}")
return False
# Check position size limit before opening new positions
if action in ['BUY', 'SHORT'] and symbol not in self.positions:
if not self._check_position_size_limit():
logger.warning(f"Position size limit reached - cannot open new position for {symbol}")
return False
return True
def _execute_buy(self, symbol: str, confidence: float, current_price: float) -> bool:
@ -1153,25 +1215,30 @@ class TradingExecutor:
return False
def _calculate_position_size(self, confidence: float, current_price: float) -> float:
"""Calculate position size - use 100% of account balance for short-term scalping"""
"""Calculate position size - limited to 20% of account balance by default"""
# Get account balance (simulation or real)
account_balance = self._get_account_balance_for_sizing()
# Use 100% of account balance since we're holding for seconds/minutes only
# Scale by confidence: 70-100% of balance based on confidence (0.7-1.0 range)
# Get maximum position size limit (default 20% of balance)
max_position_percentage = self.mexc_config.get('max_position_percentage', 0.20)
max_position_value = account_balance * max_position_percentage
# Calculate desired position size based on confidence
# Scale by confidence: 70-100% of max position size based on confidence (0.7-1.0 range)
confidence_multiplier = max(0.7, min(1.0, confidence))
position_value = account_balance * confidence_multiplier
desired_position_value = max_position_value * confidence_multiplier
# Apply reduction based on consecutive losses (risk management)
reduction_factor = self.mexc_config.get('consecutive_loss_reduction_factor', 0.8)
adjusted_reduction_factor = reduction_factor ** self.consecutive_losses
position_value *= adjusted_reduction_factor
final_position_value = desired_position_value * adjusted_reduction_factor
logger.debug(f"Position calculation: account=${account_balance:.2f}, "
f"max_position=${max_position_value:.2f} ({max_position_percentage*100:.0f}%), "
f"confidence_mult={confidence_multiplier:.2f}, "
f"position=${position_value:.2f}, confidence={confidence:.2f}")
f"final_position=${final_position_value:.2f}, confidence={confidence:.2f}")
return position_value
return final_position_value
def _get_account_balance_for_sizing(self) -> float:
"""Get account balance for position sizing calculations"""
@ -1188,6 +1255,64 @@ class TradingExecutor:
logger.warning(f"Failed to get live account balance: {e}, using simulation default")
return self.mexc_config.get('simulation_account_usd', 100.0)
def _check_position_size_limit(self) -> bool:
"""Check if total open position value exceeds the maximum allowed percentage of balance"""
try:
# Get account balance
account_balance = self._get_account_balance_for_sizing()
# Get maximum position percentage (default 20%)
max_position_percentage = self.mexc_config.get('max_position_percentage', 0.20)
max_position_value = account_balance * max_position_percentage
# Calculate total current position value
total_position_value = 0.0
# Add existing positions
for symbol, position in self.positions.items():
# Get current price for the symbol
try:
ticker = self.exchange.get_ticker(symbol) if self.exchange else None
current_price = ticker['last'] if ticker and 'last' in ticker else position.entry_price
except Exception:
# Fallback to entry price if we can't get current price
current_price = position.entry_price
# Calculate position value
position_value = position.quantity * current_price
total_position_value += position_value
logger.debug(f"Existing position {symbol}: {position.quantity:.6f} @ ${current_price:.2f} = ${position_value:.2f}")
# Add potential value from open orders that could become positions
if not self.simulation_mode and self.exchange:
try:
open_orders = self.exchange.get_open_orders()
for order in open_orders:
if order.get('side', '').lower() in ['buy', 'sell']:
# Estimate the value if this order gets filled
order_quantity = float(order.get('quantity', 0))
order_price = float(order.get('price', 0))
if order_price > 0:
order_value = order_quantity * order_price
total_position_value += order_value
logger.debug(f"Open order {order.get('symbol')}: {order_quantity:.6f} @ ${order_price:.2f} = ${order_value:.2f}")
except Exception as e:
logger.debug(f"Error calculating open order values: {e}")
# Check if we would exceed the limit
if total_position_value >= max_position_value:
logger.warning(f"Position size limit reached: ${total_position_value:.2f} >= ${max_position_value:.2f} ({max_position_percentage*100:.0f}% of balance)")
return False
logger.debug(f"Position size check passed: ${total_position_value:.2f} < ${max_position_value:.2f} ({max_position_percentage*100:.0f}% of balance)")
return True
except Exception as e:
logger.error(f"Error checking position size limit: {e}")
# Allow trade if we can't check the limit
return True
def update_positions(self, symbol: str, current_price: float):
"""Update position P&L with current market price"""
if symbol in self.positions: