"""Yahoo Finance data provider for Indonesian stocks (IDX) and US stocks (NYSE/NASDAQ)."""
from __future__ import annotations

import logging
import time
from datetime import datetime, timedelta
from typing import Optional

from app.services.data_sync.base import AbstractDataProvider

logger = logging.getLogger(__name__)

# Suppress noisy yfinance HTTP 404 logs (e.g. "No fundamentals data found")
logging.getLogger('yfinance').setLevel(logging.CRITICAL)

# Yahoo Finance timeframe mapping (our key → yfinance interval)
# Supported: 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo
_TF_MAP = {
    '1m': '1m',
    '15m': '15m',
    '30m': '30m',
    '1h': '1h',
    '4h': '1h',   # Yahoo doesn't support 4h natively; fetch 1h and aggregate
    '1D': '1d',
    '1W': '1wk',
}

# Period mapping (how much history to fetch per timeframe)
_PERIOD_MAP = {
    '1m': '5d',    # yfinance max ~7 days for 1m
    '15m': '60d',  # yfinance max 60 days for intraday
    '30m': '60d',
    '1h': '730d',  # yfinance max ~2 years for 1h
    '4h': '730d',  # fetched as 1h then aggregated
    '1D': '2y',
    '1W': '5y',
}

# US exchange identifiers returned by yfinance
_US_EXCHANGES = ('NYQ', 'NMS', 'NYSE', 'NASDAQ', 'NGM', 'NCM', 'PCX', 'BTS')


def _to_yahoo_ticker(asset_id: str) -> str:
    """Convert a namespaced asset ID to a Yahoo Finance ticker symbol.

    IDX.BBCA    → BBCA.JK  (Jakarta Stock Exchange)
    NYSE.AAPL   → AAPL     (no suffix for US stocks)
    NASDAQ.MSFT → MSFT     (no suffix for US stocks)
    """
    from app.helpers.asset_id import parse_asset_id
    prefix, raw = parse_asset_id(asset_id)
    ticker = raw.upper()

    if prefix == 'IDX':
        if not ticker.endswith('.JK'):
            ticker += '.JK'
    # NYSE and NASDAQ tickers need no suffix
    return ticker


# Module-level USD/IDR rate cache
_usd_idr_cache: dict = {'rate': None, 'ts': 0}


def _get_usd_idr_rate() -> float:
    """Get USD/IDR exchange rate from Yahoo Finance, cached for 1 hour.

    Falls back to stored AppSettings value, then to a hardcoded default.
    """
    now = time.time()
    if _usd_idr_cache['rate'] and now - _usd_idr_cache['ts'] < 3600:
        return _usd_idr_cache['rate']

    try:
        import yfinance as yf
        ticker = yf.Ticker('USDIDR=X')
        info = ticker.info or {}
        rate = info.get('regularMarketPrice') or info.get('previousClose')
        if rate:
            _usd_idr_cache['rate'] = float(rate)
            _usd_idr_cache['ts'] = now
            # Persist to AppSettings for use elsewhere
            from app.models.settings import AppSettings
            from app.extensions import db
            AppSettings.set('usd_idr_rate', float(rate), 'system',
                            'USD/IDR exchange rate from Yahoo Finance')
            logger.info(f'USD/IDR rate updated: {rate}')
            return float(rate)
    except Exception as e:
        logger.warning(f'Failed to fetch USD/IDR rate: {e}')

    # Fallback to stored rate
    try:
        from app.models.settings import AppSettings
        stored = AppSettings.get('usd_idr_rate')
        if stored:
            rate = float(stored)
            _usd_idr_cache['rate'] = rate
            _usd_idr_cache['ts'] = now
            return rate
    except Exception:
        pass

    return 16000.0  # reasonable fallback


def _safe_val(val):
    """Convert a single pandas/numpy value to a JSON-safe Python native type.

    Handles: None, pd.NA, pd.NaT, float NaN/Inf, numpy scalars, Timestamps.
    """
    import math
    import pandas as pd
    if val is None or val is pd.NA or val is pd.NaT:
        return None
    try:
        if pd.isna(val):
            return None
    except (TypeError, ValueError):
        pass
    if isinstance(val, float) and (math.isnan(val) or math.isinf(val)):
        return None
    if hasattr(val, 'item'):  # numpy scalar → Python native
        return val.item()
    if hasattr(val, 'isoformat'):  # Timestamp → ISO string
        return val.isoformat()
    return val


def _df_to_dict(df) -> dict | None:
    """Convert a yfinance pandas DataFrame to a JSON-serializable dict.

    Structure: {column_name: {date_iso: value, ...}, ...}
    Handles NaN/Inf/NA/NaT → None, Timestamps → ISO strings, numpy types → native.
    """
    if df is None or (hasattr(df, 'empty') and df.empty):
        return None
    result = {}
    for col in df.columns:
        col_data = {}
        for idx, val in df[col].items():
            key = idx.isoformat() if hasattr(idx, 'isoformat') else str(idx)
            col_data[key] = _safe_val(val)
        result[str(col)] = col_data
    return result


def _df_to_rows(df) -> list | None:
    """Convert a yfinance DataFrame to a list of row-dicts (for tabular data like holders)."""
    if df is None or (hasattr(df, 'empty') and df.empty):
        return None
    rows = []
    for idx, row in df.iterrows():
        entry = {}
        idx_str = idx.isoformat() if hasattr(idx, 'isoformat') else str(idx)
        entry['_index'] = idx_str
        for col in df.columns:
            entry[str(col)] = _safe_val(row[col])
        rows.append(entry)
    return rows if rows else None


def _series_to_list(series) -> list | None:
    """Convert a pandas Series (e.g. dividends, splits) to a list of {date, value} dicts."""
    if series is None or (hasattr(series, 'empty') and series.empty):
        return None
    result = []
    for idx, val in series.items():
        date_str = idx.isoformat() if hasattr(idx, 'isoformat') else str(idx)
        safe = _safe_val(val)
        if safe is not None:
            result.append({'date': date_str, 'value': safe})
    return result if result else None


