work with order execution - we are forced to do limit orders over the API

This commit is contained in:
Dobromir Popov
2025-07-14 13:36:07 +03:00
parent d7205a9745
commit f861559319
4 changed files with 387 additions and 182 deletions

View File

@ -406,18 +406,26 @@ class TradingExecutor:
# Place real order with enhanced error handling
result = self._place_order_with_retry(symbol, 'BUY', 'MARKET', quantity, current_price)
if result and 'orderId' in result:
# Create position record
self.positions[symbol] = Position(
symbol=symbol,
side='LONG',
quantity=quantity,
entry_price=current_price,
entry_time=datetime.now(),
order_id=result['orderId']
)
logger.info(f"BUY order executed: {result}")
self.last_trade_time[symbol] = datetime.now()
return True
# Use actual fill information if available, otherwise fall back to order parameters
filled_quantity = result.get('executedQty', quantity)
fill_price = result.get('avgPrice', current_price)
# Only create position if order was actually filled
if result.get('filled', True): # Assume filled for backward compatibility
self.positions[symbol] = Position(
symbol=symbol,
side='LONG',
quantity=float(filled_quantity),
entry_price=float(fill_price),
entry_time=datetime.now(),
order_id=result['orderId']
)
logger.info(f"BUY position created: {filled_quantity:.6f} {symbol} at ${fill_price:.4f}")
self.last_trade_time[symbol] = datetime.now()
return True
else:
logger.error(f"BUY order placed but not filled: {result}")
return False
else:
logger.error("Failed to place BUY order")
return False
@ -465,18 +473,26 @@ class TradingExecutor:
# Place real short order with enhanced error handling
result = self._place_order_with_retry(symbol, 'SELL', 'MARKET', quantity, current_price)
if result and 'orderId' in result:
# Create short position record
self.positions[symbol] = Position(
symbol=symbol,
side='SHORT',
quantity=quantity,
entry_price=current_price,
entry_time=datetime.now(),
order_id=result['orderId']
)
logger.info(f"SHORT order executed: {result}")
self.last_trade_time[symbol] = datetime.now()
return True
# Use actual fill information if available, otherwise fall back to order parameters
filled_quantity = result.get('executedQty', quantity)
fill_price = result.get('avgPrice', current_price)
# Only create position if order was actually filled
if result.get('filled', True): # Assume filled for backward compatibility
self.positions[symbol] = Position(
symbol=symbol,
side='SHORT',
quantity=float(filled_quantity),
entry_price=float(fill_price),
entry_time=datetime.now(),
order_id=result['orderId']
)
logger.info(f"SHORT position created: {filled_quantity:.6f} {symbol} at ${fill_price:.4f}")
self.last_trade_time[symbol] = datetime.now()
return True
else:
logger.error(f"SHORT order placed but not filled: {result}")
return False
else:
logger.error("Failed to place SHORT order")
return False
@ -494,7 +510,18 @@ class TradingExecutor:
logger.error(f"Elapsed time: {elapsed_time:.2f}s, cancelling order to prevent lock hanging")
return {}
try:
result = self.exchange.place_order(symbol, side, order_type, quantity, current_price)
# For retries, use more aggressive pricing for LIMIT orders
order_price = current_price
if order_type.upper() == 'LIMIT' and attempt > 0:
# Increase aggressiveness with each retry
aggression_factor = 1 + (0.005 * (attempt + 1)) # 0.5%, 1.0%, 1.5% etc.
if side.upper() == 'BUY':
order_price = current_price * aggression_factor
else:
order_price = current_price / aggression_factor
logger.info(f"Retry {attempt + 1}: Using more aggressive price ${order_price:.4f} (vs market ${current_price:.4f})")
result = self.exchange.place_order(symbol, side, order_type, quantity, order_price)
# Check if result contains error information
if isinstance(result, dict) and 'error' in result:
@ -552,10 +579,39 @@ class TradingExecutor:
else:
return {}
# Success case
# Success case - order placed
elif isinstance(result, dict) and ('orderId' in result or 'symbol' in result):
logger.info(f"Order placed successfully on attempt {attempt + 1}")
return result
# For LIMIT orders, verify that the order actually fills
if order_type.upper() == 'LIMIT' and 'orderId' in result:
order_id = result['orderId']
filled_result = self._wait_for_order_fill(symbol, order_id, max_wait_time=5.0)
if filled_result['filled']:
logger.info(f"LIMIT order {order_id} filled successfully")
# Update result with fill information
result.update(filled_result)
return result
else:
logger.warning(f"LIMIT order {order_id} not filled within timeout, cancelling...")
# Cancel the unfilled order
try:
self.exchange.cancel_order(symbol, order_id)
logger.info(f"Cancelled unfilled order {order_id}")
except Exception as e:
logger.error(f"Failed to cancel unfilled order {order_id}: {e}")
# If this was the last attempt, return failure
if attempt == max_retries - 1:
return {}
# Try again with a more aggressive price
logger.info(f"Retrying with more aggressive LIMIT pricing...")
continue
else:
# MARKET orders or orders without orderId - assume immediate fill
return result
# Empty result - treat as failure
else:
@ -593,6 +649,86 @@ class TradingExecutor:
logger.error(f"Failed to place order after {max_retries} attempts")
return {}
def _wait_for_order_fill(self, symbol: str, order_id: str, max_wait_time: float = 5.0) -> Dict[str, Any]:
"""Wait for a LIMIT order to fill and return fill status
Args:
symbol: Trading symbol
order_id: Order ID to monitor
max_wait_time: Maximum time to wait for fill in seconds
Returns:
dict: {'filled': bool, 'status': str, 'executedQty': float, 'avgPrice': float}
"""
start_time = time.time()
check_interval = 0.2 # Check every 200ms
while time.time() - start_time < max_wait_time:
try:
order_status = self.exchange.get_order_status(symbol, order_id)
if order_status and isinstance(order_status, dict):
status = order_status.get('status', '').upper()
executed_qty = float(order_status.get('executedQty', 0))
orig_qty = float(order_status.get('origQty', 0))
avg_price = float(order_status.get('cummulativeQuoteQty', 0)) / executed_qty if executed_qty > 0 else 0
logger.debug(f"Order {order_id} status: {status}, executed: {executed_qty}/{orig_qty}")
if status == 'FILLED':
return {
'filled': True,
'status': status,
'executedQty': executed_qty,
'avgPrice': avg_price,
'fillTime': time.time()
}
elif status in ['CANCELED', 'REJECTED', 'EXPIRED']:
return {
'filled': False,
'status': status,
'executedQty': executed_qty,
'avgPrice': avg_price,
'reason': f'Order {status.lower()}'
}
elif status == 'PARTIALLY_FILLED':
# For partial fills, continue waiting but log progress
fill_percentage = (executed_qty / orig_qty * 100) if orig_qty > 0 else 0
logger.debug(f"Order {order_id} partially filled: {fill_percentage:.1f}%")
# Wait before next check
time.sleep(check_interval)
except Exception as e:
logger.error(f"Error checking order status for {order_id}: {e}")
time.sleep(check_interval)
# Timeout - check final status
try:
final_status = self.exchange.get_order_status(symbol, order_id)
if final_status:
status = final_status.get('status', '').upper()
executed_qty = float(final_status.get('executedQty', 0))
avg_price = float(final_status.get('cummulativeQuoteQty', 0)) / executed_qty if executed_qty > 0 else 0
return {
'filled': status == 'FILLED',
'status': status,
'executedQty': executed_qty,
'avgPrice': avg_price,
'reason': 'timeout' if status != 'FILLED' else None
}
except Exception as e:
logger.error(f"Error getting final order status for {order_id}: {e}")
return {
'filled': False,
'status': 'UNKNOWN',
'executedQty': 0,
'avgPrice': 0,
'reason': 'timeout_and_status_check_failed'
}
def _close_short_position(self, symbol: str, confidence: float, current_price: float) -> bool:
"""Close a short position by buying"""
if symbol not in self.positions: