diff --git a/backend/app/__init__.py b/backend/app/__init__.py
index d280d59..a92c512 100644
--- a/backend/app/__init__.py
+++ b/backend/app/__init__.py
@@ -627,6 +627,17 @@ def create_app(config: dict[str, Any] | None = None) -> Flask:
except Exception as exc:
theme_reads["auto_credit"] = {"source": "tradingeconomics", "error": str(exc), "frequency": "monthly"}
+ # auto NPL (credit-quality) from BOT — deepens auto theme
+ try:
+ from app import auto_npl
+ npl = cache.fetch_or_stale(
+ "auto_npl", lambda: auto_npl.fetch_auto_npl().to_dict())
+ npl_d = npl["data"] if isinstance(npl, dict) and "data" in npl else npl
+ theme_reads["auto_credit"]["auto_npl_pct"] = npl_d.get("pct_of_npls")
+ theme_reads["auto_credit"]["auto_npl_amount"] = npl_d.get("npl_amount")
+ except Exception:
+ pass
+
# refining_energy: Thai Oil (TOP) quarterly financials
try:
en = cache.fetch_or_stale(
diff --git a/backend/app/auto_npl.py b/backend/app/auto_npl.py
new file mode 100644
index 0000000..b7d3ca1
--- /dev/null
+++ b/backend/app/auto_npl.py
@@ -0,0 +1,118 @@
+"""Auto NPL / credit-quality factor — BOT Gross NPLs by business type.
+
+Source: https://app.bot.or.th/BTWS_STAT/statistics/ReportPage.aspx?reportID=794
+(Bank of Thailand, FI_NP_003_S2 — Gross NPLs outstanding classified by business
+type, ISIC Rev.4). Server-rendered HTML table.
+
+Table layout (quarterly, 3 metrics per period):
+ header2: [ , , 'ยอดคงค้าง NPL', '% ต่อ NPLs', '% ต่อสินเชื่อรวม', (repeat) ]
+ row: [no, sector_label, NPL_amount, pct_of_npls, pct_of_loans, (prev period repeat) ]
+
+This module extracts the **auto loan NPL** (sector 'รถยนต์') — the credit-quality
+component that deepens the auto_credit theme beyond just new-car sales.
+"""
+
+from __future__ import annotations
+
+import html
+import re
+from dataclasses import dataclass
+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 = "https://app.bot.or.th/BTWS_STAT/statistics/ReportPage.aspx?reportID=794"
+AUTO_LOAN_LABEL = "รถยนต์"
+
+
+class AutoNplError(Exception):
+ """Raised when the BOT NPL page cannot be fetched or parsed."""
+
+
+@dataclass(frozen=True)
+class AutoNplSnapshot:
+ # latest period auto-loan NPL (million baht), % of NPLs, % of total loans
+ npl_amount: Optional[float] = None
+ pct_of_npls: Optional[float] = None # % of total NPLs
+ pct_of_loans: Optional[float] = None # % of total loans
+ period: str = ""
+ source: str = "bot"
+
+ def to_dict(self) -> dict:
+ return {
+ "source": self.source,
+ "period": self.period,
+ "npl_amount": self.npl_amount,
+ "pct_of_npls": self.pct_of_npls,
+ "pct_of_loans": self.pct_of_loans,
+ }
+
+
+def _fetch(url: str = _URL, 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 AutoNplError(f"failed to fetch {url}: {exc}") from exc
+ try:
+ return raw.decode("utf-8")
+ except UnicodeDecodeError:
+ return raw.decode("latin-1", "ignore")
+
+
+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 _cells(row_html: str) -> list[str]:
+ return [
+ html.unescape(re.sub(r"<[^>]+>", "", td)).strip()
+ for td in re.findall(r"
| Q2/2568 | Q1/2568 | ||||||
|---|---|---|---|---|---|---|---|
| ยอดคงค้าง NPL | % ต่อ NPLs | % ต่อสินเชื่อรวม | ยอดคงค้าง NPL | % ต่อ NPLs | % ต่อสินเชื่อรวม | ||
| 1 | การเกษตร | 10,325 | 1.98 | 11.81 | 10,669 | 2.07 | 11.94 |
| 12 | รถยนต์ | 20,602 | 3.95 | 2.06 | 22,046 | 4.10 | 2.15 |