- 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
106 lines
4.5 KiB
Python
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()
|