fix: improve order resilience with precision and minimum quantity fallbacks, and fix rate-limiting on account fetch

This commit is contained in:
pie
2026-05-14 16:36:40 +01:00
parent fc9bb34d6b
commit 9e26623fc7
2 changed files with 70 additions and 23 deletions
+12 -4
View File
@@ -113,7 +113,8 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz, num_tickers):
params['ticker'] = t212_ticker params['ticker'] = t212_ticker
# Anti-thundering-herd: Random jitter to prevent 429s from parallel threads # 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 # Fetch Account Balance to calculate risk with backoff
for attempt in range(3): for attempt in range(3):
@@ -129,8 +130,9 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz, num_tickers):
break break
except Exception as e: except Exception as e:
if '429' in str(e): if '429' in str(e):
logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying...") wait_time = (attempt + 1) * 5 + random.uniform(1, 3)
time.sleep(2**(attempt+1)) logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {wait_time:.1f}s...")
time.sleep(wait_time)
else: else:
logger.error(f"Failed to fetch account info: {e}") logger.error(f"Failed to fetch account info: {e}")
break break
@@ -270,4 +272,10 @@ def main():
flush_logs() flush_logs()
if __name__ == "__main__": 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.")
+58 -19
View File
@@ -2,6 +2,7 @@ import time
import logging import logging
import os import os
import random import random
import json
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from src.api.client import Trading212Client from src.api.client import Trading212Client
from src.strategy.inverse_mapping import INVERSE_TICKER_MAP, LEVERAGE_MAP 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...") logger.warning(f"Rate limited. Retrying in {wait:.1f}s...")
time.sleep(wait) time.sleep(wait)
elif '400' in str(e) or '403' in str(e): 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: if hasattr(e, 'response') and e.response is not None:
logger.error(f"API Error Body: {e.response.text}") logger.error(f"API Error Body: {e.response.text}")
raise e raise e
@@ -70,9 +72,10 @@ class ExecutionManager:
approx_price = params.get('current_price', params['entry_price']) approx_price = params.get('current_price', params['entry_price'])
# 2. Position Sizing
if self.is_etp: if self.is_etp:
quantity = 1.0 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: else:
stop_loss = round(params['stop_loss'], 2) stop_loss = round(params['stop_loss'], 2)
risk_per_share = abs(approx_price - stop_loss) risk_per_share = abs(approx_price - stop_loss)
@@ -88,29 +91,49 @@ class ExecutionManager:
self.current_quantity = quantity self.current_quantity = quantity
trade_quantity = -quantity if direction == "SELL" else 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"Attempting {direction} market order for {ticker} (Qty: {quantity})...")
logger.info(f"Placing immediate {direction} market order for {ticker}...")
# 3. Execution with Smart Retry for Common Broker Errors
try: try:
order = self._call_with_retry(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') self.current_order_id = order.get('id')
logger.info(f"Market order placed successfully. ID: {self.current_order_id}") logger.info(f"Market order placed successfully. ID: {self.current_order_id}")
return True return True
except Exception as e: except Exception as e:
# Handle Quantity Precision Error if hasattr(e, 'response') and e.response is not None:
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: try:
trade_quantity = round(trade_quantity, 2) err_data = e.response.json()
self.current_quantity = abs(trade_quantity) err_type = err_data.get('type', '')
order = self._call_with_retry(self.client.place_market_order, ticker, trade_quantity) err_detail = err_data.get('detail', '')
self.current_order_id = order.get('id')
logger.info(f"Market order (retry) placed. ID: {self.current_order_id}") # Error A: Quantity Precision Mismatch
return True 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: 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 return False
def _is_ticker_in_portfolio(self, ticker: str) -> bool: def _is_ticker_in_portfolio(self, ticker: str) -> bool:
@@ -179,11 +202,21 @@ class ExecutionManager:
try: try:
logger.info(f"Hybrid Mode: Placing Broker SL for {ticker} @ {sl_price}. Monitoring TP @ {tp_price} manually.") 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") # Use retry with possible precision fix for SL too
self.sl_order_id = sl_order.get('id') 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 return True
except Exception as e: 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 return False
def check_exit_status(self) -> tuple[bool, str, float]: def check_exit_status(self) -> tuple[bool, str, float]:
@@ -225,7 +258,6 @@ class ExecutionManager:
if "404" in str(e): if "404" in str(e):
if not self._is_ticker_in_portfolio(ticker): if not self._is_ticker_in_portfolio(ticker):
self.is_in_position = False 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)) fallback_price = float(self.params.get('final_sl', 0.0))
return True, "SL Hit (Broker)", fallback_price return True, "SL Hit (Broker)", fallback_price
else: else:
@@ -257,7 +289,14 @@ class ExecutionManager:
qty = float(pos.get('quantity', 0)) qty = float(pos.get('quantity', 0))
exit_price = float(pos.get('currentPrice', 0.0)) exit_price = float(pos.get('currentPrice', 0.0))
if qty != 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 break
except Exception as e: except Exception as e:
logger.error(f"Failed to flatten position: {e}") logger.error(f"Failed to flatten position: {e}")