work with order execution - we are forced to do limit orders over the API
This commit is contained in:
@ -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:
|
||||
|
Reference in New Issue
Block a user