class YahooFinanceProvider(AbstractDataProvider):
    """Yahoo Finance provider for IDX and US stocks.

    IDX tickers get .JK suffix; US tickers (NYSE/NASDAQ) use raw symbols.
    """

    provider_id = 'yahoo'

    def __init__(self):
        self._retry_max = 3
        self._retry_base_delay = 2.0

    def _get_ticker(self, asset_id: str):
        """Create a yfinance Ticker with appropriate suffix."""
        import yfinance as yf
        return yf.Ticker(_to_yahoo_ticker(asset_id))

    def get_ohlcv(self, asset_id: str, timeframe: str,
                  start: datetime, end: datetime) -> list[dict]:
        """Fetch OHLCV data from Yahoo Finance.

        Returns list of dicts with keys: timestamp, open, high, low, close, volume
        """
        yf_interval = _TF_MAP.get(timeframe, '1d')
        yf_period = _PERIOD_MAP.get(timeframe, '1y')

        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)
                df = ticker.history(period=yf_period, interval=yf_interval)

                if df is None or df.empty:
                    logger.warning(f'Yahoo: no OHLCV data for {asset_id} ({timeframe})')
                    return []

                import math

                # Drop rows with NaN values (common on illiquid stocks)
                df = df.dropna(subset=['Open', 'High', 'Low', 'Close'])

                results = []
                for idx, row in df.iterrows():
                    ts = int(idx.timestamp())
                    o, h, l, c = float(row['Open']), float(row['High']), float(row['Low']), float(row['Close'])
                    v = float(row['Volume']) if not math.isnan(row['Volume']) else 0.0

                    # Extra safety: skip any remaining NaN/Inf
                    if any(math.isnan(x) or math.isinf(x) for x in (o, h, l, c)):
                        continue

                    results.append({
                        'timestamp': ts,
                        'open': o,
                        'high': h,
                        'low': l,
                        'close': c,
                        'volume': v,
                    })

                # If 4h requested but fetched 1h, aggregate to 4h
                if timeframe == '4h' and yf_interval == '1h' and results:
                    results = self._aggregate_to_4h(results)

                logger.info(f'Yahoo: fetched {len(results)} {timeframe} candles for {asset_id}')
                return results

            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo OHLCV attempt {attempt + 1}/{self._retry_max} failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)

        return []

    def _aggregate_to_4h(self, candles_1h: list[dict]) -> list[dict]:
        """Aggregate 1-hour candles to 4-hour candles."""
        if not candles_1h:
            return []

        result = []
        batch = []
        for c in candles_1h:
            batch.append(c)
            if len(batch) == 4:
                result.append({
                    'timestamp': batch[0]['timestamp'],
                    'open': batch[0]['open'],
                    'high': max(b['high'] for b in batch),
                    'low': min(b['low'] for b in batch),
                    'close': batch[-1]['close'],
                    'volume': sum(b['volume'] for b in batch),
                })
                batch = []

        # Handle remaining candles (incomplete 4h bar)
        if batch:
            result.append({
                'timestamp': batch[0]['timestamp'],
                'open': batch[0]['open'],
                'high': max(b['high'] for b in batch),
                'low': min(b['low'] for b in batch),
                'close': batch[-1]['close'],
                'volume': sum(b['volume'] for b in batch),
            })

        return result

    def search_coins(self, query: str, market: str = 'idx') -> list[dict]:
        """Search stocks on Yahoo Finance.

        Args:
            query: Search term (ticker or company name).
            market: 'idx' for Indonesian stocks (.JK), 'us' for NYSE/NASDAQ.
        """
        if market == 'us':
            return self._search_us(query)
        return self._search_idx(query)

    def _search_us(self, query: str) -> list[dict]:
        """Search for US stocks (NYSE/NASDAQ)."""
        results = []

        # Try direct ticker lookup
        try:
            import yfinance as yf
            ticker_id = query.upper().strip()
            ticker = yf.Ticker(ticker_id)
            info = ticker.info or {}
            exchange = info.get('exchange', '')
            if (info.get('regularMarketPrice') or info.get('previousClose')) \
                    and exchange in _US_EXCHANGES:
                prefix = 'NASDAQ' if exchange in ('NMS', 'NGM', 'NCM') else 'NYSE'
                results.append({
                    'id': f'{prefix}.{ticker_id}',
                    'symbol': ticker_id,
                    'name': info.get('longName') or info.get('shortName') or ticker_id,
                    'asset_type': 'stock_us',
                })
        except Exception:
            pass

        # Also try yfinance search for broader results
        try:
            from yfinance import search as yf_search
            search_results = yf_search(query)
            if hasattr(search_results, 'get'):
                quotes = search_results.get('quotes', [])
            elif hasattr(search_results, 'quotes'):
                quotes = search_results.quotes if search_results.quotes is not None else []
            else:
                quotes = []

            for q in quotes:
                symbol = q.get('symbol', '')
                exchange = q.get('exchange', '')
                if exchange in _US_EXCHANGES and not symbol.endswith('.JK'):
                    prefix = 'NASDAQ' if exchange in ('NMS', 'NGM', 'NCM') else 'NYSE'
                    clean_id = symbol.replace('.', '-') if '.' in symbol else symbol
                    if not any(r['symbol'] == clean_id for r in results):
                        results.append({
                            'id': f'{prefix}.{clean_id}',
                            'symbol': clean_id,
                            'name': q.get('longname') or q.get('shortname') or clean_id,
                            'asset_type': 'stock_us',
                        })
        except Exception as e:
            logger.debug(f'Yahoo US search fallback: {e}')

        return results[:20]

    def _search_idx(self, query: str) -> list[dict]:
        """Search Indonesian stocks (.JK suffix)."""
        results = []
        try:
            # Try direct ticker lookup first
            import yfinance as yf
            ticker_id = query.upper()
            ticker = yf.Ticker(f'{ticker_id}.JK')
            info = ticker.info or {}
            if info.get('regularMarketPrice') or info.get('previousClose'):
                results.append({
                    'id': ticker_id,
                    'symbol': ticker_id,
                    'name': info.get('longName') or info.get('shortName') or ticker_id,
                    'asset_type': 'stock',
                })
        except Exception:
            pass

        # Also try yfinance search
        try:
            from yfinance import search as yf_search
            search_results = yf_search(query)
            if hasattr(search_results, 'get'):
                quotes = search_results.get('quotes', [])
            elif hasattr(search_results, 'quotes'):
                quotes = search_results.quotes if search_results.quotes is not None else []
            else:
                quotes = []

            for q in quotes:
                symbol = q.get('symbol', '')
                if symbol.endswith('.JK'):
                    clean_id = symbol.replace('.JK', '')
                    # Avoid duplicates
                    if not any(r['id'] == clean_id for r in results):
                        results.append({
                            'id': clean_id,
                            'symbol': clean_id,
                            'name': q.get('longname') or q.get('shortname') or clean_id,
                            'asset_type': 'stock',
                        })
        except Exception as e:
            logger.debug(f'Yahoo search fallback: {e}')

        return results[:20]

    def get_coin_profile(self, asset_id: str) -> Optional[dict]:
        """Fetch comprehensive stock profile from Yahoo Finance.

        Maps Yahoo Finance info to our standard profile format.
        Handles both IDX (IDR-denominated) and US (USD-denominated) stocks.
        """
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)
                info = ticker.info or {}

                if not info.get('regularMarketPrice') and not info.get('previousClose'):
                    logger.warning(f'Yahoo: no profile data for {asset_id}')
                    return None

                from app.helpers.asset_id import parse_asset_id, get_raw_id
                prefix, _ = parse_asset_id(asset_id)
                raw = get_raw_id(asset_id).upper()
                price = info.get('regularMarketPrice') or info.get('previousClose', 0)

                if prefix in ('NYSE', 'NASDAQ'):
                    return self._build_us_profile(asset_id, raw, prefix, price, info)
                else:
                    return self._build_idx_profile(asset_id, raw, price, info)

            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo profile attempt {attempt + 1}/{self._retry_max} failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)

        return None

    def _build_us_profile(self, asset_id: str, raw: str, prefix: str,
                          price: float, info: dict) -> dict:
        """Build profile dict for US stocks (prices in USD)."""
        usd_idr_rate = _get_usd_idr_rate()
        market_cap_usd = info.get('marketCap') or 0
        volume_usd = (info.get('regularMarketVolume', 0) or 0) * price if price else 0

        profile = {
            'id': asset_id.upper(),
            'symbol': raw,
            'name': info.get('longName') or info.get('shortName') or raw,
            'description': info.get('longBusinessSummary', ''),
            'asset_type': 'stock_us',
            'current_price_usd': price,
            'current_price_idr': price * usd_idr_rate if usd_idr_rate else None,
            'market_cap_idr': int(market_cap_usd * usd_idr_rate) if usd_idr_rate else None,
            'total_volume_idr': int(volume_usd * usd_idr_rate) if usd_idr_rate else None,
            'price_change_24h': info.get('regularMarketChangePercent'),
            'ath_idr': (info.get('fiftyTwoWeekHigh') or 0) * usd_idr_rate if usd_idr_rate else None,
            'atl_idr': (info.get('fiftyTwoWeekLow') or 0) * usd_idr_rate if usd_idr_rate else None,

            'sector': info.get('sector'),
            'sub_sector': info.get('industry'),
            'website': info.get('website'),
            'listing_date': None,

            'twitter_followers': None,
            'reddit_subscribers': None,
            'telegram_users': None,
            'github_stars': None,

            'profile_json': {
                **self._build_profile_json(info),
                'currency': 'USD',
                'usd_idr_rate': usd_idr_rate,
                'exchange': info.get('exchange'),
                'ath_usd': info.get('fiftyTwoWeekHigh'),
                'atl_usd': info.get('fiftyTwoWeekLow'),
                'market_cap_usd': market_cap_usd,
            },
        }

        logger.info(f'Yahoo: fetched US profile for {asset_id}')
        return profile

    def _build_idx_profile(self, asset_id: str, raw: str,
                           price: float, info: dict) -> dict:
        """Build profile dict for IDX stocks (prices in IDR)."""
        profile = {
            'id': asset_id.upper(),
            'symbol': raw,
            'name': info.get('longName') or info.get('shortName') or raw,
            'description': info.get('longBusinessSummary', ''),
            'asset_type': 'stock',
            'current_price_idr': price,
            'current_price_usd': None,  # Yahoo provides IDR directly for .JK
            'market_cap_idr': info.get('marketCap'),
            'total_volume_idr': info.get('regularMarketVolume', 0) * price if price else 0,
            'price_change_24h': info.get('regularMarketChangePercent'),
            'ath_idr': info.get('fiftyTwoWeekHigh'),
            'atl_idr': info.get('fiftyTwoWeekLow'),

            # Stock-specific fields
            'sector': info.get('sector'),
            'sub_sector': info.get('industry'),
            'website': info.get('website'),
            'listing_date': None,  # Yahoo doesn't provide IPO date reliably

            # Social/community (not applicable for stocks)
            'twitter_followers': None,
            'reddit_subscribers': None,
            'telegram_users': None,
            'github_stars': None,

            # Extra info stored in profile_json
            'profile_json': self._build_profile_json(info),
        }

        logger.info(f'Yahoo: fetched profile for {asset_id}')
        return profile

    @staticmethod
    def _build_profile_json(info: dict) -> dict:
        """Extract comprehensive company profile data from yfinance info dict.

        Includes financial metrics, company officers, address, dividends,
        ownership, valuation multiples, and fundamentals.
        """
        pj: dict = {
            # ── Identifiers ──
            'isin': info.get('isin'),

            # ── Core Financial Metrics ──
            'pe_ratio': info.get('trailingPE'),
            'pb_ratio': info.get('priceToBook'),
            'eps': info.get('trailingEps'),
            'beta': info.get('beta'),
            'book_value': info.get('bookValue'),
            'revenue': info.get('totalRevenue'),
            'gross_profit': info.get('grossProfits'),
            'ebitda': info.get('ebitda'),
            'net_income': info.get('netIncomeToCommon'),
            'debt_to_equity': info.get('debtToEquity'),
            'roe': info.get('returnOnEquity'),
            'roa': info.get('returnOnAssets'),
            'revenue_per_share': info.get('revenuePerShare'),
            'total_cash_per_share': info.get('totalCashPerShare'),

            # ── Dividend Data ──
            'dividend_yield': info.get('dividendYield'),
            'dividend_rate': info.get('dividendRate'),
            'payout_ratio': info.get('payoutRatio'),
            'ex_dividend_date': info.get('exDividendDate'),
            'last_dividend_date': info.get('lastDividendDate'),
            'last_dividend_value': info.get('lastDividendValue'),
            'five_year_avg_dividend_yield': info.get('fiveYearAvgDividendYield'),
            'trailing_annual_dividend_rate': info.get('trailingAnnualDividendRate'),
            'trailing_annual_dividend_yield': info.get('trailingAnnualDividendYield'),

            # ── Valuation Multiples ──
            'forward_pe': info.get('forwardPE'),
            'peg_ratio': info.get('pegRatio'),
            'price_to_sales': info.get('priceToSalesTrailing12Months'),
            'enterprise_value': info.get('enterpriseValue'),
            'ev_to_ebitda': info.get('enterpriseToEbitda'),
            'ev_to_revenue': info.get('enterpriseToRevenue'),

            # ── Margins & Growth ──
            'profit_margins': info.get('profitMargins'),
            'operating_margins': info.get('operatingMargins'),
            'gross_margins': info.get('grossMargins'),
            'ebitda_margins': info.get('ebitdaMargins'),
            'revenue_growth': info.get('revenueGrowth'),
            'earnings_growth': info.get('earningsGrowth'),
            'earnings_quarterly_growth': info.get('earningsQuarterlyGrowth'),

            # ── Liquidity & Debt ──
            'current_ratio': info.get('currentRatio'),
            'quick_ratio': info.get('quickRatio'),
            'total_debt': info.get('totalDebt'),
            'total_cash': info.get('totalCash'),

            # ── Cashflow ──
            'operating_cashflow': info.get('operatingCashflow'),
            'free_cashflow': info.get('freeCashflow'),

            # ── Share Data ──
            'shares_outstanding': info.get('sharesOutstanding'),
            'free_float': info.get('floatShares'),
            'held_percent_insiders': info.get('heldPercentInsiders'),
            'held_percent_institutions': info.get('heldPercentInstitutions'),

            # ── Moving Averages & Price History ──
            'fifty_day_avg': info.get('fiftyDayAverage'),
            'two_hundred_day_avg': info.get('twoHundredDayAverage'),
            'all_time_high': info.get('allTimeHigh'),
            'all_time_low': info.get('allTimeLow'),
            'fifty_two_week_change': info.get('fiftyTwoWeekChangePercent'),
            'sp500_52_week_change': info.get('SandP52WeekChange'),

            # ── Volume ──
            'avg_volume_10d': info.get('averageDailyVolume10Day'),
            'avg_volume_3m': info.get('averageDailyVolume3Month'),

            # ── Analyst ──
            'target_mean_price': info.get('targetMeanPrice'),
            'recommendation_key': info.get('recommendationKey'),
            'number_of_analyst_opinions': info.get('numberOfAnalystOpinions'),

            # ── Company Info ──
            'long_business_summary': info.get('longBusinessSummary'),
            'industry': info.get('industry'),
            'sector': info.get('sector'),
            'full_time_employees': info.get('fullTimeEmployees'),
            'long_name': info.get('longName'),
            'short_name': info.get('shortName'),

            # ── Address ──
            'address': info.get('address1'),
            'address2': info.get('address2'),
            'city': info.get('city'),
            'state': info.get('state'),
            'zip': info.get('zip'),
            'country': info.get('country'),
            'phone': info.get('phone'),
            'fax': info.get('fax'),

            # ── Calendar / Dates ──
            'first_trade_date': info.get('firstTradeDateMilliseconds'),
            'last_fiscal_year_end': info.get('lastFiscalYearEnd'),
            'most_recent_quarter': info.get('mostRecentQuarter'),
            'next_fiscal_year_end': info.get('nextFiscalYearEnd'),
            'earnings_timestamp': info.get('earningsTimestamp'),
            'dividend_date': info.get('dividendDate'),

            # ── Forward Estimates ──
            'eps_forward': info.get('epsForward') or info.get('forwardEps'),
            'eps_current_year': info.get('epsCurrentYear'),

            # ── Short Interest (US stocks) ──
            'shares_short': info.get('sharesShort'),
            'short_ratio': info.get('shortRatio'),
            'short_percent_of_float': info.get('shortPercentOfFloat'),

            # ── Stock Split ──
            'last_split_factor': info.get('lastSplitFactor'),
            'last_split_date': info.get('lastSplitDate'),

            # ── Analyst ──
            'average_analyst_rating': info.get('averageAnalystRating'),
        }

        # Company officers (list of dicts with name, title, age, compensation)
        officers = info.get('companyOfficers')
        if officers and isinstance(officers, list):
            pj['company_officers'] = [
                {
                    'name': o.get('name', ''),
                    'title': o.get('title', ''),
                    'age': o.get('age'),
                    'year_born': o.get('yearBorn'),
                    'total_pay': o.get('totalPay'),
                    'exercised_value': o.get('exercisedValue'),
                    'unexercised_value': o.get('unexercisedValue'),
                }
                for o in officers[:20]  # cap at 20 to keep JSON size reasonable
            ]

        return pj

    def get_current_price(self, asset_id: str) -> Optional[float]:
        """Get current price from Yahoo Finance using fast_info (faster, no full info load).

        Returns price in native currency (IDR for IDX, USD for US stocks).
        """
        try:
            ticker = self._get_ticker(asset_id)
            # fast_info is much faster than ticker.info — single lightweight request
            fi = ticker.fast_info
            price = getattr(fi, 'last_price', None) or getattr(fi, 'previous_close', None)
            if price:
                return float(price)
            # Fallback to full info if fast_info didn't work
            info = ticker.info or {}
            price = info.get('regularMarketPrice') or info.get('previousClose')
            return float(price) if price else None
        except Exception as e:
            logger.warning(f'Yahoo price failed for {asset_id}: {e}')
            return None

    def is_available(self, asset_id: str) -> bool:
        """Check if a stock is available on Yahoo Finance."""
        try:
            ticker = self._get_ticker(asset_id)
            info = ticker.info or {}
            return bool(info.get('regularMarketPrice') or info.get('previousClose'))
        except Exception:
            return False

    # ═══════════════════════════════════════════════════════════════════════════
    # Extended data methods — financial statements, earnings, dividends,
    # analyst, ownership, insider, news, batch OHLCV, sector/industry
    # ═══════════════════════════════════════════════════════════════════════════

    def get_financial_statements(self, asset_id: str) -> dict:
        """Fetch all financial statements (income, balance, cashflow) — annual & quarterly.

        Returns dict with keys: financials, quarterly_financials, balance_sheet,
        quarterly_balance_sheet, cash_flow, quarterly_cash_flow.
        Each value is a serialized dict or None.
        """
        result = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)
                result['financials'] = _df_to_dict(ticker.financials)
                result['quarterly_financials'] = _df_to_dict(ticker.quarterly_financials)
                result['balance_sheet'] = _df_to_dict(ticker.balance_sheet)
                result['quarterly_balance_sheet'] = _df_to_dict(ticker.quarterly_balance_sheet)
                result['cash_flow'] = _df_to_dict(ticker.cashflow)
                result['quarterly_cash_flow'] = _df_to_dict(ticker.quarterly_cashflow)

                # TTM cash flow (trailing twelve months)
                try:
                    ttm_cf = ticker.ttm_cash_flow
                    if ttm_cf is not None and not ttm_cf.empty:
                        result['ttm_cash_flow'] = _df_to_dict(ttm_cf)
                except Exception:
                    pass

                populated = sum(1 for v in result.values() if v)
                logger.info(f'Yahoo: fetched {populated} financial statements for {asset_id}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo financials attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    def get_earnings_data(self, asset_id: str) -> dict:
        """Fetch earnings data: earnings history, earnings dates, calendar.

        Returns dict with keys: earnings, earnings_dates, calendar.
        Note: ticker.earnings is deprecated in yfinance >= 0.2.28. We use
        income_stmt's 'Net Income' as fallback. earnings_dates may need lxml.
        """
        result = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)

                # earnings: deprecated; try income_stmt as fallback
                try:
                    import warnings
                    with warnings.catch_warnings():
                        warnings.simplefilter('ignore', DeprecationWarning)
                        e = ticker.earnings
                    result['earnings'] = _df_to_dict(e)
                except Exception:
                    result['earnings'] = None

                # earnings_dates: may fail without lxml installed
                try:
                    result['earnings_dates'] = _df_to_rows(ticker.earnings_dates)
                except Exception as ed_err:
                    logger.debug(f'Yahoo: earnings_dates unavailable for {asset_id}: {ed_err}')
                    result['earnings_dates'] = None

                # calendar: dict or DataFrame depending on yfinance version
                cal = ticker.calendar
                if cal is not None:
                    if hasattr(cal, 'columns'):
                        result['calendar'] = _df_to_dict(cal)
                    elif isinstance(cal, dict):
                        # Clean any non-serializable values
                        clean_cal = {}
                        for k, v in cal.items():
                            if hasattr(v, 'isoformat'):
                                clean_cal[k] = v.isoformat()
                            elif hasattr(v, 'item'):
                                clean_cal[k] = v.item()
                            elif isinstance(v, list):
                                clean_cal[k] = [
                                    x.isoformat() if hasattr(x, 'isoformat')
                                    else (x.item() if hasattr(x, 'item') else x)
                                    for x in v
                                ]
                            else:
                                clean_cal[k] = v
                        result['calendar'] = clean_cal
                    else:
                        result['calendar'] = None
                else:
                    result['calendar'] = None

                logger.info(f'Yahoo: fetched earnings data for {asset_id}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo earnings attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    def get_dividends_splits(self, asset_id: str) -> dict:
        """Fetch dividend and split history.

        Returns: {dividends: [{date, value},...], splits: [{date, value},...]}
        """
        result = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)
                result['dividends'] = _series_to_list(ticker.dividends)
                result['splits'] = _series_to_list(ticker.splits)
                div_count = len(result['dividends']) if result['dividends'] else 0
                split_count = len(result['splits']) if result['splits'] else 0
                logger.info(f'Yahoo: fetched {div_count} dividends, {split_count} splits '
                            f'for {asset_id}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo dividends attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    def get_analyst_data(self, asset_id: str) -> dict:
        """Fetch analyst recommendations, upgrades/downgrades, price targets.

        Returns dict with keys: recommendations, recommendations_summary,
        upgrades_downgrades, analyst_price_targets.
        """
        result = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)

                # recommendations: DataFrame (period → strongBuy/buy/hold/sell/strongSell)
                recs = ticker.recommendations
                if recs is not None and not recs.empty:
                    result['recommendations'] = _df_to_rows(recs)

                # recommendations_summary: DataFrame (summary of current recommendations)
                recs_summary = ticker.recommendations_summary
                if recs_summary is not None and not recs_summary.empty:
                    result['recommendations_summary'] = _df_to_rows(recs_summary)

                # upgrades_downgrades: DataFrame (Firm, ToGrade, FromGrade, Action)
                try:
                    ud = ticker.upgrades_downgrades
                    if ud is not None and not ud.empty:
                        result['upgrades_downgrades'] = _df_to_rows(ud.head(50))
                except Exception:
                    pass  # Some tickers don't have this

                # analyst_price_targets: dict or DataFrame
                try:
                    apt = ticker.analyst_price_targets
                    if apt is not None:
                        if hasattr(apt, 'to_dict'):
                            result['analyst_price_targets'] = _df_to_rows(apt) if hasattr(apt, 'iterrows') else apt.to_dict()
                        elif isinstance(apt, dict):
                            result['analyst_price_targets'] = apt
                except Exception:
                    pass

                logger.info(f'Yahoo: fetched analyst data for {asset_id}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo analyst attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    def get_ownership_data(self, asset_id: str) -> dict:
        """Fetch ownership data: major holders, institutional, mutual fund, insider.

        Returns dict with keys: major_holders, institutional_holders,
        mutualfund_holders, insider_transactions, insider_purchases,
        insider_roster_holders.
        """
        result = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)

                try:
                    mh = ticker.major_holders
                    if mh is not None and not mh.empty:
                        result['major_holders'] = _df_to_rows(mh)
                except Exception:
                    pass

                try:
                    ih = ticker.institutional_holders
                    if ih is not None and not ih.empty:
                        result['institutional_holders'] = _df_to_rows(ih.head(25))
                except Exception:
                    pass

                try:
                    mfh = ticker.mutualfund_holders
                    if mfh is not None and not mfh.empty:
                        result['mutualfund_holders'] = _df_to_rows(mfh.head(25))
                except Exception:
                    pass

                try:
                    it = ticker.insider_transactions
                    if it is not None and not it.empty:
                        result['insider_transactions'] = _df_to_rows(it.head(30))
                except Exception:
                    pass

                try:
                    ip = ticker.insider_purchases
                    if ip is not None and not ip.empty:
                        result['insider_purchases'] = _df_to_rows(ip)
                except Exception:
                    pass

                try:
                    irh = ticker.insider_roster_holders
                    if irh is not None and not irh.empty:
                        result['insider_roster_holders'] = _df_to_rows(irh.head(20))
                except Exception:
                    pass

                populated = sum(1 for v in result.values() if v)
                logger.info(f'Yahoo: fetched {populated} ownership datasets for {asset_id}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo ownership attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    def get_news(self, asset_id: str) -> list | None:
        """Fetch latest news for a stock.

        Returns list of cleaned news dicts (max 20):
        [{title, link, publisher, publish_time, type, thumbnail}]

        Handles both old yfinance format (flat dicts) and new format
        (nested {id, content: {title, pubDate, provider, canonicalUrl, thumbnail}}).
        """
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)
                news = ticker.news
                if news:
                    cleaned = []
                    for item in news[:20]:
                        # New yfinance format: {id, content: {...}}
                        content = item.get('content', {}) if isinstance(item.get('content'), dict) else {}
                        if content.get('title'):
                            # New format
                            thumb = None
                            thumb_obj = content.get('thumbnail')
                            if thumb_obj and isinstance(thumb_obj, dict):
                                resolutions = thumb_obj.get('resolutions', [])
                                if resolutions:
                                    # Prefer smaller resolution for thumbnails
                                    thumb = resolutions[-1].get('url') if len(resolutions) > 1 else resolutions[0].get('url')
                            # Parse publish time to unix timestamp
                            pub_time = None
                            pub_date_str = content.get('pubDate', '')
                            if pub_date_str:
                                try:
                                    from datetime import datetime as _dt, timezone as _tz
                                    dt = _dt.fromisoformat(pub_date_str.replace('Z', '+00:00'))
                                    pub_time = int(dt.timestamp())
                                except Exception:
                                    pass
                            link = ''
                            canon = content.get('canonicalUrl') or content.get('clickThroughUrl')
                            if isinstance(canon, dict):
                                link = canon.get('url', '')
                            elif isinstance(canon, str):
                                link = canon
                            publisher = ''
                            prov = content.get('provider')
                            if isinstance(prov, dict):
                                publisher = prov.get('displayName', '')
                            cleaned.append({
                                'title': content.get('title', ''),
                                'link': link,
                                'publisher': publisher,
                                'publish_time': pub_time,
                                'type': content.get('contentType', ''),
                                'thumbnail': thumb,
                            })
                        else:
                            # Old format (flat dict)
                            thumb = None
                            if item.get('thumbnail'):
                                resolutions = item['thumbnail'].get('resolutions', [])
                                if resolutions:
                                    thumb = resolutions[0].get('url')
                            cleaned.append({
                                'title': item.get('title', ''),
                                'link': item.get('link', ''),
                                'publisher': item.get('publisher', ''),
                                'publish_time': item.get('providerPublishTime'),
                                'type': item.get('type', ''),
                                'thumbnail': thumb,
                            })
                    logger.info(f'Yahoo: fetched {len(cleaned)} news for {asset_id}')
                    return cleaned
                return None
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo news attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return None

    @staticmethod
    def batch_download_ohlcv(asset_ids: list, timeframe: str = '1D',
                             period: str = '') -> dict:
        """Batch download OHLCV for multiple tickers using yf.download().

        Much faster than individual ticker.history() calls (single HTTP request).
        Returns: {asset_id: [candle_dicts], ...}
        """
        import yfinance as yf
        import math

        yf_interval = _TF_MAP.get(timeframe, '1d')
        yf_period = period or _PERIOD_MAP.get(timeframe, '1y')

        yahoo_tickers = {cid: _to_yahoo_ticker(cid) for cid in asset_ids}
        ticker_str = ' '.join(yahoo_tickers.values())

        try:
            df = yf.download(ticker_str, period=yf_period, interval=yf_interval,
                             group_by='ticker', threads=True, progress=False)
        except Exception as e:
            logger.error(f'Yahoo batch download failed: {e}')
            return {}

        results = {}
        for asset_id, yahoo_sym in yahoo_tickers.items():
            try:
                if len(asset_ids) == 1:
                    sub_df = df
                else:
                    sub_df = df[yahoo_sym] if yahoo_sym in df.columns.get_level_values(0) else None

                if sub_df is None or sub_df.empty:
                    results[asset_id] = []
                    continue

                sub_df = sub_df.dropna(subset=['Open', 'High', 'Low', 'Close'])
                candles = []
                for idx, row in sub_df.iterrows():
                    o = float(row['Open'])
                    h = float(row['High'])
                    l = float(row['Low'])
                    c = float(row['Close'])
                    v = float(row['Volume']) if not math.isnan(row['Volume']) else 0.0
                    if any(math.isnan(x) or math.isinf(x) for x in (o, h, l, c)):
                        continue
                    candles.append({
                        'timestamp': int(idx.timestamp()),
                        'open': o, 'high': h, 'low': l, 'close': c, 'volume': v,
                    })
                results[asset_id] = candles
            except Exception as e:
                logger.warning(f'Yahoo batch parse error for {asset_id}: {e}')
                results[asset_id] = []

        logger.info(f'Yahoo batch download: {len(results)} tickers, '
                    f'{sum(len(v) for v in results.values())} total candles')
        return results

    def get_options_data(self, asset_id: str) -> dict:
        """Fetch options chain data: expiration dates + chains for nearest dates.

        Returns dict with keys: options_dates (list of expiration date strings),
        options_chain (list of {expiration, calls, puts} for first 3 dates).
        """
        result = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)

                # Get expiration dates (tuple of date strings like '2025-03-21')
                exp_dates = ticker.options
                if exp_dates:
                    result['options_dates'] = list(exp_dates)

                    # Fetch option chains for first 3 expiration dates
                    chains = []
                    for exp in list(exp_dates)[:3]:
                        try:
                            chain = ticker.option_chain(exp)
                            calls = None
                            puts = None
                            if chain.calls is not None and not chain.calls.empty:
                                calls = _df_to_rows(chain.calls.head(30))
                            if chain.puts is not None and not chain.puts.empty:
                                puts = _df_to_rows(chain.puts.head(30))
                            chains.append({
                                'expiration': exp,
                                'calls': calls,
                                'puts': puts,
                            })
                        except Exception as chain_err:
                            logger.debug(f'Yahoo: option chain failed for {asset_id}/{exp}: {chain_err}')
                    result['options_chain'] = chains

                    logger.info(f'Yahoo: fetched options data for {asset_id} '
                                f'({len(exp_dates)} dates, {len(chains)} chains)')
                else:
                    logger.info(f'Yahoo: no options data for {asset_id}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo options attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    # ── Estimate data (growth, revenue, earnings, EPS) ─────────────

    def get_estimate_data(self, asset_id: str) -> dict:
        """Fetch analyst estimate data: growth, revenue, earnings, EPS trend/revisions.

        Returns dict with up to 5 keys:
          growth_estimates, revenue_estimate, earnings_estimate, eps_trend, eps_revisions
        """
        result: dict = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)

                for attr, key in [
                    ('growth_estimates', 'growth_estimates'),
                    ('revenue_estimate', 'revenue_estimate'),
                    ('earnings_estimate', 'earnings_estimate'),
                    ('eps_trend', 'eps_trend'),
                    ('eps_revisions', 'eps_revisions'),
                ]:
                    try:
                        data = getattr(ticker, attr, None)
                        if data is not None:
                            if hasattr(data, 'empty') and not data.empty:
                                result[key] = _df_to_dict(data)
                            elif isinstance(data, dict) and data:
                                result[key] = {k: _safe_val(v) for k, v in data.items()}
                    except Exception as attr_err:
                        logger.debug(f'Yahoo: {attr} failed for {asset_id}: {attr_err}')

                logger.info(f'Yahoo: fetched {len(result)} estimate types for {asset_id}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo estimates attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    # ── Sustainability / ESG ───────────────────────────────────────

    def get_sustainability(self, asset_id: str) -> dict:
        """Fetch ESG / sustainability scores.

        Returns dict with key 'sustainability' containing ESG data.
        """
        result: dict = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)
                sus = ticker.sustainability
                if sus is not None:
                    if hasattr(sus, 'empty') and not sus.empty:
                        # sustainability is a DataFrame with single column 'Value'
                        result['sustainability'] = _df_to_rows(sus.reset_index()) if hasattr(sus, 'reset_index') else _df_to_dict(sus)
                    elif isinstance(sus, dict) and sus:
                        result['sustainability'] = {k: _safe_val(v) for k, v in sus.items()}
                logger.info(f'Yahoo: sustainability for {asset_id}: '
                            f'{"found" if result else "empty"}')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo sustainability attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    # ── SEC Filings ────────────────────────────────────────────────

    def get_sec_filings(self, asset_id: str) -> dict:
        """Fetch SEC filing links (10-K, 10-Q, 8-K, etc).

        Returns dict with key 'sec_filings' containing list of filing records.
        """
        result: dict = {}
        for attempt in range(self._retry_max):
            try:
                ticker = self._get_ticker(asset_id)
                filings = ticker.sec_filings
                if filings is not None:
                    if hasattr(filings, 'empty') and not filings.empty:
                        result['sec_filings'] = _df_to_rows(filings.head(50))
                    elif isinstance(filings, list) and filings:
                        cleaned = []
                        for f in filings[:50]:
                            if isinstance(f, dict):
                                cleaned.append({k: _safe_val(v) for k, v in f.items()})
                            else:
                                cleaned.append(str(f))
                        result['sec_filings'] = cleaned
                    elif isinstance(filings, dict) and filings:
                        result['sec_filings'] = {k: _safe_val(v) for k, v in filings.items()}
                logger.info(f'Yahoo: sec_filings for {asset_id}: '
                            f'{len(result.get("sec_filings", []))} filings')
                return result
            except Exception as e:
                delay = self._retry_base_delay * (2 ** attempt)
                logger.warning(f'Yahoo sec_filings attempt {attempt + 1}/{self._retry_max} '
                               f'failed for {asset_id}: {e}')
                if attempt < self._retry_max - 1:
                    time.sleep(delay)
        return result

    # ── Market-level: Market Status & Calendars ────────────────────

    @staticmethod
    def get_market_status(market_key: str = 'us_market') -> dict | None:
        """Fetch market status (open/close hours) using yf.Market().

        market_key examples: us_market, gb_market, de_market, jp_market
        """
        try:
            import yfinance as yf
            market = yf.Market(market_key)
            data = {'key': market_key}

            for attr in ['status', 'summary']:
                try:
                    val = getattr(market, attr, None)
                    if val is not None:
                        if hasattr(val, 'to_dict'):
                            data[attr] = val.to_dict()
                        elif hasattr(val, 'empty'):
                            data[attr] = _df_to_rows(val) if not val.empty else None
                        else:
                            data[attr] = val
                except Exception:
                    pass

            return data if len(data) > 1 else None
        except Exception as e:
            logger.warning(f'Yahoo: Market({market_key}) failed: {e}')
            return None

    @staticmethod
    def get_market_calendars() -> dict | None:
        """Fetch market-wide calendars (earnings, IPO, splits, economic events).

        Uses yf.Calendars to get upcoming market events.
        """
        try:
            import yfinance as yf
            import pandas as pd
            cals = yf.Calendars()
            data = {}

            for attr in ['earnings', 'ipo', 'splits', 'economic_events']:
                try:
                    val = getattr(cals, attr, None)
                    if val is not None:
                        if isinstance(val, pd.DataFrame) and not val.empty:
                            data[attr] = _df_to_rows(val.head(50))
                        elif isinstance(val, list) and val:
                            data[attr] = val[:50]
                        elif isinstance(val, dict) and val:
                            data[attr] = val
                except Exception as cal_err:
                    logger.debug(f'Yahoo: Calendars.{attr} failed: {cal_err}')

            return data if data else None
        except Exception as e:
            logger.warning(f'Yahoo: Calendars failed: {e}')
            return None

    @staticmethod
    def get_sector_data(sector_key: str) -> dict | None:
        """Fetch sector-level data using yf.Sector()."""
        try:
            import yfinance as yf
            import pandas as pd
            sector = yf.Sector(sector_key)
            data = {'name': getattr(sector, 'name', sector_key), 'key': sector_key}
            if hasattr(sector, 'overview'):
                ov = sector.overview
                if isinstance(ov, pd.DataFrame):
                    data['overview'] = _df_to_dict(ov)
                elif isinstance(ov, dict):
                    data['overview'] = ov
            if hasattr(sector, 'top_companies'):
                tc = sector.top_companies
                if isinstance(tc, pd.DataFrame) and not tc.empty:
                    # Reset index so symbol becomes a column
                    tc_reset = tc.reset_index()
                    data['top_companies'] = _df_to_rows(tc_reset)
                elif isinstance(tc, list):
                    data['top_companies'] = tc
            if hasattr(sector, 'top_etfs'):
                te = sector.top_etfs
                if isinstance(te, pd.DataFrame) and not te.empty:
                    data['top_etfs'] = _df_to_rows(te.reset_index())
                elif isinstance(te, dict):
                    # Convert {ticker: name} dict to list of rows
                    data['top_etfs'] = [{'symbol': k, 'name': v} for k, v in te.items()]
                elif isinstance(te, list):
                    data['top_etfs'] = te
            logger.info(f'Yahoo: fetched sector data for {sector_key}')
            return data
        except Exception as e:
            logger.warning(f'Yahoo sector data failed for {sector_key}: {e}')
            return None

    @staticmethod
    def get_industry_data(industry_key: str) -> dict | None:
        """Fetch industry-level data using yf.Industry()."""
        try:
            import yfinance as yf
            import pandas as pd
            industry = yf.Industry(industry_key)
            data = {'name': getattr(industry, 'name', industry_key), 'key': industry_key}
            if hasattr(industry, 'overview'):
                ov = industry.overview
                if isinstance(ov, pd.DataFrame):
                    data['overview'] = _df_to_dict(ov)
                elif isinstance(ov, dict):
                    data['overview'] = ov
            for attr, out_key in [('top_companies', 'top_companies'),
                                   ('top_performing_companies', 'top_performing'),
                                   ('top_growth_companies', 'top_growth')]:
                val = getattr(industry, attr, None)
                if isinstance(val, pd.DataFrame) and not val.empty:
                    data[out_key] = _df_to_rows(val.reset_index())
                elif isinstance(val, list):
                    data[out_key] = val
            logger.info(f'Yahoo: fetched industry data for {industry_key}')
            return data
        except Exception as e:
            logger.warning(f'Yahoo industry data failed for {industry_key}: {e}')
            return None

    @staticmethod
    def get_predefined_screeners() -> dict:
        """Get predefined screener query names from yfinance."""
        try:
            import yfinance as yf
            if hasattr(yf, 'PREDEFINED_SCREENER_QUERIES'):
                return yf.PREDEFINED_SCREENER_QUERIES
            return {}
        except Exception as e:
            logger.warning(f'Yahoo predefined screeners failed: {e}')
            return {}
