DEV Community

SM
SM

Posted on

Adding Commodity-to-Equity Linkage to a Macro Stress Monitor

I recently launched a rules-based macro stress monitor for African and LatAm economies on Apify. It pulls from central bank APIs, the World Bank, IMF IFS, and the Pink Sheet, and returns structured JSON covering FX momentum, inflation, commodity terms-of-trade, reserves, and structural vulnerability for each country.

The commodity signal already told me which countries were benefiting from or getting hurt by commodity price moves. What it didn't tell me was who — which listed companies sit in the direct path of that move.

So I built a second layer: companySignals.


The Problem Worth Solving

The stress monitor already computed something like this for Colombia:

{
  "commodityCode": "CRUDE_OIL_AVG",
  "commodityName": "Crude oil",
  "yoyPct": 60.05,
  "dependencePct": 30,
  "exportImpactPct": 18.02
}
Enter fullscreen mode Exit fullscreen mode

Oil up 60% YoY, 30% of Colombia's export basket, +18pp terms-of-trade impact. The country-level signal was clear. But "Colombia benefits from oil prices" tells you nothing actionable about where that benefit flows at the company level.

The obvious answer is Ecopetrol. But what about Chile and copper? Peru has both copper and gold firing simultaneously — which companies have exposure to both, and how do you represent that without duplicating entries?

Those were the design questions.


The Data Model

Static reference file

The company data is a static JSON reference file — not live-scraped. Sourced from Wikipedia sector listings, stock exchange sector pages (JSE, BVL, B3, BVC, NGX, GSE), and company investor relations pages. Researched once, committed to the repo, updated manually when something changes.

Structure per pair:

{
  "country": "CL",
  "commodityCode": "COPPER",
  "companies": [
    {
      "name": "Antofagasta plc",
      "ticker": "ANTO",
      "exchange": "LSE: ANTO",
      "exposureTier": "primary",
      "note": "Chilean copper pure-play (Luksic group) operating Los Pelambres, Centinela, Antucoya and Zaldivar."
    },
    {
      "name": "BHP Group Limited",
      "ticker": "BHP",
      "exchange": "ASX & NYSE & LSE: BHP",
      "exposureTier": "significant",
      "note": "Operates Escondida, the world's largest copper mine, plus Spence in northern Chile."
    }
  ],
  "dataYear": 2025,
  "source": "JSE/LSE sector listings + Wikipedia"
}
Enter fullscreen mode Exit fullscreen mode

exposureTier is per-commodity, not per-company. That distinction matters — a company can be primary on one commodity and significant on another. Buenaventura in Peru is the clearest example.


The Aggregation Problem: Buenaventura

Peru's commodity tailwind flags are firing on both copper (+42% YoY, +13.5pp impact) and gold (+39% YoY, +8.1pp impact) simultaneously. Buenaventura appears in both reference lists — as primary for gold and significant for copper.

A flat schema would return two separate Buenaventura entries, which is misleading. The aggregated schema groups by company and nests commodity signals:

{
  "name": "Compania de Minas Buenaventura",
  "ticker": "BVN",
  "exchange": "NYSE: BVN; BVL: BUENAVC1",
  "signalCount": 2,
  "commoditySignals": [
    {
      "commodityCode": "COPPER",
      "commodityName": "Copper",
      "exposureTier": "significant",
      "note": "Copper exposure via ~19% stake in Cerro Verde and El Brocal mine.",
      "yoyPct": 42.07,
      "exportImpactPct": 13.46,
      "direction": "tailwind"
    },
    {
      "commodityCode": "GOLD",
      "commodityName": "Gold",
      "exposureTier": "primary",
      "note": "Peru's largest publicly traded precious-metals miner.",
      "yoyPct": 38.61,
      "exportImpactPct": 8.11,
      "direction": "tailwind"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

signalCount is the sortable/filterable field — "show me companies touched by more than one of this week's commodity moves" is now a one-line filter.


Trigger Logic

companySignals only populates when a commodityShock or commodity_tailwind compound flag fires for that (country, commodityCode) pair. On quiet weeks when no flag fires, the field is absent — it doesn't try to manufacture a story.

The flag check runs against the compound flags already computed by the stress monitor:

"compoundFlags": ["commodity_tailwind:Copper+13.5pp,Gold+8.1pp"]
Enter fullscreen mode Exit fullscreen mode

If the flag fires, look up the (country, commodityCode) pair in the reference file. If a match exists, attach the companies. If not, omit the field silently. No errors, no empty arrays.

The direction field (tailwind or headwind) is derived automatically from the price change sign and whether the country is a net exporter of that commodity.


What the Output Actually Found

The interesting case is not "company benefits from tailwind."

That's obvious — Ecopetrol benefits from oil prices. Anyone following Colombian equities already knows this.

The interesting case is the gap between the country-level macro signal and the company-level reality.

Colombia has a commodity tailwind and the calmest acute stress reading in the region (1.1/100). But Colombia also has the highest structural vulnerability in the dataset (74.6/100), with debt service consuming 43% of export earnings and the IMF Flexible Credit Line cancelled in October 2025.

Ecopetrol is majority state-owned. S&P downgraded Ecopetrol's credit rating alongside Colombia's sovereign rating in April 2026 — explicitly because the rating is capped by the sovereign due to Ecopetrol's importance to government revenue.

Meanwhile, Citigroup downgraded Ecopetrol in June 2026 citing production decline. A union launched a 24-hour strike. The stock moved on presidential election news.

So companySignals correctly flags Ecopetrol as exposed to Colombia's oil tailwind. But the real story is that the company-level factors (production, labor, politics) are currently dominating over the commodity price move. The macro signal is a research hook, not a standalone buy/sell signal.

That's why the output includes a direction field and an exposureTier but deliberately does not include a price target, a recommendation, or a "benefit score." The design is intentionally a starting point for research, not a conclusion.


Coverage So Far

LatAm (7 countries, 9 pairs):

  • Peru — Copper, Gold
  • Chile — Copper
  • Colombia — Crude Oil
  • Brazil — Crude Oil, Soybeans
  • Argentina — Soybeans
  • Uruguay — Beef, Soybeans

Africa (8 pairs, in progress):

  • South Africa — Platinum, Gold
  • Ghana — Gold, Crude Oil
  • Nigeria — Crude Oil
  • Zambia — Copper
  • Tanzania — Gold
  • Côte d'Ivoire — Cocoa (first headwind test case)

35+ companies across both regions, all with real verifiable tickers, at least one major/well-known name per pair as a credibility anchor.


What's Next

The static reference file is the right call for v1 — no API dependency, no scraping, predictable output. The gap is that it only covers pairs I've researched manually. The actor returns nothing (rather than erroring) for unflagged or unresearched pairs, which is the honest behaviour — better to omit than to fabricate.

The next analytical layer is peer-relative z-scores: instead of comparing each country only to its own history, compare it to regional peers on the same run. Colombia's structural vulnerability of 74.6 is striking in isolation; it's even more striking when Chile — riding the same commodity tailwind — sits at 5.5. That cross-country comparison is pure derivation from data already in the dataset.

Actor links:

Top comments (0)