import asyncio
import math
import re
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal
from typing import Dict, List

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from loguru import logger

import optrabot.config as optrabotcfg
import optrabot.symbolinfo as symbolinfo
from optrabot.broker.brokerconnector import (BrokerConnector, ChartInterval,
                                             OHLCBar, SymbolData)
from optrabot.broker.optionpricedata import (OptionStrikeData,
                                             OptionStrikePriceData)
from optrabot.broker.order import Execution, OptionRight
from optrabot.broker.order import Order as GenericOrder
from optrabot.broker.order import OrderAction
from optrabot.broker.order import OrderStatus as GenericOrderStatus
from optrabot.broker.order import PriceEffect
from optrabot.broker.tradier import (Account, ExpirationType, OptionChain,
                                     Quote, Session, get_option_symbols,
                                     get_quotes)
from optrabot.broker.tradier.markets import get_history, get_timesales
from optrabot.broker.tradier.options import OptionType
from optrabot.broker.tradier.orders import (NewOrderLeg, OrderDuration,
                                            OrderSide, OrderStatus, OrderType,
                                            a_cancel_order, a_get_orders,
                                            a_modify_order,
                                            a_place_multileg_order,
                                            a_place_option_order)
from optrabot.exceptions.orderexceptions import (PlaceOrderException,
                                                 PrepareOrderException)
from optrabot.managedtrade import ManagedTrade
from optrabot.models import Account as ModelAccount
from optrabot.optionhelper import OptionHelper
from optrabot.tradetemplate.templatefactory import Template


@dataclass
class TradierSymbolData(SymbolData):
	def __init__(self) -> None:
		super().__init__()
		self.tradier_symbol: str = None
		self.chain: OptionChain = None

