- add te_thailand collector (TradingEconomics) -> 8 factors: interest rate, business loan growth, consumer credit, household debt/GDP, retail sales YoY, consumer confidence, residential property prices, business confidence; feed banks/retail/consumer_staples/nonbank_finance/property/telecom/healthcare - add thai_trade collector (TradingEconomics external sector) -> exports/ imports/current-account factors (concurrent in-tree work, verified green) - fix sign inversion: theme weights were negative on sign:-1 factors (NPL, inflation, unemployment) so higher NPL/inflation RAISED scores; direction now lives only in factor sign, theme weights positive (regression-locked) - tests: te_thailand parse+direction, value-key resolution contract, dashboard 8-sources, scheduler vintage counts; suite 362 OK
210 lines
9.2 KiB
Python
210 lines
9.2 KiB
Python
"""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"<td[^>]*>(.*?)</td>", row_html, re.S)
|
|
if td.strip()
|
|
]
|
|
|
|
|
|
def _to_float(text: str) -> Optional[float]:
|
|
text = text.replace(",", "").strip().replace("%", "")
|
|
if not text or text in ("-", "N/A", "…"):
|
|
return None
|
|
try:
|
|
return float(text)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _related_indicators(html_text: str) -> dict[str, dict]:
|
|
"""Map normalized-label -> {value, prev, unit, period} from the related table.
|
|
|
|
Rows are `[label, value, prev, unit, period]` (5 cols). Keys are
|
|
lowercased + stripped so a TradingEconomics label tweak (e.g. extra space /
|
|
case change) does not silently drop a series. A label may appear more than
|
|
once; first row that parses a numeric value wins.
|
|
"""
|
|
out: dict[str, dict] = {}
|
|
for table in re.findall(r"<table[^>]*>(.*?)</table>", html_text, re.S):
|
|
for row_html in re.findall(r"<tr[^>]*>(.*?)</tr>", table, re.S):
|
|
cells = _cells(row_html)
|
|
if len(cells) < 4:
|
|
continue
|
|
key = cells[0].strip().lower()
|
|
value = _to_float(cells[1])
|
|
if value is None:
|
|
continue
|
|
if key not in out or out[key].get("value") is None:
|
|
out[key] = {
|
|
"value": value,
|
|
"prev": _to_float(cells[2]) if len(cells) > 2 else None,
|
|
"unit": cells[3] if len(cells) > 3 else "",
|
|
"period": cells[4] if len(cells) > 4 else "",
|
|
}
|
|
return out
|
|
|
|
|
|
def parse_te_thailand_html(interest_html: str, confidence_html: str,
|
|
housing_html: str = "", bizconf_html: str = "") -> ThaiFactorsSnapshot:
|
|
"""Parse the TradingEconomics Thailand pages into one snapshot.
|
|
|
|
`housing_html` + `bizconf_html` are optional (Phase B property/business
|
|
factors); when omitted those two factors are simply None.
|
|
"""
|
|
ir = _related_indicators(interest_html)
|
|
cc = _related_indicators(confidence_html)
|
|
hi = _related_indicators(housing_html) if housing_html else {}
|
|
bc = _related_indicators(bizconf_html) if bizconf_html else {}
|
|
|
|
periods: dict = {}
|
|
snap = ThaiFactorsSnapshot(
|
|
interest_rate_pct=(ir.get("interest rate") or {}).get("value"),
|
|
loans_to_fin_corp=(ir.get("loans to non financial corporations") or {}).get("value"),
|
|
consumer_credit_thbmn=(cc.get("consumer credit") or {}).get("value"),
|
|
household_debt_gdp_pct=(cc.get("households debt to gdp") or {}).get("value"),
|
|
retail_sales_yoy=(cc.get("retail sales yoy") or {}).get("value"),
|
|
consumer_confidence=(cc.get("consumer confidence") or {}).get("value"),
|
|
# consumer_spending is surfaced here for display/debug only — it is not
|
|
# registered as a FACTORS factor, so it does not feed a theme surprise.
|
|
consumer_spending=(cc.get("consumer spending") or {}).get("value"),
|
|
property_prices_yoy=(hi.get("residential property prices") or {}).get("value"),
|
|
business_confidence=(bc.get("business confidence") or {}).get("value"),
|
|
)
|
|
for key, label, src in (
|
|
("interest_rate_pct", "interest rate", ir),
|
|
("retail_sales_yoy", "retail sales yoy", cc),
|
|
("consumer_confidence", "consumer confidence", cc),
|
|
("property_prices_yoy", "residential property prices", hi),
|
|
("business_confidence", "business confidence", bc),
|
|
):
|
|
meta = src.get(label) or {}
|
|
if meta and meta.get("period"):
|
|
periods[key] = meta.get("period")
|
|
|
|
if snap.interest_rate_pct is None and snap.consumer_confidence is None \
|
|
and snap.retail_sales_yoy is None:
|
|
raise ThaiFactorsError(
|
|
"no usable Thailand factor series found in TradingEconomics pages"
|
|
)
|
|
# attach parsed periods onto a copy (dataclass is frozen)
|
|
return ThaiFactorsSnapshot(
|
|
interest_rate_pct=snap.interest_rate_pct,
|
|
loans_to_fin_corp=snap.loans_to_fin_corp,
|
|
consumer_credit_thbmn=snap.consumer_credit_thbmn,
|
|
household_debt_gdp_pct=snap.household_debt_gdp_pct,
|
|
retail_sales_yoy=snap.retail_sales_yoy,
|
|
consumer_confidence=snap.consumer_confidence,
|
|
consumer_spending=snap.consumer_spending,
|
|
property_prices_yoy=snap.property_prices_yoy,
|
|
business_confidence=snap.business_confidence,
|
|
periods=periods,
|
|
)
|
|
|
|
|
|
def fetch_te_thailand(timeout: float = 30.0) -> ThaiFactorsSnapshot:
|
|
interest_html = _fetch(_URL_INTEREST, timeout=timeout)
|
|
confidence_html = _fetch(_URL_CONFIDENCE, timeout=timeout)
|
|
housing_html = _fetch(_URL_HOUSING, timeout=timeout)
|
|
bizconf_html = _fetch(_URL_BIZCONF, timeout=timeout)
|
|
return parse_te_thailand_html(interest_html, confidence_html,
|
|
housing_html, bizconf_html)
|