From 0f5d00e292040dd6b2354faf2307f3a2b5d319ff Mon Sep 17 00:00:00 2001 From: pie Date: Sat, 9 May 2026 00:18:42 +0100 Subject: [PATCH] more tweaks --- notes.txt | 4 +++ src/execution/manager.py | 64 ++++++++++++++++++++++---------------- src/strategy/touch_turn.py | 23 +++++++++----- 3 files changed, 57 insertions(+), 34 deletions(-) create mode 100644 notes.txt diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..b4a0328 --- /dev/null +++ b/notes.txt @@ -0,0 +1,4 @@ +Looking at the logs it appears that specifically SELL orders are not getting through + +When making that first trade are we specifying the price we want? I would expect immediate orders to go through in seconds, but the initial order appears to sit as "NEW" for some time... I understand calculations are made, but make a more approx calculation based on the price we are reacting to, then properly calulate SL and TP from the fullfilled order? +in fact are there any indications that other than the initial trade are the SL and TP order going through? diff --git a/src/execution/manager.py b/src/execution/manager.py index 065d565..6fb6851 100644 --- a/src/execution/manager.py +++ b/src/execution/manager.py @@ -19,6 +19,27 @@ class ExecutionManager: self.tp_order_id = None self.is_in_position = 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: + return func(*args, **kwargs) + except Exception as e: + if '429' in str(e): + wait = (2 ** attempt) + random.uniform(0.1, 1.0) + 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 + else: + raise e + raise Exception(f"Failed after {max_attempts} attempts") + def execute_trade(self, params: Dict[str, Any], target_risk_amount: 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" @@ -51,10 +72,11 @@ class ExecutionManager: approx_price = params.get('current_price', params['entry_price']) if self.is_etp: - # For ETPs, we default to 1.0 share for now as we don't have a live ETP price feed. + # 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: @@ -69,7 +91,7 @@ class ExecutionManager: logger.info(f"Placing immediate {direction} market order for {ticker} (Qty: {quantity})...") try: - order = self.client.place_market_order(ticker, 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 placed successfully. ID: {self.current_order_id}") return True @@ -77,17 +99,6 @@ class ExecutionManager: 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.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.""" if not self.current_order_id: @@ -111,7 +122,7 @@ class ExecutionManager: return False try: - positions = self.client.get_all_open_positions() + positions = self._call_with_retry(self.client.get_all_open_positions) for pos in positions: if pos.get('ticker') == ticker: self.is_in_position = True @@ -127,15 +138,12 @@ class ExecutionManager: target_pct = params.get('target_percent', 1.0) / 100.0 # as decimal if is_etp: - # ETP moves in 'opposite' direction to stock - # BUYing an Inverse ETP means we want it to go UP (Stock goes DOWN) tp_move_pct = target_pct * leverage sl_move_pct = tp_move_pct / 2.0 # 1:2 RR tp_price = actual_entry_price * (1 + tp_move_pct) sl_price = actual_entry_price * (1 - sl_move_pct) - # Since we bought the ETP, brackets are always SELL tp_qty = -quantity sl_qty = -quantity else: @@ -161,10 +169,13 @@ class ExecutionManager: 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.client.place_limit_order(ticker, tp_qty, tp_price, time_validity="GOOD_TILL_CANCEL").get('id') - self.sl_order_id = self.client.place_stop_order(ticker, sl_qty, sl_price, time_validity="GOOD_TILL_CANCEL").get('id') + 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') return True except Exception as e: logger.error(f"Failed to place SL/TP brackets: {e}") @@ -180,7 +191,7 @@ class ExecutionManager: try: if self.tp_order_id: try: - tp_info = self.client.get_order_status(self.tp_order_id) + 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 @@ -195,7 +206,7 @@ class ExecutionManager: if self.sl_order_id: try: - sl_info = self.client.get_order_status(self.sl_order_id) + 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 @@ -221,25 +232,26 @@ class ExecutionManager: logger.info(f"Closing all orders and positions for {trading_ticker}...") if self.current_order_id: - try: self.client.cancel_order(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.client.cancel_order(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.client.cancel_order(self.tp_order_id) + try: self._call_with_retry(self.client.cancel_order, self.tp_order_id) except: pass exit_price = 0.0 if self.is_in_position: try: - positions = self.client.get_all_open_positions() + # 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: qty = float(pos.get('quantity', 0)) exit_price = float(pos.get('currentPrice', 0.0)) if qty != 0: - self.client.place_market_order(trading_ticker, -qty) + self._call_with_retry(self.client.place_market_order, trading_ticker, -qty) break except Exception as e: logger.error(f"Failed to flatten position: {e}") diff --git a/src/strategy/touch_turn.py b/src/strategy/touch_turn.py index 9e7e1f1..95dcb3e 100644 --- a/src/strategy/touch_turn.py +++ b/src/strategy/touch_turn.py @@ -84,11 +84,12 @@ class TouchTurnStrategy: low = opening_candle['Low'] open_p = opening_candle['Open'] close_p = opening_candle['Close'] - range_size = high - low + self.range_size = high - low + self.current_price = close_p # 1. Liquidity Filter - if range_size < (daily_atr * self.risk_percent_atr / 100): - logger.info(f"Setup invalid: Range ({range_size:.2f}) < 25% of ATR ({daily_atr:.2f})") + if self.range_size < (daily_atr * self.risk_percent_atr / 100): + logger.info(f"Setup invalid: Range ({self.range_size:.2f}) < 25% of ATR ({daily_atr:.2f})") self.valid_setup = False return False @@ -106,19 +107,22 @@ class TouchTurnStrategy: # For LONG (from Low): target is 38.2% up from the Low # For SHORT (from High): target is 38.2% down from the High if self.direction == -1: # LONG - self.target_price = low + (range_size * 0.382) + self.target_price = low + (self.range_size * 0.382) target_distance = self.target_price - self.entry_price stop_distance = target_distance / self.rr_ratio self.stop_loss = self.entry_price - stop_distance else: # SHORT - self.target_price = high - (range_size * 0.382) + self.target_price = high - (self.range_size * 0.382) target_distance = self.entry_price - self.target_price stop_distance = target_distance / self.rr_ratio self.stop_loss = self.entry_price + stop_distance + # 4. Calculate Percentage Move (needed for ETP scaling) + # We use absolute percentage move relative to the entry price + self.target_percent = (target_distance / self.entry_price) * 100 self.valid_setup = True - logger.info(f"Valid Setup! Entry: {self.entry_price:.2f}, Target: {self.target_price:.2f}, SL: {self.stop_loss:.2f}") + logger.info(f"Valid Setup! Entry: {self.entry_price:.2f}, Target: {self.target_price:.2f} ({self.target_percent:.2f}%), SL: {self.stop_loss:.2f}") return True def get_trade_params(self): @@ -129,7 +133,10 @@ class TouchTurnStrategy: return { "ticker": self.ticker, "direction": "BUY" if self.direction == -1 else "SELL", - "entry_price": self.entry_price, + "entry_price": self.entry_price, + "current_price": self.current_price, "target_price": self.target_price, - "stop_loss": self.stop_loss + "stop_loss": self.stop_loss, + "range_size": self.range_size, + "target_percent": self.target_percent }