|
@@ -328,25 +328,27 @@ class TradingEngine:
|
|
|
# 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}")
|
|
|
+ # MODIFICATION: Remove SL order recording at this stage. SL price is stored with lifecycle.
|
|
|
+ # 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}")
|
|
|
|
|
|
# 🆕 PHASE 4: Create trade lifecycle for this entry order
|
|
|
+ lifecycle_id = None # Initialize lifecycle_id
|
|
|
if exchange_oid:
|
|
|
entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
|
|
|
if entry_order_record:
|
|
@@ -354,21 +356,17 @@ class TradingEngine:
|
|
|
symbol=symbol,
|
|
|
side='buy',
|
|
|
entry_order_id=exchange_oid, # Store exchange order ID
|
|
|
- stop_loss_price=stop_loss_price,
|
|
|
+ stop_loss_price=stop_loss_price, # Store SL price with lifecycle
|
|
|
+ take_profit_price=None, # Assuming TP is handled separately or not in this command
|
|
|
trade_type='bot'
|
|
|
)
|
|
|
|
|
|
- if lifecycle_id and stop_loss_price:
|
|
|
- # Get the stop loss order that was just created
|
|
|
- sl_order_record = self.stats.get_order_by_bot_ref_id(sl_bot_order_ref_id)
|
|
|
- # sl_order_db_id is the database ID of the pending 'STOP_LIMIT_TRIGGER' order recorded earlier.
|
|
|
- if sl_order_db_id: # Ensure the SL order was successfully recorded and we have its DB ID
|
|
|
- self.stats.link_stop_loss_to_trade(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
|
|
|
- logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for BUY {symbol}")
|
|
|
- else:
|
|
|
- logger.error(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol} with stop_loss_price, but could not link pending SL ID as sl_order_db_id was not available from earlier recording step.")
|
|
|
- elif lifecycle_id:
|
|
|
- logger.info(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol}")
|
|
|
+ if lifecycle_id:
|
|
|
+ logger.info(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol} with SL price {stop_loss_price if stop_loss_price else 'N/A'}")
|
|
|
+ # MODIFICATION: Do not link a pending DB trigger SL here.
|
|
|
+ # if stop_loss_price and sl_order_db_id: # sl_order_db_id is no longer created here
|
|
|
+ # self.stats.link_stop_loss_to_trade(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
|
|
|
+ # logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for BUY {symbol}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
@@ -385,7 +383,8 @@ class TradingEngine:
|
|
|
# "action_type": action_type, # Removed as trade is not recorded here
|
|
|
"token_amount": token_amount,
|
|
|
# "actual_price": final_price_for_stats, # Removed as fill is not processed here
|
|
|
- "stop_loss_pending": stop_loss_price is not None,
|
|
|
+ "stop_loss_pending_activation": stop_loss_price is not None, # Indicates SL needs to be placed after fill
|
|
|
+ "stop_loss_price_if_pending": stop_loss_price,
|
|
|
"trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
|
|
|
}
|
|
|
except ZeroDivisionError as e:
|
|
@@ -477,25 +476,27 @@ class TradingEngine:
|
|
|
# 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}")
|
|
|
+ # MODIFICATION: Remove SL order recording at this stage. SL price is stored with lifecycle.
|
|
|
+ # 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}")
|
|
|
|
|
|
# 🆕 PHASE 4: Create trade lifecycle for this entry order
|
|
|
+ lifecycle_id = None # Initialize lifecycle_id
|
|
|
if exchange_oid:
|
|
|
entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
|
|
|
if entry_order_record:
|
|
@@ -503,20 +504,18 @@ class TradingEngine:
|
|
|
symbol=symbol,
|
|
|
side='sell',
|
|
|
entry_order_id=exchange_oid, # Store exchange order ID
|
|
|
- stop_loss_price=stop_loss_price,
|
|
|
+ stop_loss_price=stop_loss_price, # Store SL price with lifecycle
|
|
|
+ take_profit_price=None, # Assuming TP is handled separately
|
|
|
trade_type='bot'
|
|
|
)
|
|
|
|
|
|
- if lifecycle_id and stop_loss_price:
|
|
|
- # Get the stop loss order that was just created
|
|
|
- sl_order_record = self.stats.get_order_by_bot_ref_id(sl_bot_order_ref_id)
|
|
|
- if sl_order_record:
|
|
|
- self.stats.link_stop_loss_to_trade(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
|
|
|
- logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for SELL {symbol}")
|
|
|
- else:
|
|
|
- logger.info(f"📊 Created trade lifecycle {lifecycle_id} for SELL {symbol}")
|
|
|
- elif lifecycle_id:
|
|
|
- logger.info(f"📊 Created trade lifecycle {lifecycle_id} for SELL {symbol}")
|
|
|
+ if lifecycle_id:
|
|
|
+ logger.info(f"📊 Created trade lifecycle {lifecycle_id} for SELL {symbol} with SL price {stop_loss_price if stop_loss_price else 'N/A'}")
|
|
|
+ # MODIFICATION: Do not link a pending DB trigger SL here.
|
|
|
+ # sl_order_record = self.stats.get_order_by_bot_ref_id(sl_bot_order_ref_id)
|
|
|
+ # if sl_order_record:
|
|
|
+ # self.stats.link_stop_loss_to_trade(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
|
|
|
+ # logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for SELL {symbol}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
@@ -531,7 +530,8 @@ class TradingEngine:
|
|
|
"price_requested": order_placement_price if order_type_for_stats == 'limit' else None
|
|
|
},
|
|
|
"token_amount": token_amount,
|
|
|
- "stop_loss_pending": stop_loss_price is not None,
|
|
|
+ "stop_loss_pending_activation": stop_loss_price is not None, # Indicates SL needs to be placed after fill
|
|
|
+ "stop_loss_price_if_pending": stop_loss_price,
|
|
|
"trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
|
|
|
}
|
|
|
except ZeroDivisionError as e:
|
|
@@ -658,34 +658,33 @@ 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_type_for_stats = 'stop_market' # Changed from 'limit'
|
|
|
+ order_type_for_stats = 'limit' # MODIFICATION: SL from /sl command is now a direct limit order
|
|
|
|
|
|
# 1. Generate bot_order_ref_id and record order placement intent
|
|
|
bot_order_ref_id = uuid.uuid4().hex
|
|
|
- # For stop_market, the 'price' recorded is the trigger price.
|
|
|
+ # For a direct limit SL, the 'price' recorded is the limit price.
|
|
|
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, # price here is the trigger price
|
|
|
+ amount_requested=contracts, price=stop_price, # price here is the limit price for the SL
|
|
|
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.error(f"Failed to record SL limit order intent in DB for {symbol} (direct /sl command) with bot_ref {bot_order_ref_id}")
|
|
|
+ return {"success": False, "error": "Failed to record SL limit order intent in database."}
|
|
|
|
|
|
- # 2. Place stop-market order using the updated client method
|
|
|
- logger.info(f"Placing STOP LOSS (STOP MARKET {exit_side.upper()}) order ({bot_order_ref_id}) for {formatter.format_amount(contracts, token)} {symbol} at trigger {formatter.format_price_with_symbol(stop_price, token)}")
|
|
|
- # The client method now expects 'stop_price_arg' for the trigger price.
|
|
|
- exchange_order_data, error_msg = self.client.place_stop_loss_order(symbol, exit_side, contracts, stop_price_arg=stop_price)
|
|
|
+ # 2. Place a direct LIMIT order for the stop loss
|
|
|
+ logger.info(f"Placing direct LIMIT STOP LOSS ({exit_side.upper()}) order ({bot_order_ref_id}) for {formatter.format_amount(contracts, token)} {symbol} at limit price {formatter.format_price_with_symbol(stop_price, token)}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, price=stop_price)
|
|
|
|
|
|
if error_msg:
|
|
|
- logger.error(f"Stop loss order placement failed for {symbol} ({bot_order_ref_id}): {error_msg}")
|
|
|
+ logger.error(f"Direct SL Limit 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}"}
|
|
|
+ return {"success": False, "error": f"Direct SL Limit order placement failed: {error_msg}"}
|
|
|
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.")
|
|
|
+ logger.error(f"Direct SL Limit order placement call failed for {symbol} ({bot_order_ref_id}). Client returned no data/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)."}
|
|
|
+ return {"success": False, "error": "Direct SL Limit order placement failed (no order object or error from client)."}
|
|
|
|
|
|
exchange_oid = exchange_order_data.get('id')
|
|
|
|
|
@@ -693,24 +692,28 @@ class TradingEngine:
|
|
|
if exchange_oid:
|
|
|
self.stats.update_order_status(
|
|
|
order_db_id=order_db_id,
|
|
|
- new_status='open', # SL/TP limit orders are 'open' until triggered/filled
|
|
|
+ new_status='open', # Limit orders are 'open' until filled
|
|
|
set_exchange_order_id=exchange_oid
|
|
|
)
|
|
|
else:
|
|
|
- logger.warning(f"No exchange_order_id received for SL order {order_db_id} ({bot_order_ref_id}).")
|
|
|
+ logger.warning(f"No exchange_order_id received for SL limit order {order_db_id} ({bot_order_ref_id}).")
|
|
|
|
|
|
# NOTE: Stop loss orders are protective orders for existing positions
|
|
|
# They do not create new trade cycles - they protect existing trade cycles
|
|
|
|
|
|
# Fetch the lifecycle_id for the current position
|
|
|
lifecycle_id = None
|
|
|
+ active_trade_lc = None # Define active_trade_lc to ensure it's available for the if block
|
|
|
if self.stats:
|
|
|
active_trade_lc = self.stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
|
|
|
if active_trade_lc:
|
|
|
lifecycle_id = active_trade_lc.get('trade_lifecycle_id')
|
|
|
if exchange_oid: # If SL order placed successfully on exchange
|
|
|
+ # Ensure that if an old SL (e.g. stop-market) existed and is being replaced,
|
|
|
+ # it might need to be cancelled first. However, current flow assumes this /sl places a new/updated one.
|
|
|
+ # For simplicity, link_stop_loss_to_trade will update if one already exists or insert.
|
|
|
self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, stop_price)
|
|
|
- logger.info(f"🛡️ Linked SL order {exchange_oid} to lifecycle {lifecycle_id} for {symbol}")
|
|
|
+ logger.info(f"🛡️ Linked SL limit order {exchange_oid} to lifecycle {lifecycle_id} for {symbol} (from /sl command)")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
@@ -952,6 +955,75 @@ class TradingEngine:
|
|
|
logger.error(f"Error in execute_coo_order for {token}: {e}")
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
+ async def place_limit_stop_for_lifecycle(self, lifecycle_id: str, symbol: str, sl_price: float, position_side: str, amount_to_cover: float) -> Dict[str, Any]:
|
|
|
+ """Places a limit stop-loss order for an active trade lifecycle."""
|
|
|
+ formatter = get_formatter()
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+
|
|
|
+ if not all([lifecycle_id, symbol, sl_price > 0, position_side in ['long', 'short'], amount_to_cover > 0]):
|
|
|
+ err_msg = f"Invalid parameters for place_limit_stop_for_lifecycle: lc_id={lifecycle_id}, sym={symbol}, sl_price={sl_price}, pos_side={position_side}, amt={amount_to_cover}"
|
|
|
+ logger.error(err_msg)
|
|
|
+ return {"success": False, "error": err_msg}
|
|
|
+
|
|
|
+ sl_order_side = 'sell' if position_side == 'long' else 'buy'
|
|
|
+ order_type_for_stats = 'limit' # Explicitly a limit order
|
|
|
+
|
|
|
+ # 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=sl_order_side, order_type=order_type_for_stats,
|
|
|
+ amount_requested=amount_to_cover, price=sl_price,
|
|
|
+ bot_order_ref_id=bot_order_ref_id, status='pending_submission'
|
|
|
+ )
|
|
|
+
|
|
|
+ if not order_db_id:
|
|
|
+ msg = f"Failed to record SL limit order intent in DB for {symbol} (Lifecycle: {lifecycle_id}) with bot_ref {bot_order_ref_id}"
|
|
|
+ logger.error(msg)
|
|
|
+ return {"success": False, "error": msg}
|
|
|
+
|
|
|
+ # 2. Place the limit order on the exchange
|
|
|
+ logger.info(f"Placing LIMIT STOP LOSS ({sl_order_side.upper()}) for lifecycle {lifecycle_id[:8]} ({bot_order_ref_id}): {formatter.format_amount(amount_to_cover, token)} {symbol} @ {formatter.format_price_with_symbol(sl_price, token)}")
|
|
|
+ exchange_order_data, error_msg = self.client.place_limit_order(symbol, sl_order_side, amount_to_cover, sl_price)
|
|
|
+
|
|
|
+ if error_msg:
|
|
|
+ logger.error(f"SL Limit order placement failed for {symbol} ({bot_order_ref_id}, LC: {lifecycle_id[:8]}): {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"SL Limit order placement failed: {error_msg}"}
|
|
|
+ if not exchange_order_data:
|
|
|
+ logger.error(f"SL Limit order placement call failed for {symbol} ({bot_order_ref_id}, LC: {lifecycle_id[:8]}). Client returned no data/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": "SL Limit order placement failed (no order object or error from client)."}
|
|
|
+
|
|
|
+ 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,
|
|
|
+ new_status='open', # Limit orders are 'open' until filled
|
|
|
+ set_exchange_order_id=exchange_oid
|
|
|
+ )
|
|
|
+ # 4. Link this exchange SL order to the trade lifecycle
|
|
|
+ self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, sl_price)
|
|
|
+ logger.info(f"🛡️ Successfully placed and linked SL limit order {exchange_oid} to lifecycle {lifecycle_id} for {symbol}")
|
|
|
+ else:
|
|
|
+ logger.warning(f"No exchange_order_id received for SL limit order {order_db_id} ({bot_order_ref_id}, LC: {lifecycle_id[:8]}). Status remains pending_submission.")
|
|
|
+
|
|
|
+ return {
|
|
|
+ "success": True,
|
|
|
+ "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": sl_order_side,
|
|
|
+ "type": order_type_for_stats,
|
|
|
+ "amount_requested": amount_to_cover,
|
|
|
+ "price_requested": sl_price
|
|
|
+ },
|
|
|
+ "trade_lifecycle_id": lifecycle_id
|
|
|
+ }
|
|
|
+
|
|
|
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:
|