|
@@ -8,6 +8,7 @@ import json
|
|
|
import logging
|
|
|
from typing import Dict, Any, Optional, Tuple, List
|
|
|
from datetime import datetime
|
|
|
+import uuid # For generating unique bot_order_ref_ids
|
|
|
|
|
|
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
|
|
|
|
|
|
- # State persistence
|
|
|
- self.state_file = "trading_engine_state.json"
|
|
|
+ # State persistence (Removed - state is now in DB)
|
|
|
+ # self.state_file = "data/trading_engine_state.json"
|
|
|
|
|
|
- # Position and order tracking
|
|
|
- self.bot_trade_ids = set() # Track bot-generated trades
|
|
|
- self.pending_stop_losses = {} # Pending stop loss orders
|
|
|
+ # Position and order tracking (All main state moved to DB via TradingStats)
|
|
|
|
|
|
- # Load state and initialize stats
|
|
|
- self._load_state()
|
|
|
+ # Initialize stats (this will connect to/create the DB)
|
|
|
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
|
|
|
|
|
|
+ # Determine price and amount for order placement
|
|
|
if limit_price_arg is not None:
|
|
|
- # Limit order intent
|
|
|
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:
|
|
|
- # Market order intent
|
|
|
- if current_price <= 0: # Re-check specifically for market order if current_price was initially 0
|
|
|
+ else: # Market order
|
|
|
+ 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)
|
|
|
+
|
|
|
+ # 1. Generate bot_order_ref_id and record order placement intent
|
|
|
+ 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."}
|
|
|
+
|
|
|
+ # 2. Place the order with the exchange
|
|
|
+ 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: # Market order
|
|
|
+ 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')
|
|
|
-
|
|
|
- # For stats, use average fill price if available, otherwise the price order was placed at.
|
|
|
- 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')
|
|
|
+
|
|
|
+ # 3. Update order in DB with exchange_order_id and status
|
|
|
+ if exchange_oid:
|
|
|
+ # If it's a market order that might have filled, client response might indicate status.
|
|
|
+ # For Hyperliquid, a successful market order usually means it's filled or being filled.
|
|
|
+ # Limit orders will be 'open'.
|
|
|
+ # We will rely on MarketMonitor to confirm fills for market orders too via fill data.
|
|
|
+ new_status_after_placement = 'open' # Default for limit, or submitted for market
|
|
|
+ if order_type_for_stats == 'market':
|
|
|
+ # Market orders might be considered 'submitted' until fill is confirmed by MarketMonitor
|
|
|
+ # Or, if API indicates immediate fill, could be 'filled' - for now, let's use 'open' or 'submitted'
|
|
|
+ new_status_after_placement = 'submitted' # More accurate for market until fill is seen
|
|
|
+
|
|
|
+ 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.")
|
|
|
+ # Potentially update status to 'submission_no_exch_id'
|
|
|
|
|
|
- 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)
|
|
|
-
|
|
|
- # Record in stats
|
|
|
- action_type = self.stats.record_trade_with_enhanced_tracking(
|
|
|
- symbol, 'buy', token_amount, final_price_for_stats, order_id, "bot"
|
|
|
- )
|
|
|
-
|
|
|
- # Handle stop loss if specified
|
|
|
- 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()
|
|
|
+ # DO NOT record trade here. MarketMonitor will handle fills.
|
|
|
+ # action_type = self.stats.record_trade_with_enhanced_tracking(...)
|
|
|
+
|
|
|
+ if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
|
|
|
+ # Record the pending SL order in the orders table
|
|
|
+ sl_bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ sl_order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol,
|
|
|
+ side='sell', # SL for a long is a sell
|
|
|
+ order_type='STOP_LIMIT_TRIGGER', # Indicates a conditional order that will become a limit order
|
|
|
+ amount_requested=token_amount,
|
|
|
+ price=stop_loss_price, # This is the trigger price, and also the limit price for the SL order
|
|
|
+ bot_order_ref_id=sl_bot_order_ref_id,
|
|
|
+ status='pending_trigger',
|
|
|
+ parent_bot_order_ref_id=bot_order_ref_id # Link to the main buy order
|
|
|
+ )
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ # "action_type": action_type, # Removed as trade is not recorded here
|
|
|
"token_amount": token_amount,
|
|
|
- "actual_price": final_price_for_stats,
|
|
|
+ # "actual_price": final_price_for_stats, # Removed as fill is not processed here
|
|
|
"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: # Market order
|
|
|
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)
|
|
|
+
|
|
|
+ # 1. Generate bot_order_ref_id and record order placement intent
|
|
|
+ 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."}
|
|
|
+
|
|
|
+ # 2. Place the order with the exchange
|
|
|
+ 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: # Market order
|
|
|
+ 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}")
|
|
|
+ # 3. Update order in DB with exchange_order_id and status
|
|
|
+ if exchange_oid:
|
|
|
+ new_status_after_placement = 'open' # Default for limit, or submitted for market
|
|
|
+ if order_type_for_stats == 'market':
|
|
|
+ new_status_after_placement = 'submitted' # Market orders are submitted, fills come via monitor
|
|
|
+
|
|
|
+ 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', # Exit side for short
|
|
|
- 'amount': token_amount,
|
|
|
- 'order_type': 'stop_loss'
|
|
|
- }
|
|
|
-
|
|
|
- self._save_state()
|
|
|
+ # DO NOT record trade here. MarketMonitor will handle fills.
|
|
|
+ # action_type = self.stats.record_trade_with_enhanced_tracking(...)
|
|
|
+
|
|
|
+ if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
|
|
|
+ # Record the pending SL order in the orders table
|
|
|
+ sl_bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ sl_order_db_id = self.stats.record_order_placed(
|
|
|
+ symbol=symbol,
|
|
|
+ side='buy', # SL for a short is a 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 # Link to the main sell order
|
|
|
+ )
|
|
|
+ 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)
|
|
|
-
|
|
|
- # Execute market order to close 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' # Exit orders are typically market
|
|
|
+
|
|
|
+ # 1. Generate bot_order_ref_id and record order placement intent
|
|
|
+ bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ # Price for a market order is not specified at placement for stats recording, will be determined by fill.
|
|
|
+ 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, # Market order, price determined by fill
|
|
|
+ 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."}
|
|
|
+
|
|
|
+ # 2. Execute market order to close position
|
|
|
+ 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)."}
|
|
|
|
|
|
- # If we reached here, order_data is not None
|
|
|
- order_id = order_data.get('id', 'N/A')
|
|
|
- actual_price = order_data.get('average', 0) # Fallback to 0 if 'average' is missing
|
|
|
-
|
|
|
- if actual_price is None: # Explicitly check for None if 'average' can return it
|
|
|
- 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 # Use current market price as a fallback
|
|
|
- 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')
|
|
|
+
|
|
|
+ # 3. Update order in DB with exchange_order_id and status
|
|
|
+ if exchange_oid:
|
|
|
+ # Market orders are submitted; MarketMonitor will confirm fills.
|
|
|
+ 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}).")
|
|
|
+
|
|
|
+ # DO NOT record trade here. MarketMonitor will handle fills.
|
|
|
+ # The old code below is removed:
|
|
|
+ # order_id = order_data.get('id', 'N/A')
|
|
|
+ # actual_price = order_data.get('average', 0)
|
|
|
+ # ... logic for actual_price fallback ...
|
|
|
+ # if order_id != 'N/A':
|
|
|
+ # self.bot_trade_ids.add(order_id)
|
|
|
+ # action_type = self.stats.record_trade_with_enhanced_tracking(...)
|
|
|
+
|
|
|
+ # Cancel any pending stop losses for this symbol since position will be closed
|
|
|
+ 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, # Info about the position it intends to close
|
|
|
+ "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"}
|
|
|
|
|
|
- # Place limit order at stop loss price
|
|
|
- order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, stop_price)
|
|
|
+ order_type_for_stats = 'limit' # Stop loss is a limit order at stop_price
|
|
|
+
|
|
|
+ # 1. Generate bot_order_ref_id and record order placement intent
|
|
|
+ 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."}
|
|
|
+
|
|
|
+ # 2. Place limit order at stop loss price
|
|
|
+ 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)."}
|
|
|
|
|
|
- # If we reached here, order_data is not None
|
|
|
- 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')
|
|
|
+
|
|
|
+ # 3. Update order in DB with exchange_order_id and status
|
|
|
+ if exchange_oid:
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status='open', # SL/TP limit orders are 'open' until triggered/filled
|
|
|
+ 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, # Info about the position it's protecting
|
|
|
+ "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"}
|
|
|
|
|
|
- # Place limit order at take profit price
|
|
|
- order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, profit_price)
|
|
|
+ order_type_for_stats = 'limit' # Take profit is a limit order at profit_price
|
|
|
+
|
|
|
+ # 1. Generate bot_order_ref_id and record order placement intent
|
|
|
+ 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."}
|
|
|
+
|
|
|
+ # 2. Place limit order at take profit price
|
|
|
+ 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)."}
|
|
|
|
|
|
- # If we reached here, order_data is not None
|
|
|
- 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')
|
|
|
+
|
|
|
+ # 3. Update order in DB with exchange_order_id and status
|
|
|
+ if exchange_oid:
|
|
|
+ self.stats.update_order_status(
|
|
|
+ order_db_id=order_db_id,
|
|
|
+ exchange_order_id=exchange_oid,
|
|
|
+ new_status='open', # SL/TP limit orders are 'open' until triggered/filled
|
|
|
+ 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, # Info about the position it's for
|
|
|
+ "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:
|
|
|
# return {"success": True, "result": data}
|
|
|
|
|
|
# Sticking to original assumption based on typical CCXT cancel_all_orders:
|
|
|
- result = self.client.cancel_all_orders(symbol)
|
|
|
+ # result = self.client.cancel_all_orders(symbol)
|
|
|
# CCXT cancel_all_orders often returns a list of cancelled order structures or similar.
|
|
|
# If it fails, it typically raises an exception handled by the generic catch block below.
|
|
|
|
|
|
- logger.info(f"Attempted to cancel all orders for {symbol}. Result: {result}")
|
|
|
+ # logger.info(f"Attempted to cancel all orders for {symbol}. Result: {result}")
|
|
|
+ # return {
|
|
|
+ # "success": True,
|
|
|
+ # "result": result # This might be a list of order dicts, or specific response from API
|
|
|
+ # }
|
|
|
+
|
|
|
+ # New approach: Assume client.cancel_all_orders returns a list of dicts for cancelled orders, or raises error.
|
|
|
+ # Each dict should have an 'id' field representing the exchange_order_id.
|
|
|
+ cancelled_orders_info, error_msg = self.client.cancel_all_orders(symbol)
|
|
|
+
|
|
|
+ if error_msg:
|
|
|
+ # If the client method returns an error message, it means a general failure,
|
|
|
+ # not specific order cancellation statuses.
|
|
|
+ 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: # Should ideally be an empty list if no orders, not 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)
|
|
|
+
|
|
|
+ # Cancel any pending stop losses linked to the cancelled orders
|
|
|
+ total_cancelled_linked = 0
|
|
|
+ if self.stats and successful_cancellations_db:
|
|
|
+ for exchange_oid in successful_cancellations_db:
|
|
|
+ # Get the order from DB to find its bot_order_ref_id
|
|
|
+ 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}")
|
|
|
+
|
|
|
+ # self.bot_trade_ids might need to be pruned of these OIDs if they are fully cancelled.
|
|
|
+ # However, relying on the 'orders' table status is better long-term.
|
|
|
+ # For now, _save_state() is not called here as bot_trade_ids interaction is complex with cancellations.
|
|
|
+
|
|
|
return {
|
|
|
"success": True,
|
|
|
- "result": result # This might be a list of order dicts, or specific response from API
|
|
|
+ "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:
|
|
|
# If client.cancel_all_orders raises an Exception that is caught here,
|
|
|
# we can use the _extract_error_message if it's available from client.
|
|
@@ -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 # If found, it was a bot-managed order
|
|
|
|
|
|
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')
|
|
|
+ # Side of the actual SL order to be placed (e.g., if trigger was 'sell', actual order is 'sell')
|
|
|
+ sl_order_side = trigger_order_details.get('side')
|
|
|
+ amount = trigger_order_details.get('amount_requested')
|
|
|
+ stop_price = trigger_order_details.get('price') # This was the trigger price, now becomes the limit price for the SL
|
|
|
+ parent_bot_ref_id_of_trigger = trigger_order_details.get('bot_order_ref_id') # The ref ID of the trigger order itself
|
|
|
+
|
|
|
+ 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}
|
|
|
+
|
|
|
+ # This logic is very similar to execute_stop_loss_order or execute_take_profit_order
|
|
|
+ # It places a new limit order that acts as the Stop Loss.
|
|
|
+ order_type_for_actual_sl = 'limit'
|
|
|
+
|
|
|
+ # 1. Generate a new bot_order_ref_id for this actual SL limit order
|
|
|
+ actual_sl_bot_order_ref_id = uuid.uuid4().hex
|
|
|
+ # We can link this actual SL order back to the trigger order that spawned it.
|
|
|
+ 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 # Linking actual SL to its trigger order
|
|
|
+ )
|
|
|
+
|
|
|
+ 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}
|
|
|
+
|
|
|
+ # 2. Place the actual SL limit order on the exchange
|
|
|
+ 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')
|
|
|
+
|
|
|
+ # 3. Update the actual SL order in DB with exchange_order_id and status 'open'
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|