diff --git a/main.py b/main.py index 59d491a..b13b974 100644 --- a/main.py +++ b/main.py @@ -113,7 +113,8 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz, num_tickers): params['ticker'] = t212_ticker # Anti-thundering-herd: Random jitter to prevent 429s from parallel threads - time.sleep(random.uniform(0.1, 3.0)) + # Use a larger range (1-10s) to better stagger independent threads + time.sleep(random.uniform(1.0, 10.0)) # Fetch Account Balance to calculate risk with backoff for attempt in range(3): @@ -129,8 +130,9 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz, num_tickers): break except Exception as e: if '429' in str(e): - logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying...") - time.sleep(2**(attempt+1)) + wait_time = (attempt + 1) * 5 + random.uniform(1, 3) + logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {wait_time:.1f}s...") + time.sleep(wait_time) else: logger.error(f"Failed to fetch account info: {e}") break @@ -270,4 +272,10 @@ def main(): flush_logs() if __name__ == "__main__": - main() + try: + main() + except Exception as e: + logger.critical(f"FATAL ERROR in main: {e}", exc_info=True) + finally: + flush_logs() + logger.info("Bot process terminated.") diff --git a/src/execution/manager.py b/src/execution/manager.py index 3d10489..2decb67 100644 --- a/src/execution/manager.py +++ b/src/execution/manager.py @@ -2,6 +2,7 @@ import time import logging import os import random +import json from typing import Dict, Any, Optional from src.api.client import Trading212Client from src.strategy.inverse_mapping import INVERSE_TICKER_MAP, LEVERAGE_MAP @@ -34,6 +35,7 @@ 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 @@ -70,9 +72,10 @@ class ExecutionManager: approx_price = params.get('current_price', params['entry_price']) + # 2. Position Sizing if self.is_etp: quantity = 1.0 - logger.info(f"Sizing for ETP {ticker}: Defaulting to 1.0 share (Leverage: {self.leverage}x)") + logger.info(f"Sizing for ETP {ticker}: Initial attempt with 1.0 share.") else: stop_loss = round(params['stop_loss'], 2) risk_per_share = abs(approx_price - stop_loss) @@ -88,29 +91,49 @@ class ExecutionManager: self.current_quantity = quantity trade_quantity = -quantity if direction == "SELL" else quantity - logger.info(f"Final Quantity: {quantity} shares. Approx Value: {approx_price * quantity:.2f}") - logger.info(f"Placing immediate {direction} market order for {ticker}...") + logger.info(f"Attempting {direction} market order for {ticker} (Qty: {quantity})...") + # 3. Execution with Smart Retry for Common Broker Errors try: 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 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...") + if hasattr(e, 'response') and e.response is not None: 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 + err_data = e.response.json() + err_type = err_data.get('type', '') + err_detail = err_data.get('detail', '') + + # Error A: Quantity Precision Mismatch + if "precision-mismatch" in err_type or "precision" in err_detail.lower(): + logger.warning(f"Precision mismatch for {ticker}. Retrying with 2 decimal places...") + 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') + return True + + # Error B: Minimum Quantity Exceeded + if "min-quantity-exceeded" in err_type: + import re + match = re.search(r"at least ([\d.]+)", err_detail) + if match: + min_qty = float(match.group(1)) + if (min_qty * approx_price) <= (max_capital * 1.05): # Small buffer + logger.warning(f"Quantity too low for {ticker}. Upping to minimum: {min_qty}") + trade_quantity = -min_qty if direction == "SELL" else min_qty + self.current_quantity = min_qty + order = self._call_with_retry(self.client.place_market_order, ticker, trade_quantity) + self.current_order_id = order.get('id') + return True + else: + logger.error(f"Required minimum {min_qty} exceeds available capital for {ticker}.") except Exception as retry_e: - logger.error(f"Failed entry retry: {retry_e}") + logger.error(f"Retry logic failed for {ticker}: {retry_e}") - logger.error(f"Failed to place entry market order: {e}") + logger.error(f"Failed to place entry market order for {ticker}: {e}") return False def _is_ticker_in_portfolio(self, ticker: str) -> bool: @@ -179,11 +202,21 @@ class ExecutionManager: try: 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') + # Use retry with possible precision fix for SL too + try: + 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') + except Exception as sl_e: + if "precision" in str(sl_e).lower(): + logger.warning(f"Precision mismatch for {ticker} Stop. Retrying with 2 decimals...") + sl_qty = round(sl_qty, 2) + 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') + else: + raise sl_e return True except Exception as e: - logger.error(f"Failed to place SL bracket: {e}") + logger.error(f"Failed to place SL bracket for {ticker}: {e}") return False def check_exit_status(self) -> tuple[bool, str, float]: @@ -225,7 +258,6 @@ class ExecutionManager: if "404" in str(e): if not self._is_ticker_in_portfolio(ticker): self.is_in_position = False - # 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: @@ -257,7 +289,14 @@ class ExecutionManager: qty = float(pos.get('quantity', 0)) exit_price = float(pos.get('currentPrice', 0.0)) if qty != 0: - self._call_with_retry(self.client.place_market_order, trading_ticker, -qty) + # Try to close with precision fix + try: + self._call_with_retry(self.client.place_market_order, trading_ticker, -qty) + except Exception as close_e: + if "precision" in str(close_e).lower(): + self._call_with_retry(self.client.place_market_order, trading_ticker, round(-qty, 2)) + else: + raise close_e break except Exception as e: logger.error(f"Failed to flatten position: {e}")