fix: improve order resilience with precision and minimum quantity fallbacks, and fix rate-limiting on account fetch
This commit is contained in:
@@ -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
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user