"""Thailand rates / credit / retail / confidence / property factors — TradingEconomics. Source pages (server-rendered HTML, same infrastructure as `thai_trade` / `auto_credit` — the "related indicators" table exposes a stable 5-column row `[label, value, prev, unit, period]`): - https://tradingeconomics.com/thailand/interest-rate Interest Rate, Loans to Non Financial Corporations, Banks Balance Sheet - https://tradingeconomics.com/thailand/consumer-confidence Consumer Confidence, Retail Sales YoY, Consumer Credit, Households Debt to GDP, Consumer Spending - https://tradingeconomics.com/thailand/housing-index Residential Property Prices, Housing Index, Housing Starts - https://tradingeconomics.com/thailand/business-confidence Business Confidence, Leading Economic Index, Private Investment MoM This module deepens the macro-proxy themes that today rely only on the one BOT Thai-Economy page: Factor contributes to ---------------------------- ------------------------------------- te_interest_rate -> banks (rate/credit cycle) te_loan_growth_level -> banks (credit demand) te_consumer_credit -> nonbank_finance (household credit book) te_household_debt_gdp -> nonbank_finance (household leverage risk) te_retail_sales_yoy -> retail, consumer_staples (spending) te_consumer_confidence -> retail, consumer_staples, nonbank (sentiment) te_property_prices -> property (residential price YoY) te_business_confidence -> telecom_it, property, healthcare (sentiment) Every value is a single-page snapshot (current value + prev + period), so each factor is a *level / latest-period* driver — no history join required. """ from __future__ import annotations import html import re from dataclasses import dataclass, field from typing import Optional from urllib.request import Request, urlopen _USER_AGENT = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" ) _URL_INTEREST = "https://tradingeconomics.com/thailand/interest-rate" _URL_CONFIDENCE = "https://tradingeconomics.com/thailand/consumer-confidence" _URL_HOUSING = "https://tradingeconomics.com/thailand/housing-index" _URL_BIZCONF = "https://tradingeconomics.com/thailand/business-confidence" class ThaiFactorsError(Exception): """Raised when a TradingEconomics Thailand page cannot be fetched/parsed.""" @dataclass(frozen=True) class ThaiFactorsSnapshot: interest_rate_pct: Optional[float] = None # policy-rate proxy (Aug 2026: 1.00) loans_to_fin_corp: Optional[float] = None # THB mn (Jun 2026) consumer_credit_thbmn: Optional[float] = None # THB mn (Jun 2025) household_debt_gdp_pct: Optional[float] = None # % of GDP (Dec 2025) retail_sales_yoy: Optional[float] = None # % YoY (May 2026) consumer_confidence: Optional[float] = None # points (Jul 2026) consumer_spending: Optional[float] = None # THB mn (Jun 2026) property_prices_yoy: Optional[float] = None # residential prices % YoY (Mar 2026) business_confidence: Optional[float] = None # points (Jul 2026) periods: dict = field(default_factory=dict) source: str = "tradingeconomics.thai-factors" def to_dict(self) -> dict: return { "source": self.source, "interest_rate_pct": self.interest_rate_pct, "loans_to_fin_corp": self.loans_to_fin_corp, "consumer_credit_thbmn": self.consumer_credit_thbmn, "household_debt_gdp_pct": self.household_debt_gdp_pct, "retail_sales_yoy": self.retail_sales_yoy, "consumer_confidence": self.consumer_confidence, "consumer_spending": self.consumer_spending, "property_prices_yoy": self.property_prices_yoy, "business_confidence": self.business_confidence, "periods": self.periods, } def _fetch(url: str, timeout: float = 30.0) -> str: req = Request(url, headers={"User-Agent": _USER_AGENT, "Accept": "text/html"}) try: with urlopen(req, timeout=timeout) as resp: raw = resp.read() except Exception as exc: raise ThaiFactorsError(f"failed to fetch {url}: {exc}") from exc try: return raw.decode("utf-8") except UnicodeDecodeError: return raw.decode("latin-1", "ignore") def _cells(row_html: str) -> list[str]: return [ html.unescape(re.sub(r"<[^>]+>", "", td)).strip() for td in re.findall(r"