Files
set50-system/backend/tests/test_energy_irpc.py
Kunthawat Greethong 12b34929d7 feat(factor): add energy_irpc (IRPC net margin) as 2nd Thai refiner signal
- new energy_irpc collector parsing IRPC performance-highlights table
  (net profit/EBITDA/ROE margins, latest period 3M26: +10.27%)
- factor energy_irpc_net_margin (sign +1) wired into refining_energy/
  exploration/utilities, extending the energy theme beyond TOP
- scheduler job + dashboard fetch + sources table row (now 9 sources)
- tests: parse (incl paren-negatives), value-key resolution, direction;
  suite 368 OK. Independent review passed: true
- Phase B feasibility: REIC/EPPO/NBTC/PTTEP are JS-rendered or anti-bot
  (recorded deferred in plan); IRPC was the clean server-rendered win
2026-08-29 11:13:17 +07:00

106 lines
4.5 KiB
Python

"""Tests for the IRPC refining-margin collector + theme wiring."""
from __future__ import annotations
import unittest
from app import energy_irpc, themes
from app.energy_irpc import EnergyIrpcSnapshot, parse_energy_irpc_html
_PERF_HTML = """<table>
<tr><td>Financial Highlights</td><td>2024</td><td>2025</td><td>3M26</td></tr>
<tr><td>Current Assets</td><td>56,999</td><td>67,086</td><td>101,978</td></tr>
<tr><td>Total Assets</td><td>184,555</td><td>187,383</td><td>217,356</td></tr>
<tr><td>EBITDA Margin</td><td>1.42%</td><td>2.22%</td><td>19.19%</td></tr>
<tr><td>Net Profit Margin</td><td>(1.65%)</td><td>(1.28%)</td><td>10.27%</td></tr>
<tr><td>Return on Equity</td><td>(7.12%)</td><td>(5.26%)</td><td>7.75%</td></tr>
</table>"""
class EnergyIrpcParseTest(unittest.TestCase):
def test_parses_margins(self):
snap = parse_energy_irpc_html(_PERF_HTML)
self.assertIsInstance(snap, EnergyIrpcSnapshot)
self.assertEqual(snap.columns, ["2024", "2025", "3M26"])
self.assertEqual(snap.net_margin_pct, 10.27)
self.assertEqual(snap.ebitda_margin_pct, 19.19)
self.assertEqual(snap.roe_pct, 7.75)
# latest period is the last column
self.assertEqual(snap.period, "3M26")
def test_parses_negative_parens(self):
# "(1.65%)" -> -1.65
snap = parse_energy_irpc_html(_PERF_HTML)
# net_margin picks the LAST column (3M26, +10.27), not the negative one;
# verify the paren parser separately on a table where the last col is negative
html_neg = _PERF_HTML.replace("<td>10.27%</td>", "<td>(3.20%)</td>")
snap2 = parse_energy_irpc_html(html_neg)
self.assertEqual(snap2.net_margin_pct, -3.20)
def test_to_dict_full(self):
d = parse_energy_irpc_html(_PERF_HTML).to_dict()
self.assertIn("net_margin_pct", d)
self.assertIn("period", d)
self.assertIn("source", d)
def test_missing_values_raise(self):
html_no = "<table><tr><td>Nothing</td><td>1</td></tr></table>"
with self.assertRaises(energy_irpc.EnergyIrpcError):
parse_energy_irpc_html(html_no)
def test_every_factor_value_key_resolves(self):
from app import factors
keys = set(EnergyIrpcSnapshot().to_dict().keys())
for fkey, fact in factors.FACTORS.items():
if fact.get("fetch") != "energy_irpc":
continue
self.assertIn(
fact.get("value_key"), keys,
f"factor {fkey!r} value_key not emitted by energy_irpc",
)
class EnergyIrpcThemeDirectionTest(unittest.TestCase):
"""IRPC margin must move energy themes the intended (bullish) direction."""
@staticmethod
def _fetched(**irpc):
base = {
"macro_thai": {
"private_consumption_yoy": 4.9, "private_investment_yoy": 18.1,
"headline_inflation_yoy": 1.95, "core_inflation_yoy": 1.0,
"unemployment_pct": 1.0, "manufacturing_yoy": -3.1,
"tourists_ytd_mn": 16.2,
},
"auto_credit": {"new_car_sales_yoy": 20.07, "vehicle_production": 117383.0,
"auto_exports": 81526.0},
"auto_npl": {"pct_of_npls": 3.0},
"bank_npl": {"pct_of_npls": 1.0},
"energy_thai": {"quarterly": {"Q1/2026": {"net_profit": 19481.0, "sales": 114809.0}}},
"energy_irpc": {"net_margin_pct": irpc.get("net", 0.0)},
"thai_trade": {"current_account_usdm": 500.0, "exports_usdm": 34000.0,
"imports_usdm": 38000.0},
"te_thailand": {
"interest_rate_pct": 1.5, "loans_to_fin_corp": 10000000.0,
"consumer_credit_thbmn": 5000000.0, "household_debt_gdp_pct": 85.0,
"retail_sales_yoy": 0.0, "consumer_confidence": 50.0,
"property_prices_yoy": 0.0, "business_confidence": 50.0,
},
}
# refresh energy_irpc from kwargs (neutral default 0.0 used above)
base["energy_irpc"] = {"net_margin_pct": irpc.get("net", 0.0)}
return base
def test_higher_irpc_margin_raises_energy_themes(self):
from app import themes
low = themes.compute_theme_surprises(self._fetched(net=-3.0))
high = themes.compute_theme_surprises(self._fetched(net=10.0))
self.assertGreater(high["refining_energy"], low["refining_energy"])
self.assertGreater(high["exploration"], low["exploration"])
self.assertGreater(high["utilities"], low["utilities"])
if __name__ == "__main__":
unittest.main()