feat: effective tariff rate source (#1790)

* feat: effective tariff rate source

* fix(trade): extract parse helpers, fix tests, add health monitoring

- Extract htmlToPlainText/toIsoDate/parseBudgetLabEffectiveTariffHtml
  to scripts/_trade-parse-utils.mjs so tests can import directly
- Fix toIsoDate to use month-name lookup instead of fragile
  new Date(\`\${text} UTC\`) which is not spec-guaranteed
- Replace new Function() test reconstruction with direct ESM import
- Add test fixtures for parser patterns 2 and 3 (previously untested)
- Add tariffTrendsUs to health.js STANDALONE_KEYS + SEED_META
  (key trade:tariffs:v1:840:all:10, maxStaleMin 900 = 2.5x the 6h TTL)

* fix(test): update sourceVersion assertion for budgetlab addition

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
This commit is contained in:
Fayez Bast
2026-03-19 01:45:32 +02:00
committed by GitHub
parent 5ba0d8db83
commit cf1fdefe92
17 changed files with 560 additions and 20 deletions

View File

@@ -73,6 +73,7 @@ const STANDALONE_KEYS = {
chokepointTransits: 'supply_chain:chokepoint_transits:v1',
transitSummaries: 'supply_chain:transit-summaries:v1',
thermalEscalation: 'thermal:escalation:v1',
tariffTrendsUs: 'trade:tariffs:v1:840:all:10',
};
const SEED_META = {
@@ -135,6 +136,7 @@ const SEED_META = {
sanctionsPressure: { key: 'seed-meta:sanctions:pressure', maxStaleMin: 720 },
radiationWatch: { key: 'seed-meta:radiation:observations', maxStaleMin: 30 },
thermalEscalation: { key: 'seed-meta:thermal:escalation', maxStaleMin: 240 },
tariffTrendsUs: { key: 'seed-meta:trade:tariffs:v1:840:all:10', maxStaleMin: 900 },
};
// Standalone keys that are populated on-demand by RPC handlers (not seeds).

File diff suppressed because one or more lines are too long

View File

@@ -334,6 +334,8 @@ components:
upstreamUnavailable:
type: boolean
description: True if upstream fetch failed and results may be stale/empty.
effectiveTariffRate:
$ref: '#/components/schemas/EffectiveTariffRate'
description: Response containing tariff trend datapoints.
TariffDataPoint:
type: object
@@ -363,6 +365,26 @@ components:
type: string
description: WTO indicator code used for this datapoint.
description: Single tariff data point for a reporter-partner-product combination.
EffectiveTariffRate:
type: object
properties:
sourceName:
type: string
description: Source name for the effective-rate estimate.
sourceUrl:
type: string
description: Canonical source URL for the estimate/methodology.
observationPeriod:
type: string
description: Human-readable observation period (for example "December 2025").
updatedAt:
type: string
description: ISO 8601 date when the source page was last updated, if known.
tariffRate:
type: number
format: double
description: Effective tariff rate (percentage).
description: Current effective tariff estimate for countries with coverage beyond WTO MFN baselines.
GetTradeFlowsRequest:
type: object
properties:

View File

@@ -25,4 +25,6 @@ message GetTariffTrendsResponse {
string fetched_at = 2;
// True if upstream fetch failed and results may be stale/empty.
bool upstream_unavailable = 3;
// Optional effective tariff snapshot for countries with additional coverage (currently US only).
EffectiveTariffRate effective_tariff_rate = 4;
}

View File

@@ -42,6 +42,20 @@ message TariffDataPoint {
string indicator_code = 7;
}
// Current effective tariff estimate for countries with coverage beyond WTO MFN baselines.
message EffectiveTariffRate {
// Source name for the effective-rate estimate.
string source_name = 1;
// Canonical source URL for the estimate/methodology.
string source_url = 2;
// Human-readable observation period (for example "December 2025").
string observation_period = 3;
// ISO 8601 date when the source page was last updated, if known.
string updated_at = 4;
// Effective tariff rate (percentage).
double tariff_rate = 5;
}
// Bilateral trade flow record for a reporting-partner pair.
message TradeFlowRecord {
// WTO member code of reporting country.

View File

@@ -0,0 +1,79 @@
/**
* Pure parse helpers for trade-data seed scripts.
* Extracted so test files can import directly without new Function() hacks.
*/
export const BUDGET_LAB_TARIFFS_URL = 'https://budgetlab.yale.edu/research/tracking-economic-effects-tariffs';
const MONTH_MAP = {
january: '01', february: '02', march: '03', april: '04',
may: '05', june: '06', july: '07', august: '08',
september: '09', october: '10', november: '11', december: '12',
};
export function htmlToPlainText(html) {
return String(html ?? '')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&quot;/gi, '"')
.replace(/&#39;/gi, '\'')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Convert a human-readable date string like "March 2, 2026" to ISO "2026-03-02".
* Falls back to '' on failure.
*/
export function toIsoDate(value) {
const text = String(value ?? '').trim();
if (!text) return '';
if (/^\d{4}-\d{2}-\d{2}/.test(text)) return text.slice(0, 10);
const m = text.match(/^([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})/);
if (m) {
const mm = MONTH_MAP[m[1].toLowerCase()];
if (mm) return `${m[3]}-${mm}-${m[2].padStart(2, '0')}`;
}
return '';
}
/**
* Parse the Yale Budget Lab tariff-tracking page and extract effective tariff rate.
*
* Tries three patterns in priority order:
* 1. "effective tariff rate reaching X% in [month year]"
* 2. "average effective [U.S.] tariff rate ... to X% ... in/by [month year]"
* 3. Same as 2 but no period capture
*
* Returns null when no recognisable rate is found.
*/
export function parseBudgetLabEffectiveTariffHtml(html) {
const text = htmlToPlainText(html);
if (!text) return null;
const updatedAt = toIsoDate(text.match(/\bUpdated:\s*([A-Za-z]+\s+\d{1,2},\s+\d{4})/i)?.[1] ?? '');
const patterns = [
/effective tariff rate reaching\s+(\d+(?:\.\d+)?)%\s+in\s+([A-Za-z]+\s+\d{4})/i,
/average effective (?:u\.s\.\s*)?tariff rate[^.]{0,180}?\bto\s+(\d+(?:\.\d+)?)%[^.]{0,180}?\b(?:in|by)\s+([A-Za-z]+\s+\d{4})/i,
/average effective (?:u\.s\.\s*)?tariff rate[^.]{0,180}?\bto\s+(\d+(?:\.\d+)?)%/i,
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (!match) continue;
const tariffRate = parseFloat(match[1]);
if (!Number.isFinite(tariffRate)) continue;
return {
sourceName: 'Yale Budget Lab',
sourceUrl: BUDGET_LAB_TARIFFS_URL,
observationPeriod: match[2] ?? '',
updatedAt,
tariffRate: Math.round(tariffRate * 100) / 100,
};
}
return null;
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { loadEnvFile, CHROME_UA, runSeed, writeExtraKeyWithMeta, sleep, verifySeedKey } from './_seed-utils.mjs';
import { BUDGET_LAB_TARIFFS_URL, htmlToPlainText, toIsoDate, parseBudgetLabEffectiveTariffHtml } from './_trade-parse-utils.mjs';
loadEnvFile(import.meta.url);
@@ -234,6 +235,30 @@ async function wtoFetch(path, params) {
return resp.json();
}
async function fetchBudgetLabEffectiveTariffRate() {
try {
const resp = await fetch(BUDGET_LAB_TARIFFS_URL, {
headers: { Accept: 'text/html', 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(15_000),
});
if (!resp.ok) {
console.warn(` Budget Lab tariffs: HTTP ${resp.status}`);
return null;
}
const html = await resp.text();
const parsed = parseBudgetLabEffectiveTariffHtml(html);
if (!parsed) {
console.warn(' Budget Lab tariffs: effective tariff rate not found in page content');
return null;
}
console.log(` Budget Lab effective tariff: ${parsed.tariffRate.toFixed(1)}%${parsed.observationPeriod ? ` (${parsed.observationPeriod})` : ''}`);
return parsed;
} catch (e) {
console.warn(` Budget Lab tariffs: ${e.message}`);
return null;
}
}
// ─── Trade Flows (WTO) — pre-seed major reporters vs World + key bilateral pairs ───
const BILATERAL_PAIRS = [
@@ -407,7 +432,7 @@ async function fetchTradeRestrictions() {
id: `${cc}-${year}-${row.IndicatorCode ?? ''}`,
reportingCountry: WTO_MEMBER_CODES[cc] ?? String(row.ReportingEconomy ?? ''),
affectedCountry: 'All trading partners', productSector: 'All products',
measureType: 'MFN Applied Tariff', description: `Average tariff rate: ${value.toFixed(1)}%`,
measureType: 'WTO MFN Baseline', description: `WTO MFN baseline: ${value.toFixed(1)}%`,
status: value > 10 ? 'high' : value > 5 ? 'moderate' : 'low',
notifiedAt: year, sourceUrl: 'https://stats.wto.org',
};
@@ -426,6 +451,7 @@ async function fetchTradeRestrictions() {
async function fetchTariffTrends() {
const currentYear = new Date().getFullYear();
const trends = {};
const usEffectiveTariffRate = await fetchBudgetLabEffectiveTariffRate();
for (const reporter of MAJOR_REPORTERS) {
const years = 10;
@@ -449,7 +475,12 @@ async function fetchTariffTrends() {
if (datapoints.length > 0) {
const cacheKey = `trade:tariffs:v1:${reporter}:all:${years}`;
trends[cacheKey] = { datapoints, fetchedAt: new Date().toISOString(), upstreamUnavailable: false };
trends[cacheKey] = {
datapoints,
...(reporter === '840' && usEffectiveTariffRate ? { effectiveTariffRate: usEffectiveTariffRate } : {}),
fetchedAt: new Date().toISOString(),
upstreamUnavailable: false,
};
}
await sleep(500);
}
@@ -566,7 +597,7 @@ function validate(data) {
runSeed('supply_chain', 'shipping', KEYS.shipping, fetchAll, {
validateFn: validate,
ttlSeconds: SHIPPING_TTL,
sourceVersion: 'fred-wto-sse-bdi',
sourceVersion: 'fred-wto-sse-bdi-budgetlab',
}).catch((err) => {
const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : ''; console.error('FATAL:', (err.message || err) + _cause);
process.exit(1);

View File

@@ -1,6 +1,6 @@
/**
* RPC: getTariffTrends -- reads seeded WTO tariff trend data from Railway seed cache.
* All external WTO API calls happen in seed-supply-chain-trade.mjs on Railway.
* RPC: getTariffTrends -- reads seeded WTO MFN tariff trends from Railway seed cache.
* The seed payload may also include an optional US effective tariff snapshot.
*/
import type {
ServerContext,

View File

@@ -1,5 +1,5 @@
/**
* RPC: getTradeRestrictions -- reads seeded WTO tariff restriction data from Railway seed cache.
* RPC: getTradeRestrictions -- reads seeded WTO MFN baseline overview data from Railway seed cache.
* All external WTO API calls happen in seed-supply-chain-trade.mjs on Railway.
*/
import type {

View File

@@ -5,6 +5,8 @@ import type {
GetTradeFlowsResponse,
GetTradeBarriersResponse,
GetCustomsRevenueResponse,
TariffDataPoint,
EffectiveTariffRate,
} from '@/services/trade';
import { t } from '@/services/i18n';
import { escapeHtml } from '@/utils/sanitize';
@@ -81,7 +83,7 @@ export class TradePolicyPanel extends Panel {
const tabsHtml = `
<div class="panel-tabs">
${wtoAvailable ? `<button class="panel-tab ${this.activeTab === 'restrictions' ? 'active' : ''}" data-tab="restrictions">
${t('components.tradePolicy.restrictions')}
${t('components.tradePolicy.overview')}
</button>` : ''}
${hasTariffs ? `<button class="panel-tab ${this.activeTab === 'tariffs' ? 'active' : ''}" data-tab="tariffs">
${t('components.tradePolicy.tariffs')}
@@ -125,14 +127,17 @@ export class TradePolicyPanel extends Panel {
case 'revenue': contentHtml = this.renderRevenue(); break;
}
const source = this.activeTab === 'revenue' ? t('components.tradePolicy.sourceTreasury') : t('components.tradePolicy.sourceWto');
const source = this.activeTab === 'revenue' ? t('components.tradePolicy.sourceTreasury')
: (this.activeTab === 'tariffs' || this.activeTab === 'restrictions') && this.tariffsData?.effectiveTariffRate?.sourceName
? `${t('components.tradePolicy.sourceWto')} / ${this.tariffsData.effectiveTariffRate.sourceName}`
: t('components.tradePolicy.sourceWto');
this.setContent(`
${tabsHtml}
${unavailableBanner}
<div class="economic-content">${contentHtml}</div>
<div class="economic-footer">
<span class="economic-source">${source}</span>
<span class="economic-source">${escapeHtml(source)}</span>
</div>
`);
@@ -140,10 +145,11 @@ export class TradePolicyPanel extends Panel {
private renderRestrictions(): string {
if (!this.restrictionsData || !this.restrictionsData.restrictions?.length) {
return `<div class="economic-empty">${t('components.tradePolicy.noRestrictions')}</div>`;
return `<div class="economic-empty">${t('components.tradePolicy.noOverviewData')}</div>`;
}
return `<div class="trade-restrictions-list">
return `${this.renderRestrictionsContext()}
<div class="trade-restrictions-list">
${this.restrictionsData.restrictions.map(r => {
const statusClass = r.status === 'high' ? 'status-active' : r.status === 'moderate' ? 'status-notified' : 'status-terminated';
const statusLabel = r.status === 'high' ? t('components.tradePolicy.highTariff') : r.status === 'moderate' ? t('components.tradePolicy.moderateTariff') : t('components.tradePolicy.lowTariff');
@@ -157,6 +163,7 @@ export class TradePolicyPanel extends Panel {
<div class="trade-restriction-body">
<div class="trade-sector">${escapeHtml(r.productSector)}</div>
${r.description ? `<div class="trade-description">${escapeHtml(r.description)}</div>` : ''}
${this.renderRestrictionEffectiveContext(r.reportingCountry)}
${r.affectedCountry ? `<div class="trade-affected">Affects: ${escapeHtml(r.affectedCountry)}</div>` : ''}
</div>
<div class="trade-restriction-footer">
@@ -168,12 +175,34 @@ export class TradePolicyPanel extends Panel {
</div>`;
}
private renderRestrictionsContext(): string {
const gapSummary = this.getEffectiveTariffGapSummary();
if (!gapSummary) {
return `<div class="trade-policy-note">${t('components.tradePolicy.overviewNoteNoEffective')}</div>`;
}
const gapSign = gapSummary.gap > 0 ? '+' : '';
const sourceLink = this.renderSourceUrl(gapSummary.effectiveRate.sourceUrl);
return `<div class="trade-policy-note">
${t('components.tradePolicy.usBaselineLabel')}: <strong>${gapSummary.baseline.tariffRate.toFixed(1)}%</strong>.
${t('components.tradePolicy.effectiveTariffRateLabel')}: <strong>${gapSummary.effectiveRate.tariffRate.toFixed(1)}%</strong>.
${t('components.tradePolicy.gapLabel')}: <strong>${gapSign}${gapSummary.gap.toFixed(1)}pp</strong>.
${t('components.tradePolicy.overviewNoteTail')}
${sourceLink}
</div>`;
}
private renderTariffs(): string {
if (!this.tariffsData || !this.tariffsData.datapoints?.length) {
return `<div class="economic-empty">${t('components.tradePolicy.noTariffData')}</div>`;
}
const rows = [...this.tariffsData.datapoints].sort((a, b) => b.year - a.year).map(d =>
const sortedDatapoints = [...this.tariffsData.datapoints].sort((a, b) => b.year - a.year);
const latestBaseline = sortedDatapoints[0] ?? null;
const effectiveRate = this.tariffsData.effectiveTariffRate ?? null;
const summaryHtml = this.renderTariffSummary(latestBaseline, effectiveRate);
const rows = sortedDatapoints.map(d =>
`<tr>
<td>${d.year}</td>
<td>${d.tariffRate.toFixed(1)}%</td>
@@ -181,12 +210,13 @@ export class TradePolicyPanel extends Panel {
</tr>`
).join('');
return `<div class="trade-tariffs-table">
return `${summaryHtml}
<div class="trade-tariffs-table">
<table>
<thead>
<tr>
<th>Year</th>
<th>${t('components.tradePolicy.appliedRate')}</th>
<th>${t('components.tradePolicy.mfnAppliedRate')}</th>
<th>Sector</th>
</tr>
</thead>
@@ -195,6 +225,85 @@ export class TradePolicyPanel extends Panel {
</div>`;
}
private renderTariffSummary(latestBaseline: TariffDataPoint | null, effectiveRate: EffectiveTariffRate | null): string {
if (!latestBaseline) return '';
const baselineMeta = t('components.tradePolicy.wtoBaselineMeta', { year: String(latestBaseline.year) });
const baselineCard = `
<div class="trade-tariff-card">
<div class="trade-tariff-label">${t('components.tradePolicy.baselineMfnTariff')}</div>
<div class="trade-tariff-value">${latestBaseline.tariffRate.toFixed(1)}%</div>
<div class="trade-tariff-meta">${escapeHtml(baselineMeta)}</div>
</div>
`;
if (!effectiveRate) {
return `<div class="trade-tariff-summary">
${baselineCard}
<div class="trade-tariff-card trade-tariff-card-muted">
<div class="trade-tariff-label">${t('components.tradePolicy.effectiveTariffRateLabel')}</div>
<div class="trade-tariff-value">—</div>
<div class="trade-tariff-meta">${t('components.tradePolicy.noEffectiveCoverageForCountry')}</div>
</div>
</div>`;
}
const gap = effectiveRate.tariffRate - latestBaseline.tariffRate;
const gapSign = gap > 0 ? '+' : '';
const gapClass = gap >= 0 ? 'trade-tariff-gap-positive' : 'trade-tariff-gap-negative';
const effectiveMetaParts = [
effectiveRate.sourceName,
effectiveRate.observationPeriod,
effectiveRate.updatedAt ? `Updated ${effectiveRate.updatedAt}` : '',
].filter(Boolean);
const sourceLink = this.renderSourceUrl(effectiveRate.sourceUrl);
return `<div class="trade-tariff-summary">
${baselineCard}
<div class="trade-tariff-card">
<div class="trade-tariff-label">${t('components.tradePolicy.effectiveTariffRateLabel')}</div>
<div class="trade-tariff-value">${effectiveRate.tariffRate.toFixed(1)}%</div>
<div class="trade-tariff-meta">
${escapeHtml(effectiveMetaParts.join(' | '))}
${sourceLink ? `<span class="trade-tariff-source">${sourceLink}</span>` : ''}
</div>
</div>
<div class="trade-tariff-card">
<div class="trade-tariff-label">${t('components.tradePolicy.gapLabel')}</div>
<div class="trade-tariff-value ${gapClass}">${gapSign}${gap.toFixed(1)}pp</div>
<div class="trade-tariff-meta">${t('components.tradePolicy.effectiveMinusBaseline')}</div>
</div>
</div>`;
}
private getLatestBaselineTariffPoint(): TariffDataPoint | null {
if (!this.tariffsData?.datapoints?.length) return null;
return [...this.tariffsData.datapoints].sort((a, b) => b.year - a.year)[0] ?? null;
}
private getEffectiveTariffGapSummary(): { baseline: TariffDataPoint; effectiveRate: EffectiveTariffRate; gap: number } | null {
const baseline = this.getLatestBaselineTariffPoint();
const effectiveRate = this.tariffsData?.effectiveTariffRate ?? null;
if (!baseline || !effectiveRate) return null;
return {
baseline,
effectiveRate,
gap: effectiveRate.tariffRate - baseline.tariffRate,
};
}
private renderRestrictionEffectiveContext(reportingCountry: string): string {
if (reportingCountry !== 'United States') return '';
const gapSummary = this.getEffectiveTariffGapSummary();
if (!gapSummary) return '';
const gapSign = gapSummary.gap > 0 ? '+' : '';
return `<div class="trade-policy-inline-note">
${t('components.tradePolicy.effectiveTariffRateLabel')}: ${gapSummary.effectiveRate.tariffRate.toFixed(1)}%
<span class="trade-policy-inline-sep">|</span>
${t('components.tradePolicy.gapVsMfnLabel')}: ${gapSign}${gapSummary.gap.toFixed(1)}pp
</div>`;
}
private renderFlows(): string {
if (!this.flowsData || !this.flowsData.flows?.length) {
return `<div class="economic-empty">${t('components.tradePolicy.noFlowData')}</div>`;

View File

@@ -36,6 +36,7 @@ export interface GetTariffTrendsResponse {
datapoints: TariffDataPoint[];
fetchedAt: string;
upstreamUnavailable: boolean;
effectiveTariffRate?: EffectiveTariffRate;
}
export interface TariffDataPoint {
@@ -48,6 +49,14 @@ export interface TariffDataPoint {
indicatorCode: string;
}
export interface EffectiveTariffRate {
sourceName: string;
sourceUrl: string;
observationPeriod: string;
updatedAt: string;
tariffRate: number;
}
export interface GetTradeFlowsRequest {
reportingCountry: string;
partnerCountry: string;

View File

@@ -36,6 +36,7 @@ export interface GetTariffTrendsResponse {
datapoints: TariffDataPoint[];
fetchedAt: string;
upstreamUnavailable: boolean;
effectiveTariffRate?: EffectiveTariffRate;
}
export interface TariffDataPoint {
@@ -48,6 +49,14 @@ export interface TariffDataPoint {
indicatorCode: string;
}
export interface EffectiveTariffRate {
sourceName: string;
sourceUrl: string;
observationPeriod: string;
updatedAt: string;
tariffRate: number;
}
export interface GetTradeFlowsRequest {
reportingCountry: string;
partnerCountry: string;

View File

@@ -893,16 +893,29 @@
},
"tradePolicy": {
"restrictions": "Restrictions",
"overview": "Overview",
"tariffs": "Tariffs",
"flows": "Trade Flows",
"barriers": "Barriers",
"noRestrictions": "No active trade restrictions",
"noOverviewData": "No tariff overview data available",
"noTariffData": "No tariff data available",
"noFlowData": "No trade flow data available",
"noBarriers": "No trade barriers reported",
"apiKeyMissing": "WTO API key required — add it in Settings",
"upstreamUnavailable": "WTO data temporarily unavailable — showing cached data",
"appliedRate": "Applied Rate",
"mfnAppliedRate": "MFN Applied Rate",
"baselineMfnTariff": "Baseline MFN tariff",
"effectiveTariffRateLabel": "Effective tariff rate",
"gapLabel": "Gap",
"gapVsMfnLabel": "Gap vs MFN",
"noEffectiveCoverageForCountry": "No effective-rate coverage for this country",
"effectiveMinusBaseline": "Effective rate minus WTO MFN baseline",
"wtoBaselineMeta": "WTO MFN applied rate | {{year}}",
"overviewNoteNoEffective": "These figures are WTO MFN baseline rates, not the current tariff burden from unilateral tariff actions.",
"usBaselineLabel": "US WTO MFN baseline",
"overviewNoteTail": "These cards show WTO baseline commitments, not the live effective tariff burden.",
"boundRate": "Bound Rate",
"exports": "Exports",
"imports": "Imports",
@@ -920,7 +933,7 @@
"colDate": "Date",
"colMonthly": "Monthly",
"colFytd": "FY YTD",
"infoTooltip": "<strong>Trade Policy</strong> WTO trade monitoring:<ul><li><strong>Restrictions</strong>: Active trade measures by country and sector</li><li><strong>Tariffs</strong>: Applied tariff rate trends by year</li><li><strong>Trade Flows</strong>: Export/import volumes with year-over-year changes</li><li><strong>Barriers</strong>: Technical barriers to trade (TBT/SPS notifications)</li><li><strong>Revenue</strong>: Monthly US customs duties revenue (US Treasury MTS data)</li></ul>"
"infoTooltip": "<strong>Trade Policy</strong> WTO baseline and tariff-impact monitoring:<ul><li><strong>Overview</strong>: WTO MFN baseline rates with US effective-rate context when available</li><li><strong>Tariffs</strong>: WTO MFN tariff trends vs the US effective tariff estimate</li><li><strong>Trade Flows</strong>: Export/import volumes with year-over-year changes</li><li><strong>Barriers</strong>: Technical barriers to trade (TBT/SPS notifications)</li><li><strong>Revenue</strong>: Monthly US customs duties revenue (US Treasury MTS data)</li></ul>"
},
"gdelt": {
"empty": "No recent articles for this topic"

View File

@@ -1,6 +1,6 @@
/**
* Trade policy intelligence service -- WTO data sources.
* Trade restrictions, tariff trends, trade flows, and SPS/TBT barriers.
* Trade policy intelligence service.
* WTO MFN baselines, trade flows/barriers, and US customs/effective tariff context.
*/
import { getRpcBaseUrl } from '@/services/rpc-client';
@@ -13,6 +13,7 @@ import {
type GetCustomsRevenueResponse,
type TradeRestriction,
type TariffDataPoint,
type EffectiveTariffRate,
type TradeFlowRecord,
type TradeBarrier,
type CustomsRevenueMonth,
@@ -22,7 +23,7 @@ import { isFeatureAvailable } from '../runtime-config';
import { getHydratedData } from '@/services/bootstrap';
// Re-export types for consumers
export type { TradeRestriction, TariffDataPoint, TradeFlowRecord, TradeBarrier, CustomsRevenueMonth };
export type { TradeRestriction, TariffDataPoint, EffectiveTariffRate, TradeFlowRecord, TradeBarrier, CustomsRevenueMonth };
export type {
GetTradeRestrictionsResponse,
GetTariffTrendsResponse,

View File

@@ -9380,6 +9380,87 @@ a.prediction-link:hover {
text-decoration: underline;
}
.trade-policy-note {
margin-bottom: 8px;
padding: 8px 10px;
background: var(--overlay-subtle);
border-left: 2px solid var(--accent);
border-radius: 4px;
font-size: 10px;
line-height: 1.45;
color: var(--text-secondary);
}
.trade-policy-note strong {
color: var(--text);
}
.trade-policy-inline-note {
margin-top: 4px;
font-size: 9px;
line-height: 1.4;
color: var(--text-secondary);
}
.trade-policy-inline-sep {
margin: 0 4px;
color: var(--text-faint);
}
.trade-tariff-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
margin-bottom: 8px;
}
.trade-tariff-card {
padding: 8px 10px;
background: var(--overlay-subtle);
border: 1px solid var(--border);
border-radius: 4px;
}
.trade-tariff-card-muted {
opacity: 0.85;
}
.trade-tariff-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.4px;
text-transform: uppercase;
color: var(--text-dim);
}
.trade-tariff-value {
margin-top: 4px;
font-size: 16px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--text);
}
.trade-tariff-meta {
margin-top: 4px;
font-size: 10px;
line-height: 1.4;
color: var(--text-dim);
}
.trade-tariff-source {
display: inline-block;
margin-left: 6px;
}
.trade-tariff-gap-positive {
color: var(--orange, #ffab40);
}
.trade-tariff-gap-negative {
color: var(--green, #69f0ae);
}
/* Trade Tariffs Table */
.trade-tariffs-table {
width: 100%;

View File

@@ -328,7 +328,7 @@ describe('Seed script structure', () => {
});
it('updated sourceVersion reflects new sources', () => {
assert.ok(seedSrc.includes("'fred-wto-sse-bdi'"));
assert.ok(seedSrc.includes("'fred-wto-sse-bdi-budgetlab'"));
});
});

View File

@@ -0,0 +1,168 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseBudgetLabEffectiveTariffHtml, toIsoDate, htmlToPlainText, BUDGET_LAB_TARIFFS_URL } from '../scripts/_trade-parse-utils.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');
const protoSrc = readFileSync(join(root, 'proto/worldmonitor/trade/v1/get_tariff_trends.proto'), 'utf-8');
const tradeDataProtoSrc = readFileSync(join(root, 'proto/worldmonitor/trade/v1/trade_data.proto'), 'utf-8');
const seedSrc = readFileSync(join(root, 'scripts/seed-supply-chain-trade.mjs'), 'utf-8');
const panelSrc = readFileSync(join(root, 'src/components/TradePolicyPanel.ts'), 'utf-8');
const serviceSrc = readFileSync(join(root, 'src/services/trade/index.ts'), 'utf-8');
const clientGeneratedSrc = readFileSync(join(root, 'src/generated/client/worldmonitor/trade/v1/service_client.ts'), 'utf-8');
const serverGeneratedSrc = readFileSync(join(root, 'src/generated/server/worldmonitor/trade/v1/service_server.ts'), 'utf-8');
describe('Trade tariff proto contract', () => {
it('adds EffectiveTariffRate message to shared trade data', () => {
assert.match(tradeDataProtoSrc, /message EffectiveTariffRate/);
assert.match(tradeDataProtoSrc, /string source_name = 1;/);
assert.match(tradeDataProtoSrc, /double tariff_rate = 5;/);
});
it('adds optional effective_tariff_rate to GetTariffTrendsResponse', () => {
assert.match(protoSrc, /EffectiveTariffRate effective_tariff_rate = 4;/);
});
});
describe('Generated tariff types', () => {
it('client types expose an optional effectiveTariffRate snapshot', () => {
assert.match(clientGeneratedSrc, /effectiveTariffRate\?: EffectiveTariffRate/);
});
it('server types expose an optional effectiveTariffRate snapshot', () => {
assert.match(serverGeneratedSrc, /effectiveTariffRate\?: EffectiveTariffRate/);
});
it('trade service re-exports EffectiveTariffRate', () => {
assert.match(serviceSrc, /export type \{[^}]*EffectiveTariffRate/);
});
});
describe('Budget Lab effective tariff seed integration', () => {
it('imports parse helpers from shared utils module', () => {
assert.match(seedSrc, /_trade-parse-utils\.mjs/);
assert.match(seedSrc, /parseBudgetLabEffectiveTariffHtml/);
});
it('attaches the effective tariff snapshot only to the US tariff payload', () => {
assert.match(seedSrc, /reporter === '840' && usEffectiveTariffRate/);
});
it('keeps restrictions snapshot labeled as WTO MFN baseline data', () => {
assert.match(seedSrc, /measureType: 'WTO MFN Baseline'/);
assert.match(seedSrc, /description: `WTO MFN baseline: \$\{value\.toFixed\(1\)\}%`/);
});
});
describe('parseBudgetLabEffectiveTariffHtml — pattern 1 (rate reaching … in period)', () => {
it('parses tariff rate, observation period, and updated date', () => {
const html = `
<html><body>
<div>Updated: March 2, 2026</div>
<p>U.S. consumers face tariff changes, raising the effective tariff rate reaching 9.9% in December 2025.</p>
</body></html>
`;
assert.deepEqual(parseBudgetLabEffectiveTariffHtml(html), {
sourceName: 'Yale Budget Lab',
sourceUrl: BUDGET_LAB_TARIFFS_URL,
observationPeriod: 'December 2025',
updatedAt: '2026-03-02',
tariffRate: 9.9,
});
});
it('rounds to 2 decimal places', () => {
const html = '<p>effective tariff rate reaching 12.345% in January 2026</p>';
assert.equal(parseBudgetLabEffectiveTariffHtml(html)?.tariffRate, 12.35);
});
});
describe('parseBudgetLabEffectiveTariffHtml — pattern 2 (average effective … to X% … in period)', () => {
it('parses rate and period via "average effective tariff rate … to X% … in" phrasing', () => {
const html = `
<html><body>
<div>Updated: January 15, 2026</div>
<p>Our estimates show the average effective U.S. tariff rate has risen to 18.5% in February 2026 from pre-tariff levels.</p>
</body></html>
`;
const result = parseBudgetLabEffectiveTariffHtml(html);
assert.ok(result, 'expected a non-null result for pattern 2');
assert.equal(result.tariffRate, 18.5);
assert.equal(result.observationPeriod, 'February 2026');
assert.equal(result.updatedAt, '2026-01-15');
});
});
describe('parseBudgetLabEffectiveTariffHtml — pattern 3 (rate without period)', () => {
it('parses rate when observation period is absent, leaving observationPeriod empty', () => {
const html = '<p>The average effective tariff rate has climbed to 22.1%.</p>';
const result = parseBudgetLabEffectiveTariffHtml(html);
assert.ok(result, 'expected a non-null result for pattern 3');
assert.equal(result.tariffRate, 22.1);
assert.equal(result.observationPeriod, '');
});
});
describe('parseBudgetLabEffectiveTariffHtml — edge cases', () => {
it('returns null when page contains no recognizable rate', () => {
assert.equal(parseBudgetLabEffectiveTariffHtml('<html><body><p>No tariff data here.</p></body></html>'), null);
});
it('strips HTML tags before matching', () => {
const html = '<p>effective tariff rate reaching <strong>7.5%</strong> in <em>March 2026</em></p>';
const result = parseBudgetLabEffectiveTariffHtml(html);
assert.ok(result);
assert.equal(result.tariffRate, 7.5);
});
});
describe('toIsoDate helper', () => {
it('converts "March 2, 2026" to 2026-03-02', () => {
assert.equal(toIsoDate('March 2, 2026'), '2026-03-02');
});
it('passes through an already-ISO date unchanged', () => {
assert.equal(toIsoDate('2026-01-15'), '2026-01-15');
});
it('returns empty string for unparseable input', () => {
assert.equal(toIsoDate('not a date'), '');
assert.equal(toIsoDate(''), '');
});
});
describe('Trade policy tariff panel', () => {
it('renames the misleading Restrictions tab to Overview', () => {
assert.match(panelSrc, /components\.tradePolicy\.overview/);
assert.match(panelSrc, /components\.tradePolicy\.noOverviewData/);
});
it('labels the WTO series as an MFN baseline', () => {
assert.match(panelSrc, /components\.tradePolicy\.baselineMfnTariff/);
assert.match(panelSrc, /components\.tradePolicy\.mfnAppliedRate/);
});
it('shows effective tariff and gap cards when coverage exists', () => {
assert.match(panelSrc, /components\.tradePolicy\.effectiveTariffRateLabel/);
assert.match(panelSrc, /components\.tradePolicy\.gapLabel/);
assert.match(panelSrc, /components\.tradePolicy\.effectiveMinusBaseline/);
});
it('keeps a graceful MFN-only fallback for countries without effective-rate coverage', () => {
assert.match(panelSrc, /components\.tradePolicy\.noEffectiveCoverageForCountry/);
});
it('clarifies on the Restrictions tab that WTO figures are baselines, not live tariff burden', () => {
assert.match(panelSrc, /components\.tradePolicy\.overviewNoteNoEffective/);
assert.match(panelSrc, /components\.tradePolicy\.overviewNoteTail/);
});
it('adds inline US effective-rate context on the overview card', () => {
assert.match(panelSrc, /renderRestrictionEffectiveContext/);
assert.match(panelSrc, /components\.tradePolicy\.gapVsMfnLabel/);
});
});