|
@@ -8,6 +8,7 @@ import json
|
|
|
import logging
|
|
|
from typing import Dict, Any, Optional, Tuple, List
|
|
|
from datetime import datetime
|
|
|
+import uuid
|
|
|
|
|
|
from src.config.config import Config
|
|
|
from src.clients.hyperliquid_client import HyperliquidClient
|
|
@@ -23,47 +24,14 @@ class TradingEngine:
|
|
|
self.client = HyperliquidClient()
|
|
|
self.stats = None
|
|
|
|
|
|
-
|
|
|
- self.state_file = "trading_engine_state.json"
|
|
|
+
|
|
|
+
|
|
|
|
|
|
-
|
|
|
- self.bot_trade_ids = set()
|
|
|
- self.pending_stop_losses = {}
|
|
|
+
|
|
|
|
|
|
-
|
|
|
- self._load_state()
|
|
|
+
|
|
|
self._initialize_stats()
|
|
|
|
|
|
- def _load_state(self):
|
|
|
- """Load trading engine state from disk."""
|
|
|
- try:
|
|
|
- if os.path.exists(self.state_file):
|
|
|
- with open(self.state_file, 'r') as f:
|
|
|
- state_data = json.load(f)
|
|
|
-
|
|
|
- self.bot_trade_ids = set(state_data.get('bot_trade_ids', []))
|
|
|
- self.pending_stop_losses = state_data.get('pending_stop_losses', {})
|
|
|
-
|
|
|
- logger.info(f"🔄 Loaded trading engine state: {len(self.bot_trade_ids)} tracked trades")
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Error loading trading engine state: {e}")
|
|
|
- self.bot_trade_ids = set()
|
|
|
- self.pending_stop_losses = {}
|
|
|
-
|
|
|
- def _save_state(self):
|
|
|
- """Save trading engine state to disk."""
|
|
|
- try:
|
|
|
- state_data = {
|
|
|
- 'bot_trade_ids': list(self.bot_trade_ids),
|
|
|
- 'pending_stop_losses': self.pending_stop_losses,
|
|
|
- 'last_updated': datetime.now().isoformat()
|
|
|
- }
|
|
|
-
|
|
|
- with open(self.state_file, 'w') as f:
|
|
|
- json.dump(state_data, f, indent=2, default=str)
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Error saving trading engine state: {e}")
|
|
|
-
|
|
|
def _initialize_stats(self):
|
|
|
"""Initialize trading statistics."""
|
|
|
try:
|
|
@@ -145,71 +113,111 @@ class TradingEngine:
|
|
|
if not (limit_price_arg and limit_price_arg > 0):
|
|
|
return {"success": False, "error": f"Invalid current price ({current_price}) for {token} and no valid limit price provided."}
|
|
|
|
|
|
+ order_type_for_stats = 'limit' if limit_price_arg is not None else 'market'
|
|
|
order_placement_price: float
|
|
|
token_amount: float
|
|
|
|
|
|
+
|
|
|
if limit_price_arg is not None:
|
|
|
-
|
|
|
if limit_price_arg <= 0:
|
|
|
return {"success": False, "error": "Limit price must be positive."}
|
|
|
order_placement_price = limit_price_arg
|
|
|
token_amount = usdc_amount / order_placement_price
|
|
|
- logger.info(f"Placing LIMIT BUY order for {token_amount:.6f} {symbol} at ${order_placement_price:,.2f}")
|
|
|
- order_data, error_msg = self.client.place_limit_order(symbol, 'buy', token_amount, order_placement_price)
|
|
|
- else:
|
|
|
-
|
|
|
- if current_price <= 0:
|
|
|
+ else:
|
|
|
+ if current_price <= 0:
|
|
|
return {"success": False, "error": f"Cannot place market order for {token} due to invalid current price: {current_price}"}
|
|
|
- order_placement_price = current_price
|
|
|
+ order_placement_price = current_price
|
|
|
token_amount = usdc_amount / order_placement_price
|
|
|
- logger.info(f"Placing MARKET BUY order for {token_amount:.6f} {symbol} (approx. price ${order_placement_price:,.2f})")
|
|
|
- order_data, error_msg = self.client.place_market_order(symbol, 'buy', token_amount)
|
|
|
+
|
|
|
+
|
|
|
+ bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol, side='buy', order_type=order_type_for_stats,
|
|
|
+ amount_requested=token_amount, price=order_placement_price if order_type_for_stats == 'limit' else None,
|
|
|
+ bot_order_ref_id=bot_order_ref_id, status='pending_submission'
|
|
|
+ )
|
|
|
+
|
|
|
+ if not order_db_id:
|
|
|
+ logger.error(f"Failed to record order intent in DB for {symbol} with bot_ref_id {bot_order_ref_id}")
|
|
|
+ return {"success": False, "error": "Failed to record order intent in database."}
|
|
|
+
|
|
|
+
|
|
|
+ if order_type_for_stats == 'limit':
|
|
|
+ logger.info(f"Placing LIMIT BUY order ({bot_order_ref_id}) for {token_amount:.6f} {symbol} at ${order_placement_price:,.2f}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_limit_order(symbol, 'buy', token_amount, order_placement_price)
|
|
|
+ else:
|
|
|
+ logger.info(f"Placing MARKET BUY order ({bot_order_ref_id}) for {token_amount:.6f} {symbol} (approx. price ${order_placement_price:,.2f})")
|
|
|
+ exchange_order_data, error_msg = self.client.place_market_order(symbol, 'buy', token_amount)
|
|
|
|
|
|
if error_msg:
|
|
|
- logger.error(f"Order placement failed for {symbol}: {error_msg}")
|
|
|
+ logger.error(f"Order placement failed for {symbol} ({bot_order_ref_id}): {error_msg}")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": f"Order placement failed: {error_msg}"}
|
|
|
- if not order_data:
|
|
|
- logger.error(f"Order placement call failed for {symbol}. Client returned no data and no error.")
|
|
|
+ if not exchange_order_data:
|
|
|
+ logger.error(f"Order placement call failed for {symbol} ({bot_order_ref_id}). Client returned no data and no error.")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": "Order placement failed at client level (no order object or error)."}
|
|
|
|
|
|
- order_id = order_data.get('id', 'N/A')
|
|
|
- order_avg_fill_price = order_data.get('average')
|
|
|
-
|
|
|
-
|
|
|
- final_price_for_stats = order_avg_fill_price if order_avg_fill_price is not None else order_placement_price
|
|
|
-
|
|
|
- if final_price_for_stats is None:
|
|
|
- logger.critical(f"CRITICAL: final_price_for_stats is None for order {order_id}. Order: {order_data}, Placement Price: {order_placement_price}, Avg Fill: {order_avg_fill_price}")
|
|
|
- return {"success": False, "error": "Critical: Price for stats recording became None."}
|
|
|
+ exchange_oid = exchange_order_data.get('id')
|
|
|
+
|
|
|
+
|
|
|
+ if exchange_oid:
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ new_status_after_placement = 'open'
|
|
|
+ if order_type_for_stats == 'market':
|
|
|
+
|
|
|
+
|
|
|
+ new_status_after_placement = 'submitted'
|
|
|
+
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status=new_status_after_placement,
|
|
|
+ bot_order_ref_id=bot_order_ref_id
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ logger.warning(f"No exchange_order_id received for order {order_db_id} ({bot_order_ref_id}). Status remains pending_submission or requires manual check.")
|
|
|
+
|
|
|
|
|
|
- logger.info(f"Order {order_id} for {symbol} processing. AvgFill: {order_avg_fill_price}, PlacementPrice: {order_placement_price}, StatsPrice: {final_price_for_stats}")
|
|
|
-
|
|
|
- if order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
-
|
|
|
-
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
- symbol, 'buy', token_amount, final_price_for_stats, order_id, "bot"
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
- if stop_loss_price and order_id != 'N/A':
|
|
|
- self.pending_stop_losses[order_id] = {
|
|
|
- 'token': token,
|
|
|
- 'stop_price': stop_loss_price,
|
|
|
- 'side': 'sell',
|
|
|
- 'amount': token_amount,
|
|
|
- 'order_type': 'stop_loss'
|
|
|
- }
|
|
|
-
|
|
|
- self._save_state()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
|
|
|
+
|
|
|
+ sl_bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ sl_order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol,
|
|
|
+ side='sell',
|
|
|
+ order_type='STOP_LIMIT_TRIGGER',
|
|
|
+ amount_requested=token_amount,
|
|
|
+ price=stop_loss_price,
|
|
|
+ bot_order_ref_id=sl_bot_order_ref_id,
|
|
|
+ status='pending_trigger',
|
|
|
+ parent_bot_order_ref_id=bot_order_ref_id
|
|
|
+ )
|
|
|
+ if sl_order_db_id:
|
|
|
+ logger.info(f"Pending Stop Loss order recorded in DB: ID {sl_order_db_id}, BotRef {sl_bot_order_ref_id}, ParentBotRef {bot_order_ref_id}")
|
|
|
+ else:
|
|
|
+ logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
- "order": order_data,
|
|
|
- "action_type": action_type,
|
|
|
+ "order_placed_details": {
|
|
|
+ "bot_order_ref_id": bot_order_ref_id,
|
|
|
+ "exchange_order_id": exchange_oid,
|
|
|
+ "order_db_id": order_db_id,
|
|
|
+ "symbol": symbol,
|
|
|
+ "side": "buy",
|
|
|
+ "type": order_type_for_stats,
|
|
|
+ "amount_requested": token_amount,
|
|
|
+ "price_requested": order_placement_price if order_type_for_stats == 'limit' else None
|
|
|
+ },
|
|
|
+
|
|
|
"token_amount": token_amount,
|
|
|
- "actual_price": final_price_for_stats,
|
|
|
+
|
|
|
"stop_loss_pending": stop_loss_price is not None
|
|
|
}
|
|
|
except ZeroDivisionError as e:
|
|
@@ -237,6 +245,7 @@ class TradingEngine:
|
|
|
if not (limit_price_arg and limit_price_arg > 0):
|
|
|
return {"success": False, "error": f"Invalid current price ({current_price}) for {token} and no valid limit price provided."}
|
|
|
|
|
|
+ order_type_for_stats = 'limit' if limit_price_arg is not None else 'market'
|
|
|
order_placement_price: float
|
|
|
token_amount: float
|
|
|
|
|
@@ -245,58 +254,92 @@ class TradingEngine:
|
|
|
return {"success": False, "error": "Limit price must be positive."}
|
|
|
order_placement_price = limit_price_arg
|
|
|
token_amount = usdc_amount / order_placement_price
|
|
|
- logger.info(f"Placing LIMIT SELL order for {token_amount:.6f} {symbol} at ${order_placement_price:,.2f}")
|
|
|
- order_data, error_msg = self.client.place_limit_order(symbol, 'sell', token_amount, order_placement_price)
|
|
|
- else:
|
|
|
+ else:
|
|
|
if current_price <= 0:
|
|
|
return {"success": False, "error": f"Cannot place market order for {token} due to invalid current price: {current_price}"}
|
|
|
order_placement_price = current_price
|
|
|
token_amount = usdc_amount / order_placement_price
|
|
|
- logger.info(f"Placing MARKET SELL order for {token_amount:.6f} {symbol} (approx. price ${order_placement_price:,.2f})")
|
|
|
- order_data, error_msg = self.client.place_market_order(symbol, 'sell', token_amount)
|
|
|
+
|
|
|
+
|
|
|
+ bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol, side='sell', order_type=order_type_for_stats,
|
|
|
+ amount_requested=token_amount, price=order_placement_price if order_type_for_stats == 'limit' else None,
|
|
|
+ bot_order_ref_id=bot_order_ref_id, status='pending_submission'
|
|
|
+ )
|
|
|
+
|
|
|
+ if not order_db_id:
|
|
|
+ logger.error(f"Failed to record order intent in DB for {symbol} with bot_ref_id {bot_order_ref_id}")
|
|
|
+ return {"success": False, "error": "Failed to record order intent in database."}
|
|
|
+
|
|
|
+
|
|
|
+ if order_type_for_stats == 'limit':
|
|
|
+ logger.info(f"Placing LIMIT SELL order ({bot_order_ref_id}) for {token_amount:.6f} {symbol} at ${order_placement_price:,.2f}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_limit_order(symbol, 'sell', token_amount, order_placement_price)
|
|
|
+ else:
|
|
|
+ logger.info(f"Placing MARKET SELL order ({bot_order_ref_id}) for {token_amount:.6f} {symbol} (approx. price ${order_placement_price:,.2f})")
|
|
|
+ exchange_order_data, error_msg = self.client.place_market_order(symbol, 'sell', token_amount)
|
|
|
|
|
|
if error_msg:
|
|
|
- logger.error(f"Order placement failed for {symbol}: {error_msg}")
|
|
|
+ logger.error(f"Order placement failed for {symbol} ({bot_order_ref_id}): {error_msg}")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": f"Order placement failed: {error_msg}"}
|
|
|
- if not order_data:
|
|
|
- logger.error(f"Order placement call failed for {symbol}. Client returned no data and no error.")
|
|
|
+ if not exchange_order_data:
|
|
|
+ logger.error(f"Order placement call failed for {symbol} ({bot_order_ref_id}). Client returned no data and no error.")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": "Order placement failed at client level (no order object or error)."}
|
|
|
|
|
|
- order_id = order_data.get('id', 'N/A')
|
|
|
- order_avg_fill_price = order_data.get('average')
|
|
|
+ exchange_oid = exchange_order_data.get('id')
|
|
|
|
|
|
- final_price_for_stats = order_avg_fill_price if order_avg_fill_price is not None else order_placement_price
|
|
|
-
|
|
|
- if final_price_for_stats is None:
|
|
|
- logger.critical(f"CRITICAL: final_price_for_stats is None for order {order_id}. Order: {order_data}, Placement Price: {order_placement_price}, Avg Fill: {order_avg_fill_price}")
|
|
|
- return {"success": False, "error": "Critical: Price for stats recording became None."}
|
|
|
-
|
|
|
- logger.info(f"Order {order_id} for {symbol} processing. AvgFill: {order_avg_fill_price}, PlacementPrice: {order_placement_price}, StatsPrice: {final_price_for_stats}")
|
|
|
+
|
|
|
+ if exchange_oid:
|
|
|
+ new_status_after_placement = 'open'
|
|
|
+ if order_type_for_stats == 'market':
|
|
|
+ new_status_after_placement = 'submitted'
|
|
|
+
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status=new_status_after_placement,
|
|
|
+ bot_order_ref_id=bot_order_ref_id
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ logger.warning(f"No exchange_order_id received for order {order_db_id} ({bot_order_ref_id}).")
|
|
|
|
|
|
- if order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
-
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
- symbol, 'sell', token_amount, final_price_for_stats, order_id, "bot"
|
|
|
- )
|
|
|
-
|
|
|
- if stop_loss_price and order_id != 'N/A':
|
|
|
- self.pending_stop_losses[order_id] = {
|
|
|
- 'token': token,
|
|
|
- 'stop_price': stop_loss_price,
|
|
|
- 'side': 'buy',
|
|
|
- 'amount': token_amount,
|
|
|
- 'order_type': 'stop_loss'
|
|
|
- }
|
|
|
-
|
|
|
- self._save_state()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
|
|
|
+
|
|
|
+ sl_bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ sl_order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol,
|
|
|
+ side='buy',
|
|
|
+ order_type='STOP_LIMIT_TRIGGER',
|
|
|
+ amount_requested=token_amount,
|
|
|
+ price=stop_loss_price,
|
|
|
+ bot_order_ref_id=sl_bot_order_ref_id,
|
|
|
+ status='pending_trigger',
|
|
|
+ parent_bot_order_ref_id=bot_order_ref_id
|
|
|
+ )
|
|
|
+ if sl_order_db_id:
|
|
|
+ logger.info(f"Pending Stop Loss order recorded in DB: ID {sl_order_db_id}, BotRef {sl_bot_order_ref_id}, ParentBotRef {bot_order_ref_id}")
|
|
|
+ else:
|
|
|
+ logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
- "order": order_data,
|
|
|
- "action_type": action_type,
|
|
|
+ "order_placed_details": {
|
|
|
+ "bot_order_ref_id": bot_order_ref_id,
|
|
|
+ "exchange_order_id": exchange_oid,
|
|
|
+ "order_db_id": order_db_id,
|
|
|
+ "symbol": symbol,
|
|
|
+ "side": "sell",
|
|
|
+ "type": order_type_for_stats,
|
|
|
+ "amount_requested": token_amount,
|
|
|
+ "price_requested": order_placement_price if order_type_for_stats == 'limit' else None
|
|
|
+ },
|
|
|
"token_amount": token_amount,
|
|
|
- "actual_price": final_price_for_stats,
|
|
|
"stop_loss_pending": stop_loss_price is not None
|
|
|
}
|
|
|
except ZeroDivisionError as e:
|
|
@@ -314,44 +357,81 @@ class TradingEngine:
|
|
|
|
|
|
try:
|
|
|
symbol = f"{token}/USDC:USDC"
|
|
|
- position_type, exit_side, contracts = self.get_position_direction(position)
|
|
|
-
|
|
|
-
|
|
|
- order_data, error_msg = self.client.place_market_order(symbol, exit_side, contracts)
|
|
|
+ position_type, exit_side, contracts_to_close = self.get_position_direction(position)
|
|
|
+ order_type_for_stats = 'market'
|
|
|
+
|
|
|
+
|
|
|
+ bot_order_ref_id = uuid.uuid4().hex
|
|
|
+
|
|
|
+ order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol, side=exit_side, order_type=order_type_for_stats,
|
|
|
+ amount_requested=contracts_to_close, price=None,
|
|
|
+ bot_order_ref_id=bot_order_ref_id, status='pending_submission'
|
|
|
+ )
|
|
|
+
|
|
|
+ if not order_db_id:
|
|
|
+ logger.error(f"Failed to record exit order intent in DB for {symbol} with bot_ref_id {bot_order_ref_id}")
|
|
|
+ return {"success": False, "error": "Failed to record exit order intent in database."}
|
|
|
+
|
|
|
+
|
|
|
+ logger.info(f"Placing MARKET {exit_side.upper()} order ({bot_order_ref_id}) to close {contracts_to_close:.6f} {symbol}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_market_order(symbol, exit_side, contracts_to_close)
|
|
|
|
|
|
if error_msg:
|
|
|
- logger.error(f"Exit order execution failed for {symbol}: {error_msg}")
|
|
|
+ logger.error(f"Exit order execution failed for {symbol} ({bot_order_ref_id}): {error_msg}")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": f"Exit order execution failed: {error_msg}"}
|
|
|
- if not order_data:
|
|
|
- logger.error(f"Exit order execution call failed for {symbol}. Client returned no data and no error.")
|
|
|
+ if not exchange_order_data:
|
|
|
+ logger.error(f"Exit order execution call failed for {symbol} ({bot_order_ref_id}). Client returned no data and no error.")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": "Exit order execution failed (no order object or error)."}
|
|
|
|
|
|
-
|
|
|
- order_id = order_data.get('id', 'N/A')
|
|
|
- actual_price = order_data.get('average', 0)
|
|
|
-
|
|
|
- if actual_price is None:
|
|
|
- ticker_data = self.get_market_data(symbol)
|
|
|
- current_market_price = float(ticker_data['ticker'].get('last', 0.0) or 0.0) if ticker_data and ticker_data.get('ticker') else 0.0
|
|
|
- actual_price = current_market_price
|
|
|
- logger.warning(f"Order {order_id} for {symbol} had no average fill price. Using current market price ${actual_price:.2f} for stats.")
|
|
|
-
|
|
|
- if order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
-
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
- symbol, exit_side, contracts, actual_price, order_id, "bot"
|
|
|
- )
|
|
|
-
|
|
|
- self._save_state()
|
|
|
+ exchange_oid = exchange_order_data.get('id')
|
|
|
+
|
|
|
+
|
|
|
+ if exchange_oid:
|
|
|
+
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status='submitted',
|
|
|
+ bot_order_ref_id=bot_order_ref_id
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ logger.warning(f"No exchange_order_id received for exit order {order_db_id} ({bot_order_ref_id}).")
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if self.stats:
|
|
|
+ cancelled_sl_count = self.stats.cancel_pending_stop_losses_by_symbol(
|
|
|
+ symbol=symbol,
|
|
|
+ new_status='cancelled_manual_exit'
|
|
|
+ )
|
|
|
+ if cancelled_sl_count > 0:
|
|
|
+ logger.info(f"🛑 Cancelled {cancelled_sl_count} pending stop losses for {symbol} due to manual exit order")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
- "order": order_data,
|
|
|
- "action_type": action_type,
|
|
|
- "position_type": position_type,
|
|
|
- "contracts": contracts,
|
|
|
- "actual_price": actual_price
|
|
|
+ "order_placed_details": {
|
|
|
+ "bot_order_ref_id": bot_order_ref_id,
|
|
|
+ "exchange_order_id": exchange_oid,
|
|
|
+ "order_db_id": order_db_id,
|
|
|
+ "symbol": symbol,
|
|
|
+ "side": exit_side,
|
|
|
+ "type": order_type_for_stats,
|
|
|
+ "amount_requested": contracts_to_close
|
|
|
+ },
|
|
|
+ "position_type_closed": position_type,
|
|
|
+ "contracts_intended_to_close": contracts_to_close,
|
|
|
+ "cancelled_stop_losses": cancelled_sl_count if self.stats else 0
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error executing exit order: {e}")
|
|
@@ -374,30 +454,61 @@ class TradingEngine:
|
|
|
elif position_type == "SHORT" and stop_price <= entry_price:
|
|
|
return {"success": False, "error": "Stop loss price should be above entry price for short positions"}
|
|
|
|
|
|
-
|
|
|
- order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, stop_price)
|
|
|
+ order_type_for_stats = 'limit'
|
|
|
+
|
|
|
+
|
|
|
+ bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol, side=exit_side, order_type=order_type_for_stats,
|
|
|
+ amount_requested=contracts, price=stop_price,
|
|
|
+ bot_order_ref_id=bot_order_ref_id, status='pending_submission'
|
|
|
+ )
|
|
|
+
|
|
|
+ if not order_db_id:
|
|
|
+ logger.error(f"Failed to record SL order intent in DB for {symbol} with bot_ref_id {bot_order_ref_id}")
|
|
|
+ return {"success": False, "error": "Failed to record SL order intent in database."}
|
|
|
+
|
|
|
+
|
|
|
+ logger.info(f"Placing STOP LOSS (LIMIT {exit_side.upper()}) order ({bot_order_ref_id}) for {contracts:.6f} {symbol} at ${stop_price:,.2f}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, stop_price)
|
|
|
|
|
|
if error_msg:
|
|
|
- logger.error(f"Stop loss order placement failed for {symbol}: {error_msg}")
|
|
|
+ logger.error(f"Stop loss order placement failed for {symbol} ({bot_order_ref_id}): {error_msg}")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": f"Stop loss order placement failed: {error_msg}"}
|
|
|
- if not order_data:
|
|
|
- logger.error(f"Stop loss order placement call failed for {symbol}. Client returned no data and no error.")
|
|
|
+ if not exchange_order_data:
|
|
|
+ logger.error(f"Stop loss order placement call failed for {symbol} ({bot_order_ref_id}). Client returned no data and no error.")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": "Stop loss order placement failed (no order object or error)."}
|
|
|
|
|
|
-
|
|
|
- order_id = order_data.get('id', 'N/A')
|
|
|
-
|
|
|
- if order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
-
|
|
|
- self._save_state()
|
|
|
+ exchange_oid = exchange_order_data.get('id')
|
|
|
+
|
|
|
+
|
|
|
+ if exchange_oid:
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status='open',
|
|
|
+ bot_order_ref_id=bot_order_ref_id
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ logger.warning(f"No exchange_order_id received for SL order {order_db_id} ({bot_order_ref_id}).")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
- "order": order_data,
|
|
|
- "position_type": position_type,
|
|
|
- "contracts": contracts,
|
|
|
- "stop_price": stop_price
|
|
|
+ "order_placed_details": {
|
|
|
+ "bot_order_ref_id": bot_order_ref_id,
|
|
|
+ "exchange_order_id": exchange_oid,
|
|
|
+ "order_db_id": order_db_id,
|
|
|
+ "symbol": symbol,
|
|
|
+ "side": exit_side,
|
|
|
+ "type": order_type_for_stats,
|
|
|
+ "amount_requested": contracts,
|
|
|
+ "price_requested": stop_price
|
|
|
+ },
|
|
|
+ "position_type_for_sl": position_type,
|
|
|
+ "contracts_for_sl": contracts,
|
|
|
+ "stop_price_set": stop_price
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error executing stop loss order: {e}")
|
|
@@ -420,30 +531,61 @@ class TradingEngine:
|
|
|
elif position_type == "SHORT" and profit_price >= entry_price:
|
|
|
return {"success": False, "error": "Take profit price should be below entry price for short positions"}
|
|
|
|
|
|
-
|
|
|
- order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, profit_price)
|
|
|
+ order_type_for_stats = 'limit'
|
|
|
+
|
|
|
+
|
|
|
+ bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol, side=exit_side, order_type=order_type_for_stats,
|
|
|
+ amount_requested=contracts, price=profit_price,
|
|
|
+ bot_order_ref_id=bot_order_ref_id, status='pending_submission'
|
|
|
+ )
|
|
|
+
|
|
|
+ if not order_db_id:
|
|
|
+ logger.error(f"Failed to record TP order intent in DB for {symbol} with bot_ref_id {bot_order_ref_id}")
|
|
|
+ return {"success": False, "error": "Failed to record TP order intent in database."}
|
|
|
+
|
|
|
+
|
|
|
+ logger.info(f"Placing TAKE PROFIT (LIMIT {exit_side.upper()}) order ({bot_order_ref_id}) for {contracts:.6f} {symbol} at ${profit_price:,.2f}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, profit_price)
|
|
|
|
|
|
if error_msg:
|
|
|
- logger.error(f"Take profit order placement failed for {symbol}: {error_msg}")
|
|
|
+ logger.error(f"Take profit order placement failed for {symbol} ({bot_order_ref_id}): {error_msg}")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": f"Take profit order placement failed: {error_msg}"}
|
|
|
- if not order_data:
|
|
|
- logger.error(f"Take profit order placement call failed for {symbol}. Client returned no data and no error.")
|
|
|
+ if not exchange_order_data:
|
|
|
+ logger.error(f"Take profit order placement call failed for {symbol} ({bot_order_ref_id}). Client returned no data and no error.")
|
|
|
+ self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=bot_order_ref_id)
|
|
|
return {"success": False, "error": "Take profit order placement failed (no order object or error)."}
|
|
|
|
|
|
-
|
|
|
- order_id = order_data.get('id', 'N/A')
|
|
|
-
|
|
|
- if order_id != 'N/A':
|
|
|
- self.bot_trade_ids.add(order_id)
|
|
|
-
|
|
|
- self._save_state()
|
|
|
+ exchange_oid = exchange_order_data.get('id')
|
|
|
+
|
|
|
+
|
|
|
+ if exchange_oid:
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status='open',
|
|
|
+ bot_order_ref_id=bot_order_ref_id
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ logger.warning(f"No exchange_order_id received for TP order {order_db_id} ({bot_order_ref_id}).")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
- "order": order_data,
|
|
|
- "position_type": position_type,
|
|
|
- "contracts": contracts,
|
|
|
- "profit_price": profit_price
|
|
|
+ "order_placed_details": {
|
|
|
+ "bot_order_ref_id": bot_order_ref_id,
|
|
|
+ "exchange_order_id": exchange_oid,
|
|
|
+ "order_db_id": order_db_id,
|
|
|
+ "symbol": symbol,
|
|
|
+ "side": exit_side,
|
|
|
+ "type": order_type_for_stats,
|
|
|
+ "amount_requested": contracts,
|
|
|
+ "price_requested": profit_price
|
|
|
+ },
|
|
|
+ "position_type_for_tp": position_type,
|
|
|
+ "contracts_for_tp": contracts,
|
|
|
+ "profit_price_set": profit_price
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error executing take profit order: {e}")
|
|
@@ -468,15 +610,70 @@ class TradingEngine:
|
|
|
|
|
|
|
|
|
|
|
|
- result = self.client.cancel_all_orders(symbol)
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
|
- logger.info(f"Attempted to cancel all orders for {symbol}. Result: {result}")
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ cancelled_orders_info, error_msg = self.client.cancel_all_orders(symbol)
|
|
|
+
|
|
|
+ if error_msg:
|
|
|
+
|
|
|
+
|
|
|
+ logger.error(f"Error cancelling all orders for {symbol}: {error_msg}")
|
|
|
+ return {"success": False, "error": f"Failed to cancel orders: {error_msg}"}
|
|
|
+
|
|
|
+ if cancelled_orders_info is None:
|
|
|
+ cancelled_orders_info = []
|
|
|
+ logger.info(f"No orders found or reported as cancelled for {symbol} by the client.")
|
|
|
+
|
|
|
+ successful_cancellations_db = []
|
|
|
+ failed_cancellations_db_update = []
|
|
|
+
|
|
|
+ for order_info in cancelled_orders_info:
|
|
|
+ exchange_oid_to_update = order_info.get('id')
|
|
|
+ if exchange_oid_to_update:
|
|
|
+ success = self.stats.update_order_status(exchange_order_id=exchange_oid_to_update, new_status='cancelled')
|
|
|
+ if success:
|
|
|
+ successful_cancellations_db.append(exchange_oid_to_update)
|
|
|
+ else:
|
|
|
+ failed_cancellations_db_update.append(exchange_oid_to_update)
|
|
|
+
|
|
|
+
|
|
|
+ total_cancelled_linked = 0
|
|
|
+ if self.stats and successful_cancellations_db:
|
|
|
+ for exchange_oid in successful_cancellations_db:
|
|
|
+
|
|
|
+ order_in_db = self.stats.get_order_by_exchange_id(exchange_oid)
|
|
|
+ if order_in_db and order_in_db.get('bot_order_ref_id'):
|
|
|
+ cancelled_linked = self.stats.cancel_linked_orders(
|
|
|
+ parent_bot_order_ref_id=order_in_db['bot_order_ref_id'],
|
|
|
+ new_status='cancelled_parent_cancelled'
|
|
|
+ )
|
|
|
+ total_cancelled_linked += cancelled_linked
|
|
|
+ if cancelled_linked > 0:
|
|
|
+ logger.info(f"🛑 Cancelled {cancelled_linked} linked stop losses for order {exchange_oid}")
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
return {
|
|
|
"success": True,
|
|
|
- "result": result
|
|
|
+ "message": f"Cancellation request processed for {symbol}. {len(successful_cancellations_db)} marked in DB.",
|
|
|
+ "cancelled_on_exchange_ids": [info.get('id') for info in cancelled_orders_info if info.get('id')],
|
|
|
+ "db_updates_successful_ids": successful_cancellations_db,
|
|
|
+ "db_updates_failed_ids": failed_cancellations_db_update,
|
|
|
+ "cancelled_linked_stop_losses": total_cancelled_linked
|
|
|
}
|
|
|
+
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
@@ -499,10 +696,100 @@ class TradingEngine:
|
|
|
"""Alias for cancel_all_orders."""
|
|
|
return await self.cancel_all_orders(token)
|
|
|
|
|
|
- def is_bot_trade(self, trade_id: str) -> bool:
|
|
|
- """Check if a trade was generated by this bot."""
|
|
|
- return trade_id in self.bot_trade_ids
|
|
|
+ def is_bot_trade(self, exchange_order_id: str) -> bool:
|
|
|
+ """Check if an order (by its exchange ID) was recorded by this bot in the orders table."""
|
|
|
+ if not self.stats:
|
|
|
+ return False
|
|
|
+ order_data = self.stats.get_order_by_exchange_id(exchange_order_id)
|
|
|
+ return order_data is not None
|
|
|
|
|
|
def get_stats(self) -> TradingStats:
|
|
|
"""Get trading statistics object."""
|
|
|
- return self.stats
|
|
|
+ return self.stats
|
|
|
+
|
|
|
+ async def execute_triggered_stop_order(self, original_trigger_order_db_id: int) -> Dict[str, Any]:
|
|
|
+ """Executes an actual stop order on the exchange after its trigger condition was met."""
|
|
|
+ if not self.stats:
|
|
|
+ return {"success": False, "error": "TradingStats not available."}
|
|
|
+
|
|
|
+ trigger_order_details = self.stats.get_order_by_db_id(original_trigger_order_db_id)
|
|
|
+ if not trigger_order_details:
|
|
|
+ return {"success": False, "error": f"Original trigger order DB ID {original_trigger_order_db_id} not found."}
|
|
|
+
|
|
|
+ logger.info(f"Executing triggered stop order based on original trigger DB ID: {original_trigger_order_db_id}, details: {trigger_order_details}")
|
|
|
+
|
|
|
+ symbol = trigger_order_details.get('symbol')
|
|
|
+
|
|
|
+ sl_order_side = trigger_order_details.get('side')
|
|
|
+ amount = trigger_order_details.get('amount_requested')
|
|
|
+ stop_price = trigger_order_details.get('price')
|
|
|
+ parent_bot_ref_id_of_trigger = trigger_order_details.get('bot_order_ref_id')
|
|
|
+
|
|
|
+ if not all([symbol, sl_order_side, amount, stop_price]):
|
|
|
+ msg = f"Missing critical details from trigger order DB ID {original_trigger_order_db_id} to place actual SL order."
|
|
|
+ logger.error(msg)
|
|
|
+ return {"success": False, "error": msg}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ order_type_for_actual_sl = 'limit'
|
|
|
+
|
|
|
+
|
|
|
+ actual_sl_bot_order_ref_id = uuid.uuid4().hex
|
|
|
+
|
|
|
+ actual_sl_order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol,
|
|
|
+ side=sl_order_side,
|
|
|
+ order_type=order_type_for_actual_sl,
|
|
|
+ amount_requested=amount,
|
|
|
+ price=stop_price,
|
|
|
+ bot_order_ref_id=actual_sl_bot_order_ref_id,
|
|
|
+ status='pending_submission',
|
|
|
+ parent_bot_order_ref_id=parent_bot_ref_id_of_trigger
|
|
|
+ )
|
|
|
+
|
|
|
+ if not actual_sl_order_db_id:
|
|
|
+ msg = f"Failed to record actual SL order intent in DB (spawned from trigger {original_trigger_order_db_id}). BotRef: {actual_sl_bot_order_ref_id}"
|
|
|
+ logger.error(msg)
|
|
|
+ return {"success": False, "error": msg}
|
|
|
+
|
|
|
+
|
|
|
+ logger.info(f"Placing ACTUAL SL ORDER (LIMIT {sl_order_side.upper()}) from trigger {original_trigger_order_db_id}. New BotRef: {actual_sl_bot_order_ref_id}, Amount: {amount}, Price: ${stop_price}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_limit_order(symbol, sl_order_side, amount, stop_price)
|
|
|
+
|
|
|
+ if error_msg:
|
|
|
+ logger.error(f"Actual SL order placement failed for {symbol} (BotRef {actual_sl_bot_order_ref_id}): {error_msg}")
|
|
|
+ self.stats.update_order_status(order_db_id=actual_sl_order_db_id, new_status='failed_submission', bot_order_ref_id=actual_sl_bot_order_ref_id)
|
|
|
+ return {"success": False, "error": f"Actual SL order placement failed: {error_msg}"}
|
|
|
+ if not exchange_order_data:
|
|
|
+ logger.error(f"Actual SL order placement call failed for {symbol} (BotRef {actual_sl_bot_order_ref_id}). No data/error from client.")
|
|
|
+ self.stats.update_order_status(order_db_id=actual_sl_order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=actual_sl_bot_order_ref_id)
|
|
|
+ return {"success": False, "error": "Actual SL order placement failed at client level."}
|
|
|
+
|
|
|
+ exchange_oid = exchange_order_data.get('id')
|
|
|
+
|
|
|
+
|
|
|
+ if exchange_oid:
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=actual_sl_order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status='open',
|
|
|
+ bot_order_ref_id=actual_sl_bot_order_ref_id
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ logger.warning(f"No exchange_order_id received for actual SL order (BotRef {actual_sl_bot_order_ref_id}).")
|
|
|
+
|
|
|
+ return {
|
|
|
+ "success": True,
|
|
|
+ "message": "Actual Stop Loss order placed successfully.",
|
|
|
+ "placed_sl_order_details": {
|
|
|
+ "bot_order_ref_id": actual_sl_bot_order_ref_id,
|
|
|
+ "exchange_order_id": exchange_oid,
|
|
|
+ "order_db_id": actual_sl_order_db_id,
|
|
|
+ "symbol": symbol,
|
|
|
+ "side": sl_order_side,
|
|
|
+ "type": order_type_for_actual_sl,
|
|
|
+ "amount_requested": amount,
|
|
|
+ "price_requested": stop_price
|
|
|
+ }
|
|
|
+ }
|