todays modifications
This commit is contained in:
+71
-66
@@ -10,8 +10,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class ExecutionManager:
|
||||
"""
|
||||
Manages the lifecycle of a trade: Entry, SL/TP placement, and Exit.
|
||||
Supports Inverse ETPs for Shorting in ISA mode.
|
||||
Manages the lifecycle of a trade: Entry, SL placement, and Exit.
|
||||
Uses a Hybrid Strategy: Broker-side SL and Bot-side TP monitoring.
|
||||
"""
|
||||
def __init__(self, client: Trading212Client):
|
||||
self.client = client
|
||||
@@ -19,10 +19,11 @@ class ExecutionManager:
|
||||
self.sl_order_id = None
|
||||
self.tp_order_id = None
|
||||
self.is_in_position = False
|
||||
self.leverage = 1.0
|
||||
self.is_etp = False
|
||||
|
||||
def _call_with_retry(self, func, *args, **kwargs):
|
||||
"""Helper to call an API function with retries and jitter."""
|
||||
import random
|
||||
max_attempts = 5
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
@@ -33,7 +34,6 @@ class ExecutionManager:
|
||||
logger.warning(f"Rate limited. Retrying in {wait:.1f}s...")
|
||||
time.sleep(wait)
|
||||
elif '400' in str(e) or '403' in str(e):
|
||||
# For 400/403, logging the body is crucial
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
logger.error(f"API Error Body: {e.response.text}")
|
||||
raise e
|
||||
@@ -41,7 +41,7 @@ class ExecutionManager:
|
||||
raise e
|
||||
raise Exception(f"Failed after {max_attempts} attempts")
|
||||
|
||||
def execute_trade(self, params: Dict[str, Any], target_risk_amount: float = 0.0):
|
||||
def execute_trade(self, params: Dict[str, Any], target_risk_amount: float = 0.0, max_capital: float = 0.0):
|
||||
"""Starts the trade process by placing a MARKET entry order for immediate execution."""
|
||||
isa_mode = os.getenv("ISA_MODE", "False").lower() == "true"
|
||||
|
||||
@@ -53,7 +53,6 @@ class ExecutionManager:
|
||||
self.is_etp = False
|
||||
self.leverage = 1.0
|
||||
|
||||
# 1. ISA Mode Short Substitution
|
||||
if isa_mode and direction == "SELL":
|
||||
if base_ticker in INVERSE_TICKER_MAP:
|
||||
inverse_ticker = INVERSE_TICKER_MAP[base_ticker]
|
||||
@@ -69,19 +68,19 @@ class ExecutionManager:
|
||||
else:
|
||||
self.params['trading_ticker'] = ticker
|
||||
|
||||
# 2. Position Sizing
|
||||
approx_price = params.get('current_price', params['entry_price'])
|
||||
|
||||
if self.is_etp:
|
||||
# Sizing for ETP: Default to 1.0 share if price unknown, otherwise could fetch.
|
||||
quantity = 1.0
|
||||
logger.info(f"Sizing for ETP {ticker}: Defaulting to 1.0 share (Leverage: {self.leverage}x)")
|
||||
else:
|
||||
# We must use round() to avoid 400 Bad Request
|
||||
stop_loss = round(params['stop_loss'], 2)
|
||||
risk_per_share = abs(approx_price - stop_loss)
|
||||
if target_risk_amount > 0 and risk_per_share > 0:
|
||||
quantity = round(target_risk_amount / risk_per_share, 4)
|
||||
q_risk = target_risk_amount / risk_per_share if risk_per_share > 0 else 0
|
||||
q_capital = (max_capital * 0.95) / approx_price if approx_price > 0 else 0
|
||||
|
||||
if q_risk > 0 and q_capital > 0:
|
||||
quantity = round(min(q_risk, q_capital), 4)
|
||||
if quantity < 0.01: quantity = 0.01
|
||||
else:
|
||||
quantity = 1.0
|
||||
@@ -89,7 +88,8 @@ class ExecutionManager:
|
||||
self.current_quantity = quantity
|
||||
trade_quantity = -quantity if direction == "SELL" else quantity
|
||||
|
||||
logger.info(f"Placing immediate {direction} market order for {ticker} (Qty: {quantity})...")
|
||||
logger.info(f"Final Quantity: {quantity} shares. Approx Value: {approx_price * quantity:.2f}")
|
||||
logger.info(f"Placing immediate {direction} market order for {ticker}...")
|
||||
|
||||
try:
|
||||
order = self._call_with_retry(self.client.place_market_order, ticker, trade_quantity)
|
||||
@@ -97,11 +97,35 @@ class ExecutionManager:
|
||||
logger.info(f"Market order placed successfully. ID: {self.current_order_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
# Handle Quantity Precision Error
|
||||
if "precision-mismatch" in str(e) or "precision" in str(e).lower():
|
||||
logger.warning(f"Quantity precision mismatch for {ticker}. Retrying with 2 decimal places...")
|
||||
try:
|
||||
trade_quantity = round(trade_quantity, 2)
|
||||
self.current_quantity = abs(trade_quantity)
|
||||
order = self._call_with_retry(self.client.place_market_order, ticker, trade_quantity)
|
||||
self.current_order_id = order.get('id')
|
||||
logger.info(f"Market order (retry) placed. ID: {self.current_order_id}")
|
||||
return True
|
||||
except Exception as retry_e:
|
||||
logger.error(f"Failed entry retry: {retry_e}")
|
||||
|
||||
logger.error(f"Failed to place entry market order: {e}")
|
||||
return False
|
||||
|
||||
def _is_ticker_in_portfolio(self, ticker: str) -> bool:
|
||||
"""Helper to check if a ticker currently has an open position."""
|
||||
try:
|
||||
positions = self._call_with_retry(self.client.get_all_open_positions)
|
||||
for pos in positions:
|
||||
if pos.get('ticker') == ticker:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking portfolio: {e}")
|
||||
return False
|
||||
|
||||
def monitor_and_bracket(self, params: Dict[str, Any]):
|
||||
"""Polls the entry and places SL/TP based on ACTUAL fill price."""
|
||||
"""Polls the entry and places physical Stop Loss at the broker."""
|
||||
if not self.current_order_id:
|
||||
return False
|
||||
|
||||
@@ -111,17 +135,8 @@ class ExecutionManager:
|
||||
is_etp = getattr(self, 'is_etp', False)
|
||||
leverage = getattr(self, 'leverage', 1.0)
|
||||
|
||||
# Wait for immediate fill confirmation
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
tz = pytz.timezone('US/Eastern')
|
||||
|
||||
actual_entry_price = 0.0
|
||||
|
||||
while not self.is_in_position:
|
||||
if datetime.now(tz).hour >= 11:
|
||||
return False
|
||||
|
||||
try:
|
||||
positions = self._call_with_retry(self.client.get_all_open_positions)
|
||||
for pos in positions:
|
||||
@@ -132,20 +147,15 @@ class ExecutionManager:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Waiting for market fill: {e}")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Calculate brackets from ACTUAL fill price
|
||||
target_pct = params.get('target_percent', 1.0) / 100.0 # as decimal
|
||||
target_pct = params.get('target_percent', 1.0) / 100.0
|
||||
|
||||
if is_etp:
|
||||
tp_move_pct = target_pct * leverage
|
||||
sl_move_pct = tp_move_pct / 2.0 # 1:2 RR
|
||||
|
||||
sl_move_pct = tp_move_pct / 2.0
|
||||
tp_price = actual_entry_price * (1 + tp_move_pct)
|
||||
sl_price = actual_entry_price * (1 - sl_move_pct)
|
||||
|
||||
tp_qty = -quantity
|
||||
sl_qty = -quantity
|
||||
else:
|
||||
range_size = params.get('range_size', 0)
|
||||
@@ -154,69 +164,70 @@ class ExecutionManager:
|
||||
risk_distance = (tp_price - actual_entry_price) / 2.0
|
||||
sl_price = actual_entry_price - risk_distance
|
||||
sl_qty = -quantity
|
||||
tp_qty = -quantity
|
||||
else: # SHORT (Normal stock)
|
||||
tp_price = actual_entry_price - (range_size * 0.382)
|
||||
risk_distance = (actual_entry_price - tp_price) / 2.0
|
||||
sl_price = actual_entry_price + risk_distance
|
||||
sl_qty = quantity
|
||||
tp_qty = quantity
|
||||
|
||||
tp_price = round(tp_price, 2)
|
||||
sl_price = round(sl_price, 2)
|
||||
|
||||
# Save final calculated prices for PnL recording
|
||||
self.params['final_sl'] = sl_price
|
||||
self.params['final_tp'] = tp_price
|
||||
self.params['final_entry'] = actual_entry_price
|
||||
|
||||
# Jitter before placing brackets to avoid 429
|
||||
time.sleep(random.uniform(0.5, 2.0))
|
||||
|
||||
try:
|
||||
logger.info(f"Placing protection for {ticker} (Fill: {actual_entry_price:.2f}): TP @ {tp_price}, SL @ {sl_price}")
|
||||
self.tp_order_id = self._call_with_retry(self.client.place_limit_order, ticker, tp_qty, tp_price, time_validity="GOOD_TILL_CANCEL").get('id')
|
||||
self.sl_order_id = self._call_with_retry(self.client.place_stop_order, ticker, sl_qty, sl_price, time_validity="GOOD_TILL_CANCEL").get('id')
|
||||
logger.info(f"Hybrid Mode: Placing Broker SL for {ticker} @ {sl_price}. Monitoring TP @ {tp_price} manually.")
|
||||
sl_order = self._call_with_retry(self.client.place_stop_order, ticker, sl_qty, sl_price, time_validity="GOOD_TILL_CANCEL")
|
||||
self.sl_order_id = sl_order.get('id')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place SL/TP brackets: {e}")
|
||||
logger.error(f"Failed to place SL bracket: {e}")
|
||||
return False
|
||||
|
||||
def check_exit_status(self) -> tuple[bool, str, float]:
|
||||
"""Checks if the SL or TP orders have been filled by the broker."""
|
||||
"""Checks if SL triggered at broker OR if TP target was hit in market."""
|
||||
if not self.is_in_position:
|
||||
return False, "", 0.0
|
||||
|
||||
ticker = self.params.get('trading_ticker', self.params.get('ticker'))
|
||||
tp_target = self.params.get('final_tp', 0.0)
|
||||
direction = self.params.get('direction', 'BUY')
|
||||
is_etp = getattr(self, 'is_etp', False)
|
||||
|
||||
try:
|
||||
if self.tp_order_id:
|
||||
try:
|
||||
tp_info = self._call_with_retry(self.client.get_order_status, self.tp_order_id)
|
||||
if tp_info.get('status') == "FILLED":
|
||||
fill_price = tp_info.get('filledPrice', tp_info.get('limitPrice', 0))
|
||||
self.is_in_position = False
|
||||
return True, "TP Hit", float(fill_price)
|
||||
except Exception as e:
|
||||
if "404" in str(e):
|
||||
if not self._is_ticker_in_portfolio(ticker):
|
||||
self.is_in_position = False
|
||||
return True, "TP Hit", 0.0
|
||||
else:
|
||||
raise e
|
||||
|
||||
positions = self._call_with_retry(self.client.get_all_open_positions)
|
||||
current_price = 0.0
|
||||
for pos in positions:
|
||||
if pos.get('ticker') == ticker:
|
||||
current_price = float(pos.get('currentPrice', 0.0))
|
||||
break
|
||||
|
||||
if current_price > 0:
|
||||
if (direction == "BUY" or is_etp) and current_price >= tp_target:
|
||||
logger.info(f"Take Profit Target Reached! Price: {current_price} >= {tp_target}")
|
||||
self.close_all(ticker)
|
||||
return True, "TP Hit (Bot)", current_price
|
||||
elif direction == "SELL" and not is_etp and current_price <= tp_target:
|
||||
logger.info(f"Take Profit Target Reached! Price: {current_price} <= {tp_target}")
|
||||
self.close_all(ticker)
|
||||
return True, "TP Hit (Bot)", current_price
|
||||
|
||||
if self.sl_order_id:
|
||||
try:
|
||||
sl_info = self._call_with_retry(self.client.get_order_status, self.sl_order_id)
|
||||
if sl_info.get('status') == "FILLED":
|
||||
fill_price = sl_info.get('filledPrice', sl_info.get('stopPrice', 0))
|
||||
self.is_in_position = False
|
||||
return True, "SL Hit", float(fill_price)
|
||||
return True, "SL Hit (Broker)", float(fill_price)
|
||||
except Exception as e:
|
||||
if "404" in str(e):
|
||||
if not self._is_ticker_in_portfolio(ticker):
|
||||
self.is_in_position = False
|
||||
return True, "SL Hit", 0.0
|
||||
# Fallback to known SL price to avoid 0.0 PnL math
|
||||
fallback_price = float(self.params.get('final_sl', 0.0))
|
||||
return True, "SL Hit (Broker)", fallback_price
|
||||
else:
|
||||
raise e
|
||||
|
||||
@@ -227,25 +238,19 @@ class ExecutionManager:
|
||||
|
||||
def close_all(self, ticker: str) -> float:
|
||||
"""Forces a close of all open orders and positions. Returns the exit price."""
|
||||
# Robust lookup in case execute_trade bypassed early
|
||||
params = getattr(self, 'params', {})
|
||||
trading_ticker = params.get('trading_ticker', ticker)
|
||||
logger.info(f"Closing all orders and positions for {trading_ticker}...")
|
||||
|
||||
if self.current_order_id:
|
||||
try: self._call_with_retry(self.client.cancel_order, self.current_order_id)
|
||||
except: pass
|
||||
if self.sl_order_id:
|
||||
try: self._call_with_retry(self.client.cancel_order, self.sl_order_id)
|
||||
except: pass
|
||||
if self.tp_order_id:
|
||||
try: self._call_with_retry(self.client.cancel_order, self.tp_order_id)
|
||||
try:
|
||||
self._call_with_retry(self.client.cancel_order, self.sl_order_id)
|
||||
self.sl_order_id = None
|
||||
except: pass
|
||||
|
||||
exit_price = 0.0
|
||||
if self.is_in_position:
|
||||
try:
|
||||
# Portfolio check with retry and jitter
|
||||
positions = self._call_with_retry(self.client.get_all_open_positions)
|
||||
for pos in positions:
|
||||
if pos.get('ticker') == trading_ticker:
|
||||
|
||||
Reference in New Issue
Block a user