class TradierConnector(BrokerConnector):
	"""
	Broker Connector for Tradier Brokerage
	"""
	# Maximum symbols per request to avoid URL length limit (414 error)
	_MAX_SYMBOLS_PER_REQUEST = 50
	# Tradier Rate Limit: 120 requests per minute = 2 per second
	# We use a safe margin to avoid hitting the limit
	_MAX_REQUESTS_PER_MINUTE = 100  # Safe limit below 120
	_MIN_POLL_INTERVAL = 3  # Minimum seconds between polls
	_MAX_POLL_INTERVAL = 15  # Maximum seconds between polls (to maintain responsiveness)

	def __init__(self) -> None:
		super().__init__()
		self.id = 'TRADIER'
		self.broker = 'Tradier'
		self._sandbox = False
		self._quote_data_symbols: List[str] = []		# Underlying symbols for quote polling via REST API
		self._option_data_symbols: List[str] = []		# Option symbols for quote polling via REST API (with greeks)
		self._backgroundScheduler: AsyncIOScheduler = None
		self._initialize()
		self._tradier_accounts: List[Account] = []
		self._symbol_reverse_lookup: Dict[str, str] = {}		# maps Tradier symbol to generic symbol
		self._option_symbol_2_strike: Dict[str, Decimal] = {}	# maps Tradier option symbol to strike price
		self._orders: List[GenericOrder] = []		# List of placed orders
		# Dynamic poll interval tracking
		self._poll_duration_avg: float = 0.0		# Exponential moving average of poll duration
		self._poll_duration_alpha: float = 0.3	# EMA smoothing factor (higher = more responsive)
		self._current_poll_interval: int = self._MIN_POLL_INTERVAL
	
	async def adjustOrder(self, managed_trade: ManagedTrade, order: GenericOrder, price: float) -> bool:
		"""
		Adjusts the given order with the given new price.
		
		Tradier API does not support modifying multileg orders directly.
		For multileg orders, we use a cancel-replace strategy: cancel the old order
		and place a new one with the updated price.
		
		For single-leg orders, we first try the modify API, and fall back to
		cancel-replace if that fails.
		"""
		if order.status == GenericOrderStatus.FILLED:
			logger.info(f'Order {order.broker_order_id} is already filled. Adjustment not required.')
			return True
		
		order_id = order.brokerSpecific.get('tradier_order_id')
		account_number = order.brokerSpecific.get('account_number')
		
		if not order_id or not account_number:
			logger.error(f'Order {order.broker_order_id} not prepared for adjustment. Cannot adjust order.')
			return False
		
		is_multileg = len(order.legs) > 1
		
		# For multileg orders, Tradier doesn't support modify - use cancel-replace directly
		if is_multileg:
			logger.debug(f'Multileg order {order_id} - using cancel-replace strategy for price adjustment to {price}')
			return await self._cancel_and_replace_order(managed_trade, order, price)
		
		# For single-leg orders, try modify first
		try:
			logger.debug(f'Adjusting Tradier order {order_id} to price {price}')
			order.price = price
			
			await a_modify_order(
				self._session,
				account_number=account_number,
				order_id=order_id,
				price=Decimal(str(abs(price))),
			)
			
			logger.debug(f'Tradier order {order_id} adjusted to price {price}')
			return True
			
		except Exception as exc:
			logger.warning(f'Modify order failed: {exc}. Trying cancel-replace strategy.')
			return await self._cancel_and_replace_order(managed_trade, order, price)
	
	async def _cancel_and_replace_order(self, managed_trade: ManagedTrade, order: GenericOrder, new_price: float) -> bool:
		"""
		Cancels the existing order and places a new one with the updated price.
		This is the fallback strategy when direct order modification is not supported.
		"""
		order_id = order.brokerSpecific.get('tradier_order_id')
		
		# Set flag to prevent TradeManager from treating the cancelled status as a real cancellation
		managed_trade.pending_cancel_replace = True
		
		try:
			# Step 1: Cancel the existing order
			logger.debug(f'Cancel-replace: Cancelling order {order_id}')
			await self.cancel_order(order)
			
			# Give Tradier a moment to process the cancellation
			await asyncio.sleep(0.3)
			
			# Step 2: Update the price and place a new order
			order.price = new_price
			order.status = None  # Reset status for the new order
			order.broker_order_id = ''
			order.brokerSpecific.pop('tradier_order_id', None)
			
			logger.debug(f'Cancel-replace: Placing new order with price {new_price}')
			await self.placeOrder(managed_trade, order)
			
			logger.info(f'Cancel-replace successful. New order ID: {order.broker_order_id}')
			managed_trade.pending_cancel_replace = False
			return True
			
		except Exception as exc:
			logger.error(f'Cancel-replace failed: {exc}')
			managed_trade.pending_cancel_replace = False
			return False

	async def cancel_order(self, order: GenericOrder) -> None:
		"""
		Cancels the given order
		"""
		await super().cancel_order(order)
		
		order_id = order.brokerSpecific.get('tradier_order_id')
		account_number = order.brokerSpecific.get('account_number')
		
		if not order_id or not account_number:
			logger.error(f'Order {order.broker_order_id} not prepared for cancellation.')
			raise ValueError(f'Order {order.broker_order_id} missing Tradier order details')
		
		logger.debug(f'Cancelling Tradier order {order_id}')
		try:
			await a_cancel_order(self._session, account_number, order_id)
		except Exception as exc:
			logger.error(f'Error cancelling Tradier order {order_id}: {exc}')
			raise

	async def connect(self) -> None:
		await super().connect()
		try:
			self._session = Session(self._api_token, sandbox=self._sandbox)
			await self.set_trading_enabled(True, 'Broker connected')
			
			# Initialize and start background scheduler for quote polling
			self._backgroundScheduler = AsyncIOScheduler()
			self._backgroundScheduler.start()
			
			# Start order status polling job (every 2 seconds when we have open orders)
			self._backgroundScheduler.add_job(
				self._poll_order_status,
				'interval',
				seconds=2,
				id='order_polling',
				max_instances=1,
				coalesce=True,
				misfire_grace_time=None
			)
			
			self._emitConnectedEvent()
		except Exception as exc:
			logger.error(f'Error connecting to Tradier: {exc}')
			self._emitConnectFailedEvent()

	async def disconnect(self) -> None:
		await super().disconnect()
		
		# Stop background scheduler first and wait for any running jobs to complete
		if self._backgroundScheduler and self._backgroundScheduler.running:
			self._backgroundScheduler.remove_all_jobs()
			self._backgroundScheduler.shutdown(wait=False)
			self._backgroundScheduler = None
			# Give running jobs time to complete/cancel gracefully
			await asyncio.sleep(0.2)

		# Close session after all jobs are stopped
		if self._session:
			await self._session.a_close()
			self._session = None

	async def eod_settlement_tasks(self) -> None:
		"""
		Perform End of Day settlement tasks
		"""
		await super().eod_settlement_tasks()

	def getAccounts(self) -> List[ModelAccount]:
		if len(self._managedAccounts) == 0 and self.isConnected():
			if len(self._tradier_accounts) == 0:
				self._tradier_accounts = Account.get_accounts(self._session)

				for tradier_account in self._tradier_accounts:
					model_account = ModelAccount(
						id=tradier_account.account_number,
						name=tradier_account.account_number,
						pdt=tradier_account.day_trader,
						broker=self.id
					)
					self._managedAccounts.append(model_account)

		return self._managedAccounts

	def getFillPrice(self, order: GenericOrder) -> float:
		""" 
		Returns the fill price of the given order if it is filled
		"""
		try:
			tradier_order_id = order.brokerSpecific.get('tradier_order_id')
			if tradier_order_id and order.averageFillPrice:
				return abs(float(order.averageFillPrice))
			elif order.status == GenericOrderStatus.FILLED and order.averageFillPrice:
				return abs(float(order.averageFillPrice))
			else:
				return 0
		except Exception as exc:
			logger.error(f'No fill price available for order {order}: {exc}')
			return 0
	
	def getLastPrice(self, symbol: str) -> float:
		""" 
		Returns the last price of the given symbol
		"""
		try:
			symbol_data = self._symbolData[symbol]
			return symbol_data.lastPrice
		except KeyError:
			logger.error(f'No last price available for symbol {symbol}')
			return 0
	
	async def get_price_history(
		self,
		symbol: str,
		interval: ChartInterval = ChartInterval.ONE_DAY,
		start: datetime = None,
		end: datetime = None,
		bars: int = 100
	) -> List[OHLCBar]:
		"""
		Returns historical price data (OHLC bars) for the given symbol using the Tradier API.
		
		Uses the history endpoint for daily/weekly/monthly bars and the timesales
		endpoint for intraday bars (1min, 5min, 15min).
		
		Args:
			symbol: The trading symbol (e.g., 'SPX', 'SPY')
			interval: Bar interval (default: ChartInterval.ONE_DAY)
			start: Start datetime (optional). If not provided, calculated from bars.
			end: End datetime (optional, defaults to now)
			bars: Number of bars to return if start not specified (default: 100)
			
		Returns:
			List of OHLCBar objects sorted by timestamp ascending.
		"""
		if not self.isConnected():
			logger.warning('Tradier not connected for price history request')
			return []
		
		# Get the Tradier symbol
		if symbol not in self._symbolData:
			logger.warning(f'Symbol {symbol} not found in symbol data')
			return []
		
		symbol_data: TradierSymbolData = self._symbolData[symbol]
		tradier_symbol = symbol_data.tradier_symbol
		
		try:
			# Check if this is an intraday interval
			intraday_intervals = {
				ChartInterval.ONE_MINUTE: '1min',
				ChartInterval.FIVE_MINUTES: '5min',
				ChartInterval.FIFTEEN_MINUTES: '15min',
				ChartInterval.ONE_HOUR: '15min',  # Use 15min as closest available
			}
			
			if interval in intraday_intervals:
				# Use timesales endpoint for intraday data
				return await self._get_intraday_history(
					tradier_symbol, 
					intraday_intervals[interval], 
					start, 
					end, 
					bars
				)
			else:
				# Use history endpoint for daily/weekly/monthly data
				return await self._get_daily_history(
					tradier_symbol, 
					start, 
					end, 
					bars
				)
			
		except Exception as exc:
			logger.error(f'Error fetching price history for {symbol}: {exc}')
			return []
	
	async def _get_intraday_history(
		self,
		tradier_symbol: str,
		interval: str,
		start: datetime,
		end: datetime,
		bars: int
	) -> List[OHLCBar]:
		"""
		Get intraday price history using the timesales endpoint.
		Note: Timesales data is limited to about 20 trading days.
		"""
		from datetime import timedelta

		# Calculate start time if not provided
		if start is None:
			# For intraday, calculate based on trading hours
			# Approximately 78 5-min bars per day (6.5 hours * 12 bars/hour)
			minutes_per_bar = {'1min': 1, '5min': 5, '15min': 15}.get(interval, 5)
			total_minutes = bars * minutes_per_bar
			# Add buffer for non-trading hours
			days_needed = (total_minutes // 390) + 3  # 390 trading minutes per day
			start = datetime.now() - timedelta(days=days_needed)
		
		if end is None:
			end = datetime.now()
		
		# Fetch timesales data from Tradier
		timesales_bars = await get_timesales(
			self._session,
			tradier_symbol,
			interval=interval,
			start=start,
			end=end,
			session_filter='all',
		)
		
		# Convert to OHLCBar format
		ohlc_bars = []
		for bar in timesales_bars:
			if bar.time and bar.open and bar.high and bar.low and bar.close:
				ohlc_bar = OHLCBar(
					timestamp=bar.time,
					open=float(bar.open),
					high=float(bar.high),
					low=float(bar.low),
					close=float(bar.close),
					volume=bar.volume,
				)
				ohlc_bars.append(ohlc_bar)
		
		# Limit to requested number of bars (take the most recent ones)
		if len(ohlc_bars) > bars:
			ohlc_bars = ohlc_bars[-bars:]
		
		#logger.debug(f'Retrieved {len(ohlc_bars)} intraday bars for {tradier_symbol}')
		return ohlc_bars
	
	async def _get_daily_history(
		self,
		tradier_symbol: str,
		start: datetime,
		end: datetime,
		bars: int
	) -> List[OHLCBar]:
		"""
		Get daily price history using the history endpoint.
		"""
		from datetime import timedelta

		# Calculate start date if not provided
		if start is None:
			# Add extra days to account for weekends/holidays
			days_needed = int(bars * 1.5)
			start = datetime.now() - timedelta(days=days_needed)
		
		# Convert datetime to date for Tradier API
		start_date = start.date() if isinstance(start, datetime) else start
		end_date = end.date() if end and isinstance(end, datetime) else (end or date.today())
		
		# Fetch history from Tradier
		history_bars = await get_history(
			self._session,
			tradier_symbol,
			interval='daily',
			start=start_date,
			end=end_date,
		)
		
		# Convert to OHLCBar format
		ohlc_bars = []
		for bar in history_bars:
			# Convert date to datetime if needed
			if isinstance(bar.date, date) and not isinstance(bar.date, datetime):
				bar_datetime = datetime.combine(bar.date, datetime.min.time())
			else:
				bar_datetime = bar.date
			
			ohlc_bar = OHLCBar(
				timestamp=bar_datetime,
				open=float(bar.open),
				high=float(bar.high),
				low=float(bar.low),
				close=float(bar.close),
				volume=bar.volume,
			)
			ohlc_bars.append(ohlc_bar)
		
		# Limit to requested number of bars (take the most recent ones)
		if len(ohlc_bars) > bars:
			ohlc_bars = ohlc_bars[-bars:]
		
		#logger.debug(f'Retrieved {len(ohlc_bars)} daily bars for {tradier_symbol}')
		return ohlc_bars
	
	def get_option_strike_data(self, symbol: str, expiration: date) -> OptionStrikeData:
		""" 
		Returns the option strike data for the given symbol and expiration. It is including
		prices and greeks.
		"""
		symbol_data = self._symbolData[symbol]
		try:
			return symbol_data.optionPriceData[expiration]
		except KeyError as keyexc:
			raise ValueError(f'No option strike data for symbol {symbol} and expiration {expiration} found!') from keyexc

	def get_option_strike_price_data(self, symbol: str, expiration: date, strike: float) -> OptionStrikePriceData:
		""" 
		Returns the option strike price data for the given symbol, expiration date, strike price and right.
		Returns None if no price data is available for the expiration date or strike.
		"""
		if symbol not in self._symbolData:
			return None
		symbol_data = self._symbolData[symbol]
		try:
			option_strike_data = symbol_data.optionPriceData[expiration]
			if strike in option_strike_data.strikeData:
				return option_strike_data.strikeData[strike]
			else:
				return None
		except KeyError:
			return None

	def get_strike_by_delta(self, symbol: str, right: str, delta: int) -> float:
		"""
		Returns the strike price based on the given delta based on the buffered option price data.
		
		Args:
			symbol: The underlying symbol (e.g., 'SPX')
			right: The option right ('Call' or 'Put')
			delta: The target delta value (0-100)
			
		Returns:
			The strike price closest to the target delta
			
		Raises:
			ValueError: If no strike price is found for the given delta
		"""
		symbol_data = self._symbolData[symbol]
		current_date = date.today()
		
		try:
			option_price_data: OptionStrikeData = symbol_data.optionPriceData[current_date]
		except KeyError as keyexc:
			raise ValueError(f'No option price data for symbol {symbol} on {current_date}!') from keyexc
		
		previous_delta = 0
		previous_strike = 0
		reverse = True if right == OptionRight.PUT else False
		sorted_strikes = dict(sorted(option_price_data.strikeData.items(), reverse=reverse))
		
		for strike, price_data in sorted_strikes.items():
			if right == OptionRight.PUT:
				if price_data.putDelta is None:
					continue
				adjusted_delta = price_data.putDelta * -100
			else:
				if price_data.callDelta is None:
					continue
				adjusted_delta = price_data.callDelta * 100
			
			if adjusted_delta <= delta:
				if OptionHelper.closest_number(delta, previous_delta, adjusted_delta) == adjusted_delta:
					return strike
				else:
					return previous_strike
			previous_delta = adjusted_delta
			previous_strike = strike

		raise ValueError(f'No strike price found for delta {delta} in symbol {symbol}!')
	
	def get_strike_by_price(self, symbol: str, right: str, price: float) -> float:
		""" 
		Returns the strike price based on the given premium price based on the buffered option price data.
		
		Args:
			symbol: The underlying symbol (e.g., 'SPX')
			right: The option right ('Call' or 'Put')
			price: The target premium price
			
		Returns:
			The strike price closest to the target premium price
			
		Raises:
			ValueError: If no strike price is found for the given price
		"""
		symbol_data = self._symbolData[symbol]
		current_date = date.today()
		
		try:
			option_price_data: OptionStrikeData = symbol_data.optionPriceData[current_date]
		except KeyError as keyexc:
			raise ValueError(f'No option price data for symbol {symbol} on {current_date}!') from keyexc
		
		previous_price = 0
		previous_strike = 0
		reverse = True if right == OptionRight.PUT else False
		sorted_strikes = dict(sorted(option_price_data.strikeData.items(), reverse=reverse))
		
		for strike, price_data in sorted_strikes.items():
			if right == OptionRight.PUT:
				current_strike_price = price_data.getPutMidPrice()
				if current_strike_price is None:
					continue
			elif right == OptionRight.CALL:
				current_strike_price = price_data.getCallMidPrice()
				if current_strike_price is None:
					continue
			else:
				continue
			
			if current_strike_price > 0 and current_strike_price <= price:
				if OptionHelper.closest_number(price, previous_price, current_strike_price) == current_strike_price:
					return strike
				else:
					return previous_strike
			previous_price = current_strike_price
			previous_strike = strike
		
		raise ValueError(f'No strike price found for price {price} in symbol {symbol}!')
	
	def isConnected(self) -> bool:
			return self._session is not None
	
	async def placeOrder(self, managed_trade: ManagedTrade, order: GenericOrder, parent_order: GenericOrder = None) -> None:
		""" 
		Places the given order for a managed account via the broker connection.
		
		Raises:
			PlaceOrderException: If the order placement fails with a specific reason.
		"""
		account_number = managed_trade.template.account
		
		try:
			# Check if this is a multileg order
			if len(order.legs) > 1:
				# Multileg order
				legs = []
				for leg in order.legs:
					tradier_side = self._map_order_side(order.action, leg.action)
					tradier_symbol = leg.brokerSpecific.get('tradier_symbol')
					if not tradier_symbol:
						raise PlaceOrderException(f'No Tradier symbol for leg {leg}', order=order)
					
					new_leg = NewOrderLeg(
						option_symbol=tradier_symbol,
						side=tradier_side,
						quantity=leg.quantity * order.quantity
					)
					legs.append(new_leg)
				
				# Determine order type based on price effect
				tradier_order_type = self._get_tradier_order_type_multileg(order)
				
				# Log order details for debugging
				logger.debug(f'Placing multileg order: symbol={order.symbol}, type={tradier_order_type}, '
							f'price={order.price}, price_effect={order.price_effect}')
				for i, leg in enumerate(legs):
					logger.debug(f'  Leg {i}: {leg.option_symbol} {leg.side.value} qty={leg.quantity}')
				
				response = await a_place_multileg_order(
					self._session,
					account_number=account_number,
					symbol=order.symbol,
					legs=legs,
					order_type=tradier_order_type,
					duration=OrderDuration.DAY,
					price=Decimal(str(abs(order.price))) if order.price else None,
				)
			else:
				# Single leg order
				leg = order.legs[0]
				tradier_side = self._map_order_side(order.action, leg.action)
				tradier_symbol = leg.brokerSpecific.get('tradier_symbol')
				if not tradier_symbol:
					raise PlaceOrderException(f'No Tradier symbol for leg {leg}', order=order)
				
				tradier_order_type = self._get_tradier_order_type_single(order)
				
				response = await a_place_option_order(
					self._session,
					account_number=account_number,
					symbol=order.symbol,
					option_symbol=tradier_symbol,
					side=tradier_side,
					quantity=leg.quantity * order.quantity,
					order_type=tradier_order_type,
					duration=OrderDuration.DAY,
					price=Decimal(str(abs(order.price))) if order.price and tradier_order_type in [OrderType.LIMIT, OrderType.STOP_LIMIT] else None,
					stop=Decimal(str(abs(order.price))) if order.price and tradier_order_type in [OrderType.STOP, OrderType.STOP_LIMIT] else None,
				)
			
			# Store order response for later reference
			order.broker_order_id = str(response.id)
			order.brokerSpecific['tradier_order_id'] = response.id
			order.brokerSpecific['account_number'] = account_number
			self._orders.append(order)
			
			logger.debug(f'Tradier order {response.id} placed successfully')
			
		except Exception as exc:
			logger.error(f'Error placing Tradier order: {exc}')
			raise PlaceOrderException(f'{exc}', order=order) from exc
	
	async def place_complex_order(self, take_profit_order: GenericOrder, stop_loss_order: GenericOrder, template: Template) -> bool:
		"""
		Places the Take Profit and Stop Loss Order as complex order.
		Note: Tradier does not support OCO orders natively, so we place them as separate orders.
		"""
		logger.warning('Tradier does not support OCO orders natively. Placing as separate orders.')
		try:
			# Place take profit order
			managed_trade = ManagedTrade()
			managed_trade.template = template
			await self.placeOrder(managed_trade, take_profit_order)
			
			# Place stop loss order
			await self.placeOrder(managed_trade, stop_loss_order)
			
			return True
		except PlaceOrderException as exc:
			logger.error(f'Error placing complex order: {exc}')
			return False
	
	async def prepareOrder(self, order: GenericOrder, need_valid_price_data: bool = True) -> None:
		"""
		Prepares the given order for execution

		Args:
			order: The order to prepare
			need_valid_price_data: If True, validates price data availability and freshness.
				If False, creates order legs without price validation (e.g., for trade recovery)

		Raises:
			PrepareOrderException: If the order preparation fails with a specific reason.
		"""
		symbol_data: TradierSymbolData = self._symbolData[order.symbol]
		
		for leg in order.legs:
			if need_valid_price_data:
				# Normal order flow: use price data with validation
				try:
					option_price_data = symbol_data.optionPriceData[leg.expiration]
				except KeyError as keyexc:
					raise PrepareOrderException(f'No option price data for expiration date {leg.expiration} available!', order) from keyexc
				
				try:
					price_data: OptionStrikePriceData = option_price_data.strikeData[leg.strike]
					if not price_data.is_outdated():
						if leg.right == OptionRight.CALL:
							leg.askPrice = float(price_data.callAsk) if price_data.callAsk else 0
							leg.bidPrice = float(price_data.callBid) if price_data.callBid else 0
							leg.brokerSpecific['tradier_symbol'] = price_data.brokerSpecific.get('call_option')
						elif leg.right == OptionRight.PUT:
							leg.askPrice = float(price_data.putAsk) if price_data.putAsk else 0
							leg.bidPrice = float(price_data.putBid) if price_data.putBid else 0
							leg.brokerSpecific['tradier_symbol'] = price_data.brokerSpecific.get('put_option')
					else:
						raise PrepareOrderException(f'Price data for strike {leg.strike} is outdated or not available!', order)
				except KeyError as keyexc:
					raise PrepareOrderException(f'No option price data for strike {leg.strike} available!', order) from keyexc
				except Exception as exc:
					raise PrepareOrderException(f'Error preparing order: {exc}', order) from exc
			else:
				# Trade recovery flow: create option instrument without price data validation
				try:
					if symbol_data.chain is None:
						raise PrepareOrderException(
							f'No option chain available for {order.symbol} - symbol data may not be initialized',
							order
						)
					
					# Find the specific option in the cached chain
					found = False
					for expiration in symbol_data.chain.expirations:
						if expiration.expiration_date == leg.expiration:
							# Get option symbols for this expiration
							strike_data = await get_option_symbols(self._session, symbol_data.tradier_symbol, leg.expiration)
							for strike_price, strike_info in strike_data.items():
								if abs(float(strike_price) - leg.strike) < 0.01:
									if leg.right == OptionRight.CALL and strike_info.call_symbol:
										leg.brokerSpecific['tradier_symbol'] = strike_info.call_symbol
										leg.askPrice = 0
										leg.bidPrice = 0
										found = True
										break
									elif leg.right == OptionRight.PUT and strike_info.put_symbol:
										leg.brokerSpecific['tradier_symbol'] = strike_info.put_symbol
										leg.askPrice = 0
										leg.bidPrice = 0
										found = True
										break
							if found:
								break
					
					if not found:
						raise PrepareOrderException(
							f'Could not find option instrument for {order.symbol} {leg.expiration} {leg.strike} {leg.right}',
							order
						)
				except PrepareOrderException:
					raise
				except Exception as e:
					raise PrepareOrderException(f'Error creating option instrument for trade recovery: {e}', order) from e
		
		order.determine_price_effect()
		return True
	
	async def requestTickerData(self, symbols: List[str]) -> None:
		""" 
		Request ticker data for the given symbols and their options.
		All data is retrieved via REST API polling (no streaming).
		"""
		for symbol in symbols:
			match symbol:
				case 'SPX':
					symbol_data = TradierSymbolData()
					symbol_data.symbol = symbol
					symbol_data.tradier_symbol = 'SPX'
					self._quote_data_symbols.append(symbol_data.tradier_symbol)
					self._symbolData[symbol] = symbol_data
					self._symbol_reverse_lookup[symbol_data.tradier_symbol] = symbol
				case 'VIX':
					symbol_data = TradierSymbolData()
					symbol_data.symbol = symbol
					symbol_data.tradier_symbol = 'VIX'
					self._quote_data_symbols.append(symbol_data.tradier_symbol)
					symbol_data.trade_options = False  # No options for VIX
					self._symbolData[symbol] = symbol_data
					self._symbol_reverse_lookup[symbol_data.tradier_symbol] = symbol
				case _:
					logger.error(f'Symbol {symbol} currently not supported by Tradier Connector!')
					continue

		current_date = date.today()
		for item in self._symbolData.values():
			symbol_data: TradierSymbolData = item
			symbol_information = symbolinfo.symbol_infos[symbol_data.symbol]
			desired_chain = None
			if symbol_data.trade_options:
				chains = OptionChain.get(self._session, symbol=symbol_data.tradier_symbol)
				if symbol_information.option_symbol_suffix == 'W':
					desired_chain = next((chain for chain in chains if chain.expiration_type == ExpirationType.WEEKLYS), None)
				else:
					desired_chain = next((chain for chain in chains if chain.expiration_type == ExpirationType.STANDARD), None)
				
				if desired_chain is None:
					logger.error(f'No desired option chain found for symbol {symbol_data.symbol} with expiration type {symbol_information.option_symbol_suffix}')
					continue

				symbol_data.chain = desired_chain
				got_strikes = False
				for expiration in desired_chain.expirations:
					if expiration.expiration_date < current_date:
							continue
					symbol_data.expirations.append(expiration.expiration_date)
					if not got_strikes:
						for strike in expiration.strikes:
							symbol_data.strikes.append(float(strike))
							got_strikes = True
					
				if current_date > symbol_data.expirations[0]:
					logger.warning(f'There are no {symbol_data.symbol} options expiring today!')

		# Start quote polling job if we have symbols to poll
		if self._backgroundScheduler and (len(self._quote_data_symbols) > 0 or len(self._option_data_symbols) > 0):
			# Calculate poll interval based on number of requests needed
			poll_interval = self._calculate_poll_interval()
			self._current_poll_interval = poll_interval  # Store initial interval
			self._poll_duration_avg = 0.0  # Reset average for new subscription
			
			# Remove existing job if any
			existing_job = self._backgroundScheduler.get_job('quote_polling')
			if existing_job:
				self._backgroundScheduler.remove_job('quote_polling')
			self._backgroundScheduler.add_job(
				self._poll_quotes,
				'interval',
				seconds=poll_interval,
				id='quote_polling',
				max_instances=1,  # Prevent overlapping polls
				coalesce=True,    # Combine missed runs into one
				misfire_grace_time=None
			)
			logger.info(f'Started quote polling every {poll_interval}s for underlyings: {self._quote_data_symbols}, options: {len(self._option_data_symbols)} symbols')
	
	async def unsubscribe_ticker_data(self) -> None:
		"""
		Unsubscribe from ticker data - including base symbols and all options
		"""
		await super().unsubscribe_ticker_data()
		
		# Clear all polled symbols
		self._quote_data_symbols.clear()
		self._option_data_symbols.clear()
		
		# Stop polling job
		if self._backgroundScheduler:
			existing_job = self._backgroundScheduler.get_job('quote_polling')
			if existing_job:
				self._backgroundScheduler.remove_job('quote_polling')

	def _calculate_poll_interval(self) -> int:
		"""
		Calculate the poll interval based on the number of API requests needed per poll.
		
		Tradier Rate Limit: 120 requests per minute.
		We calculate based on:
		- 1 request for underlying quotes
		- N requests for option quotes (batched by _MAX_SYMBOLS_PER_REQUEST)
		
		Returns the interval in seconds that ensures we stay under the rate limit.
		"""
		# Calculate number of requests per poll
		underlying_requests = 1 if len(self._quote_data_symbols) > 0 else 0
		option_batches = math.ceil(len(self._option_data_symbols) / self._MAX_SYMBOLS_PER_REQUEST) if len(self._option_data_symbols) > 0 else 0
		requests_per_poll = underlying_requests + option_batches
		
		if requests_per_poll == 0:
			return self._MIN_POLL_INTERVAL
		
		# Calculate minimum interval to stay under rate limit
		# requests_per_poll * (60 / interval) <= _MAX_REQUESTS_PER_MINUTE
		# interval >= requests_per_poll * 60 / _MAX_REQUESTS_PER_MINUTE
		min_interval_for_rate_limit = math.ceil(requests_per_poll * 60 / self._MAX_REQUESTS_PER_MINUTE)
		
		# Use at least _MIN_POLL_INTERVAL
		poll_interval = max(self._MIN_POLL_INTERVAL, min_interval_for_rate_limit)
		
		logger.debug(f'Poll interval calculation: {requests_per_poll} requests/poll, interval={poll_interval}s')
		return poll_interval

	def _update_poll_interval(self, duration: float) -> None:
		"""
		Updates the poll interval based on measured request duration.
		Uses exponential moving average (EMA) to smooth out variations.
		
		The new interval is calculated as: max(MIN_INTERVAL, avg_duration * 1.5)
		This ensures there's always buffer time between polls.
		
		:param duration: The duration of the last poll in seconds
		"""
		# Update EMA: new_avg = alpha * new_value + (1 - alpha) * old_avg
		if self._poll_duration_avg == 0:
			# First measurement - use it directly
			self._poll_duration_avg = duration
		else:
			self._poll_duration_avg = (
				self._poll_duration_alpha * duration + 
				(1 - self._poll_duration_alpha) * self._poll_duration_avg
			)
		
		# Calculate new interval: average duration * 1.5 for safety margin
		# Also respect rate limit calculation
		rate_limit_interval = self._calculate_poll_interval()
		duration_based_interval = math.ceil(self._poll_duration_avg * 1.5)
		
		new_interval = max(
			self._MIN_POLL_INTERVAL,
			rate_limit_interval,
			duration_based_interval
		)
		
		# Cap at maximum to maintain responsiveness even during slow periods
		new_interval = min(new_interval, self._MAX_POLL_INTERVAL)
		
		# Only reschedule if interval changed significantly (more than 1 second)
		if abs(new_interval - self._current_poll_interval) >= 1:
			old_interval = self._current_poll_interval
			self._current_poll_interval = new_interval
			
			# Reschedule the job with new interval
			if self._backgroundScheduler:
				existing_job = self._backgroundScheduler.get_job('quote_polling')
				if existing_job:
					existing_job.reschedule(trigger='interval', seconds=new_interval)
					logger.info(f'Adjusted poll interval: {old_interval}s → {new_interval}s (avg duration: {self._poll_duration_avg:.1f}s)')
	
	def _initialize(self) -> None:
		"""
		Initializes the Tradier Connector from the configuration.
		"""
		if not optrabotcfg.appConfig:
			return
		
		config :optrabotcfg.Config = optrabotcfg.appConfig
		try:
			config.get('broker.tradier')
		except KeyError:
			logger.debug('No Tradier connection configured')
			return
		
		try:
			self._api_token = config.get('broker.tradier.api_token')
		except KeyError:
			logger.error('Tradier API token not configured')
			return
		
		try:
			self._sandbox = config.get('broker.tradier.sandbox')
		except KeyError:
			pass
		self._initialized = True

	async def _on_market_quote(self, quotes: List[Quote], is_option_quote: bool = False) -> None:
		"""
		Callback function when market quotes are received via REST polling.
		
		:param quotes: List of Quote objects
		:param is_option_quote: True if these are option quotes (with greeks)
		"""
		for quote in quotes:
			logger.trace(f'Received Tradier market quote: {quote}')
			
			if is_option_quote:
				# Process option quote with Greeks
				try:
					generic_symbol, option_type, expiration = self._get_option_infos(quote.symbol)
					strike = self._option_symbol_2_strike.get(quote.symbol)
					if strike is None:
						continue
					
					symbol_data = self._symbolData[generic_symbol]
					symbol_information = symbolinfo.symbol_infos[symbol_data.symbol]
					option_strike_data = symbol_data.optionPriceData[expiration]
					option_strike_price_data = option_strike_data.strikeData[strike]
					
					if option_type == OptionType.CALL:
						option_strike_price_data.callBid = float(quote.bid) if quote.bid else None
						option_strike_price_data.callAsk = float(quote.ask) if quote.ask else None
						if quote.greeks:
							option_strike_price_data.callDelta = float(quote.greeks.delta) if quote.greeks.delta else None
					elif option_type == OptionType.PUT:
						option_strike_price_data.putBid = float(quote.bid) if quote.bid else None
						option_strike_price_data.putAsk = float(quote.ask) if quote.ask else None
						if quote.greeks:
							option_strike_price_data.putDelta = float(quote.greeks.delta) if quote.greeks.delta else None
					
					option_strike_price_data.lastUpdated = datetime.now(symbol_information.timezone)
					self._last_option_price_update_time = option_strike_price_data.lastUpdated
				except Exception as exc:
					logger.error(f'Error processing option quote {quote.symbol}: {exc}')
			else:
				# Process underlying quote
				generic_symbol = self._symbol_reverse_lookup.get(quote.symbol)
				if generic_symbol and generic_symbol in self._symbolData:
					symbol_data: TradierSymbolData = self._symbolData[generic_symbol]
					# Skip if no last price available (can happen pre-market or during outages)
					if quote.last is None:
						continue
					symbol_data.lastPrice = float(quote.last)
				
					# Symbol is an underlying, update ATM strike
					atm_strike = OptionHelper.roundToStrikePrice(symbol_data.lastPrice)
					if symbol_data.lastAtmStrike != atm_strike:  # Check for missing Option Data only if ATM Strike has changed
						symbol_data.lastAtmStrike = atm_strike
						asyncio.create_task(self._request_missing_option_data(symbol_data, atm_strike))


	def _get_option_infos(self, tradier_symbol: str) -> tuple:
		"""
		Extracts the generic symbol and expiration date, strike and option side from the Tradier option symbol.
		If the option symbol information cannot be parsed as expected, a ValueError exception is raised.
		"""
		error = False
		pattern = r'^(?P<optionsymbol>[A-Z]+)(?P<expiration>[0-9]+)(?P<type>[CP])(?P<strike>[0-9]+)'
		compiled_pattern = re.compile(pattern)
		match = compiled_pattern.match(tradier_symbol)
		try:
			if match:
				option_symbol = match.group('optionsymbol')
				for _symbol, symbol_info in symbolinfo.symbol_infos.items():
					if symbol_info.symbol + symbol_info.option_symbol_suffix == option_symbol:
						generic_symbol = symbol_info.symbol
						break
				expiration_date = datetime.strptime(match.group('expiration'), '%y%m%d').date()
				option_type = OptionType.CALL if match.group('type') == 'C' else OptionType.PUT
		except IndexError:
			logger.error(f'Invalid option symbol {tradier_symbol}')
			error = True
		except ValueError:
			logger.error(f'Invalid option symbol {tradier_symbol}')
			error = True
		if generic_symbol is None or error:
			raise ValueError(f'Invalid option symbol {tradier_symbol}')
		return generic_symbol, option_type, expiration_date

	async def _poll_quotes(self) -> None:
		"""
		Polls quotes for underlyings and options via REST API.
		Called periodically by the background scheduler.
		Options are polled in batches to avoid URL length limits.
		
		Uses a timeout to prevent hanging when the Tradier API is unresponsive.
		Measures execution time to dynamically adjust the poll interval.
		"""
		# Check if we're still connected and scheduler is running
		if not self._session or not self._backgroundScheduler:
			return
		
		# OTB-247: Skip polling if no symbols are subscribed
		# This can happen after EOD settlement when all trades are closed
		if len(self._quote_data_symbols) == 0 and len(self._option_data_symbols) == 0:
			return
		
		start_time = asyncio.get_event_loop().time()
		
		# Use a 60 second timeout for the entire poll operation
		# This prevents the job from hanging indefinitely if the API is unresponsive
		try:
			async with asyncio.timeout(60):
				# Poll underlying quotes (without greeks)
				if len(self._quote_data_symbols) > 0:
					quotes = await get_quotes(self._session, self._quote_data_symbols, greeks=False)
					await self._on_market_quote(quotes, is_option_quote=False)
				
				# Poll option quotes in batches (with greeks for delta)
				if len(self._option_data_symbols) > 0:
					for i in range(0, len(self._option_data_symbols), self._MAX_SYMBOLS_PER_REQUEST):
						batch = self._option_data_symbols[i:i + self._MAX_SYMBOLS_PER_REQUEST]
						option_quotes = await get_quotes(self._session, batch, greeks=True)
						await self._on_market_quote(option_quotes, is_option_quote=True)
			
			# Measure duration and update average (only on success)
			duration = asyncio.get_event_loop().time() - start_time
			self._update_poll_interval(duration)
			
		except asyncio.TimeoutError:
			logger.warning('Quote polling timed out after 60 seconds - Tradier API may be unresponsive')
			# Don't update EMA on timeout - it would skew the average too much
			# The MAX_POLL_INTERVAL will cap the interval anyway
		except asyncio.CancelledError:
			# Gracefully handle cancellation during shutdown
			logger.debug('Quote polling cancelled during shutdown')
		except Exception as exc:
			# Provide more context for network errors (ConnectError often has empty message)
			error_details = str(exc) if str(exc) else 'No details available - possible network connectivity issue'
			logger.error(f'Error polling Tradier quotes: {type(exc).__name__}: {error_details}')
			# Don't update EMA on error - only successful polls should influence timing

	async def _request_missing_option_data(self, symbol_data: TradierSymbolData, atm_strike: float) -> None:
		"""
		Requests missing option price data for the given symbol and ATM strike.
		
		:param symbol_data: The symbol data object
		:param atm_strike: The current ATM strike price
		"""
		relevant_expirations = self._determine_relevant_expirations(symbol_data)
		strikes_of_interest = self._determine_strikes_of_interest(symbol_data, atm_strike)
		options_to_be_requested = []
		for relevant_expiration in relevant_expirations:
			# Optain saved options data for the expiration date
			try:
				option_strike_data = symbol_data.optionPriceData[relevant_expiration]
			except KeyError:
				option_strike_data = OptionStrikeData()
				symbol_data.optionPriceData[relevant_expiration] = option_strike_data
			
			for chain_at_expiration in symbol_data.chain.expirations:
				if chain_at_expiration.expiration_date >= relevant_expiration:
					break

			if chain_at_expiration is None or chain_at_expiration.expiration_date != relevant_expiration:
				logger.error(f'No options available for symbol {symbol_data.tradier_symbol} and expiration date {relevant_expiration}')
				continue

			# Strikes and symbols for options
			strike_data = await get_option_symbols(self._session, symbol_data.tradier_symbol, relevant_expiration)

			for strike_price in strikes_of_interest:
				try:
					option_strike_data.strikeData[strike_price]
				except KeyError:
					option_strike_price_data = OptionStrikePriceData()
					option_strike_data.strikeData[strike_price] = option_strike_price_data
					options_to_be_requested.append(strike_price)

			if len(options_to_be_requested) > 0:
				for item in chain_at_expiration.strikes:
					strike = strike_data.get(item)
					if strike and strike.strike_price in options_to_be_requested:
						option_strike_price_data = option_strike_data.strikeData[strike.strike_price]
						option_strike_price_data.brokerSpecific['call_option'] = strike.call_symbol
						option_strike_price_data.brokerSpecific['put_option'] = strike.put_symbol
						# Add to polling list (REST API with Greeks)
						if strike.call_symbol and strike.call_symbol not in self._option_data_symbols:
							self._option_data_symbols.append(strike.call_symbol)
							self._option_symbol_2_strike[strike.call_symbol] = strike.strike_price
						if strike.put_symbol and strike.put_symbol not in self._option_data_symbols:
							self._option_data_symbols.append(strike.put_symbol)
							self._option_symbol_2_strike[strike.put_symbol] = strike.strike_price
		
		logger.debug(f'Now polling {len(self._option_data_symbols)} option symbols with Greeks')
		
		# Update poll interval if scheduler is running (new options may require longer interval)
		self._update_poll_interval_if_needed()

	def _update_poll_interval_if_needed(self) -> None:
		"""
		Update the poll interval if the number of symbols has changed significantly.
		This ensures we stay under the rate limit when new options are added.
		"""
		if not self._backgroundScheduler:
			return
		
		existing_job = self._backgroundScheduler.get_job('quote_polling')
		if not existing_job:
			return
		
		new_interval = self._calculate_poll_interval()
		current_interval = existing_job.trigger.interval.total_seconds()
		
		# Only update if we need a longer interval (more symbols added)
		if new_interval > current_interval:
			self._backgroundScheduler.reschedule_job(
				'quote_polling',
				trigger='interval',
				seconds=new_interval
			)
			logger.debug(f'Updated poll interval from {current_interval}s to {new_interval}s due to additional symbols')

	def _map_order_side(self, order_action: OrderAction, leg_action: OrderAction) -> OrderSide:
		"""
		Maps the generic order action and leg action to the Tradier OrderSide.
		
		The order_action determines whether we are opening or closing a position:
		- BUY_TO_OPEN / BUY: Opening a position
		- SELL_TO_CLOSE / SELL: Closing a position
		
		For multileg orders (like Iron Condor), the individual leg action (BUY/SELL)
		needs to be combined with the order action to determine the correct side.
		"""
		# If leg has explicit open/close action, use it directly
		if leg_action == OrderAction.BUY_TO_OPEN:
			return OrderSide.BUY_TO_OPEN
		elif leg_action == OrderAction.BUY_TO_CLOSE:
			return OrderSide.BUY_TO_CLOSE
		elif leg_action == OrderAction.SELL_TO_OPEN:
			return OrderSide.SELL_TO_OPEN
		elif leg_action == OrderAction.SELL_TO_CLOSE:
			return OrderSide.SELL_TO_CLOSE
		
		# For generic BUY/SELL leg actions, determine open/close from order action
		is_opening = order_action in [OrderAction.BUY, OrderAction.BUY_TO_OPEN]
		
		if leg_action == OrderAction.BUY:
			return OrderSide.BUY_TO_OPEN if is_opening else OrderSide.BUY_TO_CLOSE
		elif leg_action == OrderAction.SELL:
			return OrderSide.SELL_TO_OPEN if is_opening else OrderSide.SELL_TO_CLOSE
		else:
			# Default based on order action
			if is_opening:
				return OrderSide.BUY_TO_OPEN
			else:
				return OrderSide.SELL_TO_CLOSE

	def _get_tradier_order_type_single(self, order: GenericOrder) -> OrderType:
		"""
		Get the Tradier order type for single-leg orders.
		"""
		from optrabot.broker.order import OrderType as GenOrderType
		
		if order.type == GenOrderType.MARKET:
			return OrderType.MARKET
		elif order.type == GenOrderType.LIMIT:
			return OrderType.LIMIT
		elif order.type == GenOrderType.STOP:
			return OrderType.STOP
		elif order.type == GenOrderType.STOP_LIMIT:
			return OrderType.STOP_LIMIT
		else:
			return OrderType.LIMIT  # Default to limit

	def _get_tradier_order_type_multileg(self, order: GenericOrder) -> OrderType:
		"""
		Get the Tradier order type for multileg orders based on price effect.
		"""
		if order.price_effect == PriceEffect.CREDIT:
			return OrderType.CREDIT
		elif order.price_effect == PriceEffect.DEBIT:
			return OrderType.DEBIT
		else:
			return OrderType.EVEN

	async def _poll_order_status(self) -> None:
		"""
		Polls order status for all open orders via REST API.
		Called periodically by the background scheduler.
		
		Uses a timeout to prevent hanging when the Tradier API is unresponsive.
		"""
		# Skip if no orders to poll or session not available
		if not self._session or len(self._orders) == 0:
			return
		
		try:
			# Use a 30 second timeout for order polling
			async with asyncio.timeout(30):
				# Get orders that are still open (not filled or cancelled)
				open_orders = [o for o in self._orders if o.status not in [GenericOrderStatus.FILLED, GenericOrderStatus.CANCELLED]]
				
				if len(open_orders) == 0:
					return
				
				# Get unique account numbers from open orders
				account_numbers = set()
				for order in open_orders:
					account_number = order.brokerSpecific.get('account_number')
					if account_number:
						account_numbers.add(account_number)
				
				# Poll each account for orders
				for account_number in account_numbers:
					try:
						tradier_orders = await a_get_orders(self._session, account_number)
						
						for tradier_order in tradier_orders:
							# Find matching managed order
							for managed_order in open_orders:
								tradier_order_id = managed_order.brokerSpecific.get('tradier_order_id')
								if tradier_order_id and tradier_order.id == tradier_order_id:
									# Update order status
									new_status = self._generic_order_status(tradier_order.status)
									if new_status and new_status != managed_order.status:
										# Update fill information
										if tradier_order.exec_quantity:
											# For multileg orders, exec_quantity is total contracts across all legs
											# We need to divide by number of legs to get the actual order quantity (spreads)
											num_legs = len(managed_order.legs) if managed_order.legs else 1
											managed_order.filledQuantity = int(tradier_order.exec_quantity) // num_legs
										if tradier_order.avg_fill_price:
											managed_order.averageFillPrice = float(tradier_order.avg_fill_price)
										
										# Emit execution details for FILLED orders to create transactions
										if new_status == GenericOrderStatus.FILLED:
											self._emit_execution_details_for_order(managed_order, tradier_order)
										
										self._emitOrderStatusEvent(managed_order, new_status, managed_order.filledQuantity)
										logger.debug(f'Order {tradier_order.id} status changed to {new_status}')
										
										# Log reason for cancellation/rejection
										if tradier_order.reason_description and new_status in [GenericOrderStatus.CANCELLED]:
											logger.warning(f'Order {tradier_order.id} rejected/cancelled: {tradier_order.reason_description}')
									break
					except asyncio.TimeoutError:
						raise  # Re-raise to be caught by outer handler
					except Exception as exc:
						error_details = str(exc) if str(exc) else 'No details available - possible network connectivity issue'
						logger.error(f'Error polling orders for account {account_number}: {type(exc).__name__}: {error_details}')
		except asyncio.TimeoutError:
			logger.warning('Order status polling timed out after 30 seconds - Tradier API may be unresponsive')
		except asyncio.CancelledError:
			logger.debug('Order polling cancelled during shutdown')
		except Exception as exc:
			error_details = str(exc) if str(exc) else 'No details available - possible network connectivity issue'
			logger.error(f'Error polling Tradier order status: {type(exc).__name__}: {error_details}')

	def _generic_order_status(self, tradier_status: OrderStatus) -> GenericOrderStatus:
		"""
		Maps the Tradier order status to the generic order status.
		"""
		if tradier_status is None:
			return None
		
		match tradier_status:
			case OrderStatus.OPEN:
				return GenericOrderStatus.OPEN
			case OrderStatus.PENDING:
				return GenericOrderStatus.OPEN
			case OrderStatus.PARTIALLY_FILLED:
				return GenericOrderStatus.OPEN
			case OrderStatus.FILLED:
				return GenericOrderStatus.FILLED
			case OrderStatus.CANCELED:
				return GenericOrderStatus.CANCELLED
			case OrderStatus.EXPIRED:
				return GenericOrderStatus.CANCELLED
			case OrderStatus.REJECTED:
				return GenericOrderStatus.CANCELLED
			case _:
				logger.debug(f'Unknown Tradier order status: {tradier_status}')
				return None

	def _emit_execution_details_for_order(self, managed_order: GenericOrder, tradier_order) -> None:
		"""
		Emits execution details events for a filled order.
		This creates transactions in the database for PNL tracking.
		
		For multileg orders, we emit one execution event per leg.
		For single-leg orders, we emit one execution event.
		"""
		try:
			# Determine if this is a multileg order
			if tradier_order.leg and len(tradier_order.leg) > 0:
				# Multileg order - emit execution for each leg
				for i, tradier_leg in enumerate(tradier_order.leg):
					self._emit_execution_for_leg(managed_order, tradier_order, tradier_leg, i)
			else:
				# Single leg order - emit one execution
				self._emit_execution_for_single_leg(managed_order, tradier_order)
				
		except Exception as exc:
			logger.error(f'Error emitting execution details for order {tradier_order.id}: {exc}')

	def _emit_execution_for_leg(self, managed_order: GenericOrder, tradier_order, tradier_leg, leg_index: int) -> None:
		"""
		Emits execution details for a single leg of a multileg order.
		"""
		# Find the matching generic leg
		matching_generic_leg = None
		if leg_index < len(managed_order.legs):
			matching_generic_leg = managed_order.legs[leg_index]
		
		if matching_generic_leg is None:
			logger.error(f'No matching leg found for Tradier leg at index {leg_index} in order {tradier_order.id}')
			return
		
		# Determine action from Tradier leg side
		action = self._execution_action_from_side(tradier_leg.side)
		
		# Determine option type
		sec_type = 'C' if matching_generic_leg.right == OptionRight.CALL else 'P'
		
		# Get fill price - use leg's avg_fill_price or fall back to last_fill_price
		fill_price = 0.0
		if tradier_leg.avg_fill_price:
			fill_price = float(tradier_leg.avg_fill_price)
		elif tradier_leg.last_fill_price:
			fill_price = float(tradier_leg.last_fill_price)
		
		# Get executed quantity
		exec_qty = int(tradier_leg.exec_quantity) if tradier_leg.exec_quantity else int(tradier_leg.quantity or 0)
		
		# Create unique execution ID
		exec_id = f'{tradier_order.id}-{tradier_leg.id}'
		
		# Use transaction_date as timestamp, or current time if not available
		timestamp = tradier_order.transaction_date or datetime.now()
		
		execution = Execution(
			id=exec_id,
			action=action,
			sec_type=sec_type,
			strike=matching_generic_leg.strike,
			amount=exec_qty,
			price=fill_price,
			expiration=matching_generic_leg.expiration,
			timestamp=timestamp
		)
		
		logger.debug(f'Emitting execution details for Tradier order {tradier_order.id} leg {tradier_leg.id}: '
					f'{matching_generic_leg.right} {matching_generic_leg.strike} qty={exec_qty} @ ${fill_price:.2f}')
		self._emitOrderExecutionDetailsEvent(managed_order, execution)

	def _emit_execution_for_single_leg(self, managed_order: GenericOrder, tradier_order) -> None:
		"""
		Emits execution details for a single-leg order.
		"""
		if len(managed_order.legs) == 0:
			logger.error(f'No legs found in managed order for Tradier order {tradier_order.id}')
			return
		
		generic_leg = managed_order.legs[0]
		
		# Determine action from order side
		action = self._execution_action_from_side(tradier_order.side)
		
		# Determine option type
		sec_type = 'C' if generic_leg.right == OptionRight.CALL else 'P'
		
		# Get fill price - use avg_fill_price or fall back to last_fill_price
		fill_price = 0.0
		if tradier_order.avg_fill_price:
			fill_price = float(tradier_order.avg_fill_price)
		elif tradier_order.last_fill_price:
			fill_price = float(tradier_order.last_fill_price)
		
		# Get executed quantity
		exec_qty = int(tradier_order.exec_quantity) if tradier_order.exec_quantity else int(tradier_order.quantity or 0)
		
		# Create unique execution ID
		exec_id = str(tradier_order.id)
		
		# Use transaction_date as timestamp
		timestamp = tradier_order.transaction_date or datetime.now()
		
		execution = Execution(
			id=exec_id,
			action=action,
			sec_type=sec_type,
			strike=generic_leg.strike,
			amount=exec_qty,
			price=fill_price,
			expiration=generic_leg.expiration,
			timestamp=timestamp
		)
		
		logger.debug(f'Emitting execution details for Tradier order {tradier_order.id}: '
					f'{generic_leg.right} {generic_leg.strike} qty={exec_qty} @ ${fill_price:.2f}')
		self._emitOrderExecutionDetailsEvent(managed_order, execution)

	def _execution_action_from_side(self, side) -> OrderAction:
		"""
		Maps Tradier OrderSide to generic OrderAction for transaction recording.
		
		Note: We normalize to BUY/SELL only (not BUY_TO_OPEN etc.) to maintain
		consistency with other connectors (IB, TastyTrade) in the database.
		"""
		from optrabot.broker.tradier.orders import OrderSide
		
		if side in (OrderSide.BUY_TO_OPEN, OrderSide.BUY_TO_CLOSE):
			return OrderAction.BUY
		elif side in (OrderSide.SELL_TO_OPEN, OrderSide.SELL_TO_CLOSE):
			return OrderAction.SELL
		else:
			# Default to BUY for unknown sides
			return OrderAction.BUY