Data snapshot: 18 October 2026 Dashboard

Truflation as a proxy for US CPI¶

Algebris Data Analytics Case Study | Shi Yuchen | Data snapshot 18 October 2026

This walkthrough uses Truflation to inform a calibrated CPI forecast and tests its value against simple baselines. Results use the included source archive. Q4 covers 14 morning/evening CTP-ISW reports from 28 February to 6 March 2026, collected retrospectively in October, with saved DeepSeek extractions and a separate synthetic software example.

Run the notebook from its folder or the project root. It needs the dependencies in requirements-lock.txt; the offline run needs no credentials or internet connection. The companion Dash app is started with python -m dashboard.app from the project root.

In [1]:
from pathlib import Path
import sys, json, hashlib, tempfile
import pandas as pd
from IPython.display import display, HTML, Markdown
ROOT = Path.cwd()
if not (ROOT / 'inflation_monitor').exists():
    ROOT = ROOT.parent
sys.path.insert(0, str(ROOT))
from inflation_monitor.config import Settings
settings = Settings(data_dir=ROOT / 'data', log_dir=ROOT / 'logs')
print('Project:', ROOT.name)
print('Python:', sys.version.split()[0])
Project: Algebris_Case_Study_Shi_Yuchen
Python: 3.12.14

1 Data engineering¶

The pipeline separates API acquisition, immutable raw snapshots, parsing, quality checks and derived tables. Each adapter implements the same source contract. BLS is the primary official CPI source, with FRED as fallback. EIA gasoline is an optional modelling input; Brent and NBER recessions provide context. API keys are read from environment variables, never committed.

python -m inflation_monitor.pipeline run fetches live data. run --offline rebuilds from the included archive. schedule --time 07:30 --timezone Europe/London invokes the same pipeline daily while the scheduler process is running. External scheduler examples are in scheduling/. No permanent scheduler is installed.

The raw Truflation stream values are already percentage YoY rates: applying another 12-month percentage change to them would be incorrect. Official CPI YoY is 100 * (I_t / I_(t-12) - 1). Missing months remain missing.

In [2]:
manifest = [json.loads(line) for line in (settings.raw_dir / 'manifest.jsonl').read_text().splitlines() if line.strip()]
for item in manifest:
    raw = (settings.raw_dir / item['path']).read_bytes()
    assert hashlib.sha256(raw).hexdigest() == item['sha256']
display(pd.DataFrame(manifest)[['source', 'series_id', 'fetched_at', 'bytes', 'sha256']])
print('All raw snapshot hashes verified.')
source series_id fetched_at bytes sha256
0 truflation TRUFLATION_US_CPI_YOY 2026-10-18T10:43:12+00:00 356244 e9f615a2a7c2019f7bfc9fecb8cbe33c002d98b3cc165e...
1 bls cpi_2008-2017 2026-10-18T10:43:13+00:00 106273 6052315a770c6d0de58bffffe1ace87561366a9d410feb...
2 bls cpi_2018-2026 2026-10-18T10:43:13+00:00 93723 0c5f83190f7ece924daf252525ff4c3c70961893b11080...
3 eia GASREGW 2026-10-18T10:43:26+00:00 127488 668bee46e7b136d492434654e8723fce18e278761aac68...
4 eia DCOILBRENTEU 2026-10-18T10:43:47+00:00 452608 ae240e004d70e279e1e6a0e5297d1558c168c2279b7752...
5 nber USREC 2026-10-18T10:43:50+00:00 91028 457028612263f0cff09bf2a3dca379b16b37a7a6f815ab...
All raw snapshot hashes verified.
In [3]:
from inflation_monitor.pipeline import run_pipeline
result = run_pipeline(offline=True, settings=settings)
assert result.ok, result.error
print('Rebuild:', result.status)
display(pd.DataFrame(list(result.rows.items()), columns=['Output table', 'Rows']))
display(pd.DataFrame(result.issues))
Rebuild: success_with_warnings
Output table Rows
0 truflation_daily 6134
1 cpi_monthly 225
2 comparison_monthly 202
3 recessions 12
4 context_prices 4576
5 analysis_correlations 5
6 analysis_lead_lag 13
7 analysis_rolling_corr 200
8 analysis_divergences 5
9 analysis_gap_decomposition 202
10 nowcast_backtest_18 980
11 nowcast_backtest_eom 980
12 nowcast_live 7
13 nowcast_horizon_profile 98
14 nowcast_blackout 24
level check message
0 WARN jump Truflation YoY: 1 one-step move(s) > 0.5pp in ...
1 INFO truflation_fresh Truflation last print 2026-10-17 (1 day(s) old)
2 INFO known_gap CPI 2025-10 not published by BLS (2025 lapse i...
3 INFO next_release Next CPI release: 2026-10 on 2026-11-10

2 Interactive comparison¶

Both traces use the same YoY-percent axis. CPI is displayed by reference month, not its later publication date. This chart describes the series; forecast tests apply release-date cutoffs. Grey bands are NBER recessions. The slider supports zooming. The Dash app adds date presets, daily/7-day/30-day/monthly Truflation views, CPI-measure selection, model evidence, data export and a manual refresh button.

In [4]:
from dashboard.app import DashboardData
from dashboard import figures as F
d = DashboardData.load(settings)
display(HTML(F.comparison_figure(d.comp, d.truf, d.rec, start='2020-01-01',
    nowcast=d.nowcast, cpi_col='cpi_yoy_nsa').to_html(full_html=False, include_plotlyjs=True)))

3 Analysis and the next CPI release¶

The calculations below distinguish correlations in levels from correlations in monthly changes. Persistent level gaps can coexist with useful directional information. Gap episodes are descriptive; the shelter comparison is not a causal attribution. Missing months are preserved before differencing.

In [5]:
display(d.correlations)
display(d.divergences)
display(HTML(F.rolling_corr_figure(d.rolling).to_html(full_html=False, include_plotlyjs=False)))
display(HTML(F.gap_decomposition_figure(d.gaps, start='2020-01-01').to_html(full_html=False, include_plotlyjs=False)))
period months change_pairs corr levels corr monthly changes corr levels vs CPI ex-shelter mean gap (Truflation - CPI), pp gap std, pp
0 2011-2019 108 107 0.926033 0.779305 0.924331 0.078060 0.395111
1 2020-2022 36 35 0.978741 0.919545 0.995152 1.264408 1.359940
2 2023-2026 44 42 0.856273 0.809244 0.544367 -0.852849 0.511806
3 Live era 2022+ 56 54 0.973167 0.814180 0.958332 -0.266912 1.302770
4 Full 2011-2026 188 186 0.942392 0.844367 0.955951 0.087361 0.985127
start end months direction mean_spread_pp max_abs_spread_pp
0 2010-07 2011-01 7 Truflation above CPI 1.340250 1.606289
1 2011-04 2011-06 3 Truflation above CPI 1.173828 1.246644
2 2021-03 2022-09 19 Truflation above CPI 2.422331 3.544006
3 2023-04 2023-08 5 Truflation below CPI -1.135778 1.313238
4 2026-02 2026-09 8 Truflation below CPI -1.650096 2.209950

Forecast design and evaluation¶

The target is the NSA headline CPI 12-month rate. Baselines include a no-change forecast and an autoregression. Bridge models anchor on the last published CPI and add the change in Truflation and/or EIA gasoline inflation. Coefficients are fitted with an expanding window. Each fit uses only outcomes released before the simulated forecast cutoff. Source-specific publication lags are applied to high-frequency observations.

This is a pseudo-real-time test using current data vintages and conservative release lags. Truflation history before December 2021 is back-cast; later revisions are also possible. The 2022+ results are shown separately, but that same sample was used to select the preferred model. P-values are exploratory after comparing several models. The preferred model's nominal 80% bands covered about 73% of full-sample outcomes.

In [6]:
display(d.metrics)
display(d.by_period[d.by_period['period'].str.startswith('Live era')])
display(HTML(F.rmse_bar_figure(d.metrics, d.nowcast['preferred_model']).to_html(full_html=False, include_plotlyjs=False)))
display(d.blackout)
display(HTML(F.blackout_figure(d.blackout).to_html(full_html=False, include_plotlyjs=False)))
model n RMSE MAE bias RMSE vs RW DM stat DM p-value direction hit exact print (0.1pp) within 0.2pp 80% interval coverage
0 Random walk 140 0.407883 0.303491 0.026113 1.000000 NaN NaN NaN 0.085714 0.421429 0.735714
1 AR(1) on changes 140 0.361191 0.276444 0.021312 0.885528 -2.242086 2.653978e-02 0.685484 0.114286 0.450000 0.785714
2 Truflation level (face value) 140 1.115377 0.743382 -0.063398 2.734555 5.324030 3.977579e-07 0.693548 0.042857 0.264286 0.664286
3 Truflation change, b=1 140 0.287996 0.212717 0.021475 0.706077 -3.556508 5.143854e-04 0.870968 0.192857 0.564286 0.728571
4 Truflation bridge (OLS) 140 0.242207 0.183108 0.010148 0.593816 -4.295090 3.258681e-05 0.862903 0.257143 0.657143 0.735714
5 Gasoline bridge (OLS) 140 0.237755 0.168758 -0.016637 0.582901 -4.321253 2.936010e-05 0.895161 0.228571 0.714286 0.742857
6 Truflation + gasoline (OLS) 140 0.194481 0.139458 -0.008604 0.476807 -4.866814 3.035835e-06 0.927419 0.221429 0.785714 0.728571
model period n RMSE MAE RMSE vs RW DM p-value
28 Random walk Live era (Truflation published, 2022+) 56 0.409530 0.318698 1.000000 NaN
29 AR(1) on changes Live era (Truflation published, 2022+) 56 0.393865 0.316448 0.961749 0.628916
30 Truflation level (face value) Live era (Truflation published, 2022+) 56 1.355138 1.106546 3.309007 0.000003
31 Truflation change, b=1 Live era (Truflation published, 2022+) 56 0.323506 0.245324 0.789945 0.123909
32 Truflation bridge (OLS) Live era (Truflation published, 2022+) 56 0.282729 0.220942 0.690374 0.006783
33 Gasoline bridge (OLS) Live era (Truflation published, 2022+) 56 0.270483 0.201902 0.660472 0.001930
34 Truflation + gasoline (OLS) Live era (Truflation published, 2022+) 56 0.232733 0.177201 0.568292 0.000783
blackout_months model sample n RMSE MAE
0 1 Random walk Full sample 140 0.391408 0.289309
1 1 Random walk Live era 2022+ 56 0.407141 0.313741
2 1 Truflation + gasoline (OLS) Full sample 140 0.182810 0.125911
3 1 Truflation + gasoline (OLS) Live era 2022+ 56 0.212857 0.155095
4 1 Truflation bridge (OLS) Full sample 140 0.211256 0.164546
5 1 Truflation bridge (OLS) Live era 2022+ 56 0.251552 0.200329
6 2 Random walk Full sample 140 0.671787 0.491438
7 2 Random walk Live era 2022+ 56 0.667467 0.533247
8 2 Truflation + gasoline (OLS) Full sample 140 0.303585 0.203344
9 2 Truflation + gasoline (OLS) Live era 2022+ 56 0.368719 0.277029
10 2 Truflation bridge (OLS) Full sample 140 0.332161 0.256420
11 2 Truflation bridge (OLS) Live era 2022+ 56 0.392428 0.303209
12 3 Random walk Full sample 140 0.888523 0.645088
13 3 Random walk Live era 2022+ 56 0.872752 0.692551
14 3 Truflation + gasoline (OLS) Full sample 140 0.393037 0.270774
15 3 Truflation + gasoline (OLS) Live era 2022+ 56 0.495657 0.388104
16 3 Truflation bridge (OLS) Full sample 140 0.409218 0.315307
17 3 Truflation bridge (OLS) Live era 2022+ 56 0.488396 0.366215
18 6 Random walk Full sample 140 1.382790 1.004652
19 6 Random walk Live era 2022+ 56 1.488621 1.088658
20 6 Truflation + gasoline (OLS) Full sample 140 0.556057 0.383121
21 6 Truflation + gasoline (OLS) Live era 2022+ 56 0.752726 0.600143
22 6 Truflation bridge (OLS) Full sample 140 0.524550 0.407631
23 6 Truflation bridge (OLS) Live era 2022+ 56 0.644445 0.512523
In [7]:
n = d.nowcast
print('Forecast cutoff:', n['asof'], '| target month:', n['target_month'], '| scheduled release:', n['release_date'])
print('Selected model:', n['preferred_model'])
print('Information set:', json.dumps(n['information_set'], indent=2))
summary = pd.DataFrame([{ 'Model': name, 'Implied YoY %': m['yoy'], 'Band lower': m['lo80'],
    'Band upper': m['hi80'], 'Conditional SA m/m %': m['print']['mom_sa_pct'] }
    for name, m in n['models'].items()])
display(summary)
display(HTML(F.nowcast_models_figure(n).to_html(full_html=False, include_plotlyjs=False)))
Forecast cutoff: 2026-10-18 | target month: 2026-10 | scheduled release: 2026-11-10
Selected model: Truflation + gasoline (OLS)
Information set: {
  "target": "2026-10",
  "asof": "2026-10-18",
  "last_cpi_month": "2026-09",
  "steps": 1,
  "y_last": 3.737684729064039,
  "y_prev": 3.3965478924364856,
  "truf_t": 2.8972765625,
  "truf_last": 2.3885172,
  "truf_days": 16,
  "d_truf": 0.5087593625000002,
  "momentum": 0.34113683662755356,
  "gap_last": 1.3491675290640393,
  "gas_t": 40.43115423854942,
  "gas_last": 37.72323494535617,
  "d_gas": 2.7079192931932496,
  "gas_weeks": 2,
  "actual": null,
  "actual_release": "2026-11-10"
}
Model Implied YoY % Band lower Band upper Conditional SA m/m %
0 Random walk 3.738 3.388 4.070 0.126
1 AR(1) on changes 3.903 3.547 4.183 0.285
2 Truflation level (face value) 2.897 3.086 4.516 -0.685
3 Truflation change, b=1 4.246 3.920 4.597 0.617
4 Truflation bridge (OLS) 4.090 3.818 4.385 0.467
5 Gasoline bridge (OLS) 3.873 3.655 4.020 0.257
6 Truflation + gasoline (OLS) 4.013 3.800 4.195 0.392

Interpretation for the Portfolio Manager¶

The PM memo (reports/PM_commentary.pdf) explains the next-release scenario. BLS lacks the October 2025 headline CPI base and will not publish the October 2026 12-month comparison. The implied YoY is counterfactual. The conditional SA m/m translation interpolates the missing base and carries forward the latest seasonal gap; it has not been independently backtested. reports/model_audit.md records the calculation and timing assumptions.

4 ISW and LLM system design¶

The design extracts source-supported events that may affect energy supply, transport, insurance, food inputs and inflation expectations. It separates reported claims from economic interpretation. Source and model interfaces, plus separate parsing and storage components, make the system extensible. Pydantic checks the structured fields; the extractor checks each quote against its source paragraph.

See reports/Q4_System_Design.pdf for the complete OOP mapping, schema, operational design, research references and failure handling. reports/Q4_Live_Experiment.pdf presents the real DeepSeek experiment, coverage, automated semantic review and costs. OpenRouter remains an optional adapter tested with mocks. The fixture below is synthetic and is kept separate from the real article results.

In [8]:
from isw_llm.pipeline import run_demo
with tempfile.TemporaryDirectory(prefix='isw-demo-') as tmp:
    demo = run_demo(Path(tmp))
    print(json.dumps({k: v for k, v in demo.items() if k != "aggregate_file"}, indent=2, default=str))
    print((Path(tmp) / 'extraction.json').read_text())
    print((Path(tmp) / 'aggregate.json').read_text())
{
  "synthetic": true,
  "status": "accepted",
  "extraction_key": "dc809af1a9315af924b8c2df1792f2c2802b5d3ecd6a294823ab424e5f58c754",
  "event_count": 1,
  "notice": "Entirely synthetic offline fixture; no actual ISW report was acquired and no real LLM was called."
}
{
  "extraction_key": "dc809af1a9315af924b8c2df1792f2c2802b5d3ecd6a294823ab424e5f58c754",
  "article": {
    "canonical_url": "https://example.invalid/synthetic-isw-demo",
    "title": "SYNTHETIC DEMO: fictional conflict and inflation report",
    "publication_date": "2026-03-02",
    "retrieved_at": "2026-10-18T13:57:48.291571Z",
    "source_kind": "synthetic_fixture",
    "raw_sha256": "b6c764dcc2b737a3e46cfd231f6f7b8c9b263847eb61f9b38e23030b082976fb",
    "text_sha256": "ae3a70cd13f1f7efd15f863f69b57171e29edd59e3b28c8e0ed595e53d427888",
    "parser_version": "html-paragraphs-2-browser-dom",
    "paragraphs": [
      {
        "paragraph_id": "p0001",
        "text": "This document is an invented software test fixture. It is not an ISW article and describes no actual event.",
        "links": [],
        "reference_ids": []
      },
      {
        "paragraph_id": "p0002",
        "text": "Fictional Port Aurora's operator said oil loading was temporarily suspended on March 1, 2026.",
        "links": [],
        "reference_ids": []
      },
      {
        "paragraph_id": "p0003",
        "text": "The fictional operator gave no interruption duration, capacity loss or verified volume estimate.",
        "links": [],
        "reference_ids": []
      },
      {
        "paragraph_id": "p0004",
        "text": "A fictional analyst said a prolonged interruption could require longer shipping routes. This is a conditional assessment.",
        "links": [],
        "reference_ids": []
      }
    ],
    "references": {},
    "acquisition_metadata": {},
    "narrative_character_count": 420,
    "reference_character_count": 0,
    "body_selector": "article",
    "media_links": [],
    "parsing_warnings": []
  },
  "extracted_at": "2026-10-18T13:57:48.298084Z",
  "model_id": "synthetic/fake-client-v1",
  "prompt_version": "conflict-inflation-3",
  "schema_version": "1.0",
  "payload": {
    "schema_version": "1.0",
    "relevant": true,
    "summary": "SYNTHETIC DEMO: a fictional port interruption illustrates structured extraction.",
    "events": [
      {
        "event_type": "shipping_disruption",
        "claim_status": "attributed_claim",
        "event_date": "2026-03-01",
        "date_text": "March 1, 2026",
        "actors": [
          "Fictional Port Aurora operator"
        ],
        "location": "Fictional Port Aurora",
        "commodities": [
          "oil"
        ],
        "summary": "SYNTHETIC: the fictional port operator reported a temporary loading suspension.",
        "quantities": [],
        "evidence": [
          {
            "paragraph_id": "p0002",
            "quote": "Fictional Port Aurora's operator said oil loading was temporarily suspended on March 1, 2026."
          }
        ],
        "impacts": [
          {
            "channel": "shipping_freight",
            "direction": "up",
            "horizon": "weeks",
            "materiality": "unknown",
            "basis": "analyst_inference",
            "rationale": "If an actual interruption persisted, detours could raise freight costs; this fictional example cannot quantify CPI."
          }
        ],
        "confidence": "high",
        "uncertainties": [
          "Entire event is fictional; duration and volumes are unspecified."
        ]
      }
    ],
    "limitations": [
      "Synthetic fixture only; no real ISW article or live LLM output."
    ]
  },
  "attempts": [
    {
      "attempt": 1,
      "model_id": "synthetic/fake-client-v1",
      "validation_version": "exact-quotes-2",
      "request_id": "synthetic-call-1",
      "usage": {
        "synthetic": true
      },
      "response_sha256": "d89849bc13d1abb9ad3a6ba5912e7e18bc6a4ed110d528b388ece7589384bdb5",
      "response_text": "{\"schema_version\": \"1.0\", \"relevant\": true, \"summary\": \"SYNTHETIC DEMO: a fictional port interruption illustrates structured extraction.\", \"events\": [{\"event_type\": \"shipping_disruption\", \"claim_status\": \"attributed_claim\", \"event_date\": \"2026-03-01\", \"date_text\": \"March 1, 2026\", \"actors\": [\"Fictional Port Aurora operator\"], \"location\": \"Fictional Port Aurora\", \"commodities\": [\"oil\"], \"summary\": \"SYNTHETIC: the fictional port operator reported a temporary loading suspension.\", \"quantities\": [], \"evidence\": [{\"paragraph_id\": \"p0002\", \"quote\": \"Fictional Port Aurora's operator said oil loading was temporarily suspended on March 1, 2026.\"}], \"impacts\": [{\"channel\": \"shipping_freight\", \"direction\": \"up\", \"horizon\": \"weeks\", \"materiality\": \"unknown\", \"basis\": \"analyst_inference\", \"rationale\": \"If an actual interruption persisted, detours could raise freight costs; this fictional example cannot quantify CPI.\"}], \"confidence\": \"high\", \"uncertainties\": [\"Entire event is fictional; duration and volumes are unspecified.\"]}], \"limitations\": [\"Synthetic fixture only; no real ISW article or live LLM output.\"]}",
      "status": "accepted"
    }
  ],
  "human_review_status": "unreviewed"
}
{
  "asof": "2026-10-18T13:57:48.310335+00:00",
  "include_synthetic": true,
  "report_count": 1,
  "event_mentions": 1,
  "days": {
    "2026-03-02": {
      "report_count": 1,
      "relevant_report_count": 1,
      "event_mentions": 1,
      "channel_direction_mentions": {
        "shipping_freight:up": 1
      },
      "synthetic_reports": 1
    }
  },
  "events": [
    {
      "extraction_key": "dc809af1a9315af924b8c2df1792f2c2802b5d3ecd6a294823ab424e5f58c754",
      "event_index": 0,
      "source_url": "https://example.invalid/synthetic-isw-demo",
      "publication_date": "2026-03-02",
      "available_at": "2026-10-18T13:57:48.299084+00:00",
      "last_observed_at": "2026-10-18T13:57:48.291571+00:00",
      "observation_id": "5814abe54740447db8ace6b10246de83",
      "observation_time_basis": "fetch",
      "first_extracted_at": "2026-10-18T13:57:48.298084+00:00",
      "source_kind": "synthetic_fixture",
      "event_date": "2026-03-01",
      "claim_status": "attributed_claim",
      "summary": "SYNTHETIC: the fictional port operator reported a temporary loading suspension.",
      "evidence": [
        {
          "paragraph_id": "p0002",
          "quote": "Fictional Port Aurora's operator said oil loading was temporarily suspended on March 1, 2026.",
          "paragraph_links": []
        }
      ],
      "impacts": [
        {
          "channel": "shipping_freight",
          "direction": "up",
          "horizon": "weeks",
          "materiality": "unknown",
          "basis": "analyst_inference",
          "rationale": "If an actual interruption persisted, detours could raise freight costs; this fictional example cannot quantify CPI."
        }
      ],
      "human_review_status": "unreviewed"
    }
  ],
  "quarantined_attempts": 0,
  "failed_source_urls": [],
  "limitations": [
    "Counts are report mentions, not unique independently verified events.",
    "Latest observation of accepted content per URL is used; failed revisions can leave the last successful observation stale.",
    "No-report days are unknown coverage, not zero conflict or zero inflation pressure.",
    "Publication-date bins are descriptive; use available_at for point-in-time applications.",
    "Economic direction is a conditional qualitative assessment, not a CPI forecast."
  ]
}
In [9]:
import inspect
from isw_llm import schemas
print(inspect.getsource(schemas))
"""Versioned extraction contract. Provenance is supplied by code, never by the LLM."""
from __future__ import annotations

from datetime import date, datetime
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

SCHEMA_VERSION = "1.0"


class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True, allow_inf_nan=False)


class Evidence(StrictModel):
    paragraph_id: str = Field(pattern=r"^p\d{4}$", description="ID in the supplied article.")
    quote: str = Field(min_length=8, max_length=500, description="Exact excerpt from that paragraph.")


class Quantity(StrictModel):
    """A source-reported magnitude, not an LLM-calculated economic effect."""

    metric: str = Field(min_length=1, max_length=100)
    value: float | None = Field(description="Use null when only a qualitative statement is available.")
    unit: str = Field(min_length=1, max_length=80, description="Preserve source units; never silently convert.")
    qualifier: Literal["exact", "approximate", "at_least", "at_most", "unknown"]
    time_period: str | None = Field(max_length=100, description="Source-stated period, else null.")
    evidence: Evidence


class InflationImpact(StrictModel):
    channel: Literal["energy_supply", "shipping_freight", "food_fertilizer", "insurance_risk",
                     "expectations_fx", "demand", "other"]
    direction: Literal["up", "down", "ambiguous", "none", "unknown"]
    horizon: Literal["days", "weeks", "months", "unknown"]
    materiality: Literal["low", "medium", "high", "unknown"]
    basis: Literal["source_assessment", "analyst_inference"] = Field(
        description="Use analyst_inference unless the report itself states the economic assessment.")
    rationale: str = Field(min_length=10, max_length=600,
                           description="Explain conditional transmission, not a numeric CPI forecast.")


class ConflictEvent(StrictModel):
    event_type: Literal["energy_asset_damage", "supply_disruption", "shipping_disruption",
                        "sanctions_policy", "military_escalation", "deescalation", "other"]
    claim_status: Literal["reported_observation", "attributed_claim", "source_assessment", "forecast"]
    event_date: date | None = Field(description="Exact event day only if supported; otherwise null.")
    date_text: str | None = Field(max_length=150, description="Original timing phrase, else null.")
    actors: list[str] = Field(max_length=20)
    location: str | None = Field(max_length=160)
    commodities: list[Literal["oil", "natural_gas", "refined_fuels", "food", "fertilizer", "other"]] = Field(max_length=6)
    summary: str = Field(min_length=10, max_length=600)
    quantities: list[Quantity] = Field(max_length=10)
    evidence: list[Evidence] = Field(min_length=1, max_length=8)
    impacts: list[InflationImpact] = Field(max_length=7)
    confidence: Literal["low", "medium", "high"] = Field(
        description="Confidence in faithful extraction, not probability that the underlying claim is true.")
    uncertainties: list[str] = Field(max_length=10)


class ExtractionPayload(StrictModel):
    schema_version: Literal["1.0"]
    relevant: bool
    summary: str = Field(min_length=10, max_length=1000)
    events: list[ConflictEvent] = Field(max_length=60)
    limitations: list[str] = Field(max_length=20)

    @model_validator(mode="after")
    def relevance_matches_events(self):
        if self.relevant != bool(self.events):
            raise ValueError("relevant must be true exactly when extracted events are present")
        return self


class Paragraph(StrictModel):
    paragraph_id: str = Field(pattern=r"^p\d{4}$")
    text: str = Field(min_length=1)
    links: list[str]
    reference_ids: list[str] = Field(default_factory=list)


class SourceReference(StrictModel):
    reference_id: str
    label: str
    text: str
    links: list[str]
    alias_of: str | None = None


class Article(StrictModel):
    canonical_url: str
    title: str
    publication_date: date | None
    retrieved_at: datetime
    source_kind: Literal["isw_http", "local_html", "browser_dom", "synthetic_fixture"]
    raw_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
    text_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
    parser_version: str
    paragraphs: list[Paragraph] = Field(min_length=1)
    references: dict[str, SourceReference] = Field(default_factory=dict)
    acquisition_metadata: dict = Field(default_factory=dict)
    narrative_character_count: int = Field(default=0, ge=0)
    reference_character_count: int = Field(default=0, ge=0)
    body_selector: str = "unspecified"
    media_links: list[str] = Field(default_factory=list)
    parsing_warnings: list[str] = Field(default_factory=list)

    @model_validator(mode="after")
    def aware_retrieval(self):
        if self.retrieved_at.utcoffset() is None:
            raise ValueError("retrieved_at must include a timezone")
        return self


class StoredExtraction(StrictModel):
    extraction_key: str
    article: Article
    extracted_at: datetime
    model_id: str
    prompt_version: str
    schema_version: str
    payload: ExtractionPayload
    attempts: list[dict]
    human_review_status: Literal["unreviewed"] = "unreviewed"


def strict_json_schema() -> dict:
    """Use required-but-nullable fields and closed objects for provider strict mode."""
    return ExtractionPayload.model_json_schema()

First-week article coverage¶

The source manifest includes both daily editions for 28 February through 6 March. The official directory omitted the 4 March evening report; its official page was independently found and included. Every narrative paragraph was sent in a paragraph-preserving chunk. Images/maps were not OCRed, and linked external references were not fetched. The DOM exports, hashes, publication dates, actual retrieval times, responses and failed attempts are retained.

The cells below read saved results without model calls. Automated semantic review records exclusions and corrections alongside the original responses. This review used AI, not human annotators; it does not measure precision/recall or verify the underlying conflict claims.

In [10]:
q4 = json.loads((ROOT / 'data/isw_live/analysis_summary.json').read_text(encoding='utf-8'))
display(pd.DataFrame([{
    'Articles': q4['article_count'], 'Paragraphs': q4['paragraph_count'],
    'Accepted chunks': q4['chunk_count'], 'Missing paragraphs': q4['missing_paragraph_count'],
    'Raw event mentions': q4['raw_event_mentions'],
    'Mentions after automated review': q4['analysis_event_mentions'],
    'Paid requests incl. pilots/repairs': q4['paid_request_count_including_pilots'],
    'Conservative cost (CNY)': q4['conservative_cost_cny_including_pilots']
}]))
display(pd.read_csv(ROOT / 'data/isw_live/daily_summary.csv'))
print(q4['cost_note'])
print(q4['interpretation'])
Articles Paragraphs Accepted chunks Missing paragraphs Raw event mentions Mentions after automated review Paid requests incl. pilots/repairs Conservative cost (CNY)
0 14 520 71 0 76 59 80 1.22984
publication_date articles paragraphs chunks raw_mentions analysis_mentions
0 2026-02-28 2 66 8 4 2
1 2026-03-01 2 68 11 11 7
2 2026-03-02 2 82 12 22 18
3 2026-03-03 2 94 11 14 10
4 2026-03-04 2 83 12 14 12
5 2026-03-05 2 78 9 9 8
6 2026-03-06 2 49 8 2 2
Estimated at peak uncached prices: CNY 2/M input and 8/M output tokens. Includes pilots and repairs; actual provider billing may be lower.
Counts are report mentions after automated semantic review. Repeated reports can describe one incident. Dates are publication dates; October collection prevents a historical trading backtest. Human verification and extraction-accuracy measurement remain outstanding.
In [11]:
import plotly.express as px
channels = pd.read_csv(ROOT / 'data/isw_live/daily_channels.csv')
fig = px.bar(channels, x='publication_date', y='mentions', color='channel',
             hover_data=['direction'], labels={'publication_date':'Report publication date',
             'mentions':'Reviewed channel mentions'},
             title='Reported transmission channels by publication date')
fig.update_layout(template='plotly_white', legend_title_text='Channel')
display(HTML(fig.to_html(full_html=False, include_plotlyjs=False)))
timeline = json.loads((ROOT / 'reports/q4_timeline.json').read_text(encoding='utf-8'))
for item in timeline['days']:
    display(Markdown('**' + item['date'] + '** — ' + item['assessment'] + '\n\n' +
        'Sources: ' + ', '.join('['+s['label']+']('+s['url']+')' for s in item['sources'])))

2026-02-28 — Warnings and voluntary avoidance precede evidence of a military blockade. The evening report attributes a 70% decline in Hormuz commercial vessel traffic to MarineTraffic and reports an IRGC warning via an EU official. This is a traffic measure, not a 70% loss of global oil supply; the report did not establish a blockade.

Sources: 2026-02-28 evening

2026-03-01 — Reports move from warnings to attacks on vessels and commercial ports. UKMTO reports incidents, while Iranian officials deny an intention to close Hormuz at that time. The reported Persian Gulf and Hormuz traffic declines refer to different locations/dates; they cannot be spliced into one measured series. Insurance and transit-cost claims remain attributed.

Sources: 2026-03-01 morning, 2026-03-01 evening

2026-03-02 — Reported production interruptions broaden the shipping-risk channel: QatarEnergy LNG suspension, the precautionary Ras Tanura refinery shutdown and Kurdish oil-field stoppages. Ras Tanura's precautionary closure does not establish physical damage. Qatar's LNG share describes exposure; it does not quantify the supply loss.

Sources: 2026-03-02 evening

2026-03-03 — Downstream and storage constraints emerge: QatarEnergy announces stoppages including urea, while the report connects Rumaila's closure to storage filling as tankers struggle to leave. These reports add fertilizer and export/storage channels. US political-risk insurance and possible tanker escorts are proposed mitigations; any reduction in freight or insurance costs remains to be measured.

Sources: 2026-03-03 morning, 2026-03-03 evening

2026-03-04 — Maersk booking suspensions widen logistics restrictions, with specified port exceptions. The report cites an approximately 90% Hormuz vessel-traffic decline and a forecast of at least one month for Qatar to regain normal LNG output. The latter concerns recovery to normal output, not a month of zero production; traffic counts do not measure lost energy volumes.

Sources: 2026-03-04 evening

2026-03-05 — Reported fires at Sarsang and BAPCO add concrete facility incidents, while the Sonangol Namibe case extends merchant-vessel risk into the northern Gulf. Saudi rerouting through Red Sea pipelines and continuing Iranian cargo/oil movements indicate partial substitution and residual flows. Regional export flows therefore continued in part.

Sources: 2026-03-05 evening

2026-03-06 — UKMTO reports an attack on a tug assisting Safeen Prestige; the evening report also describes attacks on oil-related targets around Basra without confirmed physical damage. Maritime and energy risks persist, but fewer extracted mentions on this day cannot establish de-escalation, recovery, or a lower future CPI print.

Sources: 2026-03-06 evening

Inspect an extraction and its review¶

Each review records changes to attribution, dates, quantities or relevance. Counts describe mentions: repeated reports may cover one incident, and absence from a later report does not establish recovery or lower inflation pressure.

In [12]:
reviewed = json.loads((ROOT / 'data/isw_live/reviewed_events.json').read_text(encoding='utf-8'))
example = next(e for e in reviewed if e['analysis_event']['quantities'])
print(json.dumps({k: example[k] for k in ['event_id','title','source_url','retrieved_at',
    'raw_event','analysis_event','ai_review','human_review_status']}, indent=2, ensure_ascii=False))
display(pd.read_csv(ROOT / 'data/isw_live/reviewed_events.csv')[
    ['publication_date','edition','claim_status','summary','disposition']])
{
  "event_id": "2026-02-28-iran-update-evening-special-report-february-28-2026-e4dfe81f508d:e002",
  "title": "Iran Update Evening Special Report: February 28, 2026",
  "source_url": "https://www.criticalthreats.org/analysis/iran-update-evening-special-report-february-28-2026",
  "retrieved_at": "2026-10-18T12:17:09.681000Z",
  "raw_event": {
    "event_type": "shipping_disruption",
    "claim_status": "attributed_claim",
    "event_date": null,
    "date_text": null,
    "actors": [
      "MarineTraffic",
      "United States",
      "Israel",
      "Iran"
    ],
    "location": "Strait of Hormuz",
    "commodities": [
      "oil"
    ],
    "summary": "Commercial ship traffic in the Strait of Hormuz dropped by 70 percent in response to the combined US and Israeli strike campaign on Iran, according to ship tracking platform MarineTraffic. The passage notes 20 percent of global oil supply transits the strait.",
    "quantities": [
      {
        "metric": "commercial ship traffic decline in Strait of Hormuz",
        "value": 70.0,
        "unit": "percent",
        "qualifier": "exact",
        "time_period": null,
        "evidence": {
          "paragraph_id": "p0039",
          "quote": "Commercial ship traffic dropped by 70 percent in the Strait of Hormuz in response to the United States and Israel’s combined strike campaign on Iran, according to ship tracking platform MarineTraffic."
        }
      },
      {
        "metric": "share of global oil supply passing through Strait of Hormuz",
        "value": 20.0,
        "unit": "percent",
        "qualifier": "exact",
        "time_period": null,
        "evidence": {
          "paragraph_id": "p0039",
          "quote": "Twenty percent of the global oil supply passes through the strait."
        }
      }
    ],
    "evidence": [
      {
        "paragraph_id": "p0039",
        "quote": "Commercial ship traffic dropped by 70 percent in the Strait of Hormuz in response to the United States and Israel’s combined strike campaign on Iran, according to ship tracking platform MarineTraffic."
      },
      {
        "paragraph_id": "p0039",
        "quote": "Twenty percent of the global oil supply passes through the strait."
      }
    ],
    "impacts": [
      {
        "channel": "shipping_freight",
        "direction": "up",
        "horizon": "weeks",
        "materiality": "high",
        "basis": "analyst_inference",
        "rationale": "A reported 70 percent drop in commercial transits through a chokepoint carrying about a fifth of global oil supply could raise freight and insurance costs and tighten energy logistics, conditional on the disruption persisting."
      },
      {
        "channel": "energy_supply",
        "direction": "up",
        "horizon": "weeks",
        "materiality": "medium",
        "basis": "analyst_inference",
        "rationale": "Reduced tanker transits through Hormuz could constrain crude flows and support oil prices if shippers continue avoiding the strait; the passage does not quantify price effects."
      }
    ],
    "confidence": "medium",
    "uncertainties": [
      "Traffic figure is attributed to MarineTraffic via the report, not independently observed.",
      "No event date is given for the traffic drop; article publication date is not used as event date.",
      "Duration and persistence of the disruption are unknown."
    ]
  },
  "analysis_event": {
    "event_type": "shipping_disruption",
    "claim_status": "attributed_claim",
    "event_date": null,
    "date_text": null,
    "actors": [
      "MarineTraffic",
      "United States",
      "Israel",
      "Iran"
    ],
    "location": "Strait of Hormuz",
    "commodities": [
      "oil"
    ],
    "summary": "Commercial ship traffic in the Strait of Hormuz dropped by 70 percent in response to the combined US and Israeli strike campaign on Iran, according to ship tracking platform MarineTraffic. The passage notes 20 percent of global oil supply transits the strait.",
    "quantities": [
      {
        "metric": "commercial ship traffic decline in Strait of Hormuz",
        "value": 70.0,
        "unit": "percent",
        "qualifier": "exact",
        "time_period": null,
        "evidence": {
          "paragraph_id": "p0039",
          "quote": "Commercial ship traffic dropped by 70 percent in the Strait of Hormuz in response to the United States and Israel’s combined strike campaign on Iran, according to ship tracking platform MarineTraffic."
        }
      },
      {
        "metric": "share of global oil supply passing through Strait of Hormuz",
        "value": 20.0,
        "unit": "percent",
        "qualifier": "exact",
        "time_period": null,
        "evidence": {
          "paragraph_id": "p0039",
          "quote": "Twenty percent of the global oil supply passes through the strait."
        }
      }
    ],
    "evidence": [
      {
        "paragraph_id": "p0039",
        "quote": "Commercial ship traffic dropped by 70 percent in the Strait of Hormuz in response to the United States and Israel’s combined strike campaign on Iran, according to ship tracking platform MarineTraffic."
      },
      {
        "paragraph_id": "p0039",
        "quote": "Twenty percent of the global oil supply passes through the strait."
      }
    ],
    "impacts": [
      {
        "channel": "shipping_freight",
        "direction": "up",
        "horizon": "weeks",
        "materiality": "high",
        "basis": "analyst_inference",
        "rationale": "A reported 70 percent drop in commercial transits through a chokepoint carrying about a fifth of global oil supply could raise freight and insurance costs and tighten energy logistics, conditional on the disruption persisting."
      },
      {
        "channel": "energy_supply",
        "direction": "up",
        "horizon": "weeks",
        "materiality": "medium",
        "basis": "analyst_inference",
        "rationale": "Reduced tanker transits through Hormuz could constrain crude flows and support oil prices if shippers continue avoiding the strait; the passage does not quantify price effects."
      }
    ],
    "confidence": "medium",
    "uncertainties": [
      "Traffic figure is attributed to MarineTraffic via the report, not independently observed.",
      "No event date is given for the traffic drop; article publication date is not used as event date.",
      "Duration and persistence of the disruption are unknown."
    ]
  },
  "ai_review": {
    "event_id": "2026-02-28-iran-update-evening-special-report-february-28-2026-e4dfe81f508d:e002",
    "article_id": "2026-02-28-iran-update-evening-special-report-february-28-2026-e4dfe81f508d",
    "event_index": 2,
    "raw_event_sha256": "281a5b08f671c22ff44bdb77c525572a72117d21060b0ad291df528f1a1ef31f",
    "disposition": "include_with_annotation",
    "issues": [
      "quantity_context"
    ],
    "annotation": "Faithful MarineTraffic-attributed traffic claim. The 70% figure concerns commercial ship traffic with no baseline/window specified; the 20% oil-supply share is background exposure, not a measured 20% supply loss. No realised price change follows from these numbers.",
    "overrides": {},
    "source_paragraph_ids": [
      "p0039"
    ],
    "checks": {
      "relevance": true,
      "quotation": true,
      "attribution": true,
      "event_type": true,
      "event_date": true,
      "quantities": true,
      "full_cited_paragraph_context": true
    }
  },
  "human_review_status": "unreviewed"
}
publication_date edition claim_status summary disposition
0 2026-02-28 evening attributed_claim Commercial ship traffic in the Strait of Hormu... include_with_annotation
1 2026-02-28 evening attributed_claim An EU official told Reuters on February 28 tha... include_with_annotation
2 2026-03-01 morning source_assessment CTP-ISW assesses that Iran may have started at... include_with_annotation
3 2026-03-01 morning attributed_claim UKMTO reported two vessels struck by unspecifi... include_with_annotation
4 2026-03-01 morning attributed_claim Report attributes to Emirati and other reporti... include_with_annotation
5 2026-03-01 evening attributed_claim Report states Iran reportedly struck Zayed Por... include_with_annotation
6 2026-03-01 evening attributed_claim UKMTO reported on March 1 that a projectile ex... include_with_annotation
7 2026-03-01 evening attributed_claim Commercially available maritime data indicate ... include_with_annotation
8 2026-03-01 evening attributed_claim Iranian Foreign Affairs Minister Abbas Araghch... include_with_annotation
9 2026-03-02 morning source_assessment CTP-ISW assesses that Iran is increasingly tar... include_with_annotation
10 2026-03-02 morning attributed_claim Report states Iran reportedly struck the Jebel... include_with_annotation
11 2026-03-02 morning attributed_claim Report states Iranian drones attempted to stri... include_with_annotation
12 2026-03-02 morning attributed_claim Report states Iran targeted an energy facility... include_with_annotation
13 2026-03-02 morning attributed_claim Report states an Iranian drone struck the Mesa... include_with_annotation
14 2026-03-02 morning attributed_claim Report states Iran struck the Aluminum Bahrain... include_with_annotation
15 2026-03-02 evening source_assessment The report assesses that Iran is attempting to... include_with_annotation
16 2026-03-02 evening source_assessment The report states that Iranian attacks targeti... include_with_annotation
17 2026-03-02 evening attributed_claim Qatar announced it ceased all LNG and associat... include_with_annotation
18 2026-03-02 evening attributed_claim Saudi state-owned Saudi Aramco shut down its R... include_with_annotation
19 2026-03-02 evening attributed_claim Several oil companies in Iraqi Kurdistan repor... include
20 2026-03-02 evening attributed_claim The report states natural gas and oil prices i... include_with_annotation
21 2026-03-02 evening attributed_claim IRGC adviser to former Supreme Leader Ali Kham... include_with_annotation
22 2026-03-02 evening attributed_claim The Iranian Khatam ol Anbia Central Headquarte... include_with_annotation
23 2026-03-02 evening attributed_claim UKMTO reported on March 1 that an unspecified ... include_with_annotation
24 2026-03-02 evening attributed_claim An Iraqi OSINT account claimed that Iranian-ba... include_with_annotation
25 2026-03-02 evening source_assessment CTP assesses that the Houthis would likely dir... include_with_annotation
26 2026-03-02 evening source_assessment CTP assesses the Houthis could leverage ties w... include_with_annotation
27 2026-03-03 morning source_assessment The report assesses that Iranian attacks targe... include_with_annotation
28 2026-03-03 morning attributed_claim An Israeli analyst published imagery of fires ... include_with_annotation
29 2026-03-03 morning attributed_claim Iranian drone fragments reportedly caused a ma... include_with_annotation
30 2026-03-03 morning attributed_claim An Iranian drone reportedly struck the Port of... include_with_annotation
31 2026-03-03 morning attributed_claim Several Iranian drones reportedly struck a fue... include_with_annotation
32 2026-03-03 morning attributed_claim State-owned QatarEnergy announced it halted do... include_with_annotation
33 2026-03-03 morning attributed_claim Iraq announced a halt of oil production at the... include_with_annotation
34 2026-03-03 evening attributed_claim Report states Iran conducted four attacks on c... include_with_annotation
35 2026-03-03 evening attributed_claim Report states traffic through the Strait of Ho... include_with_annotation
36 2026-03-03 evening attributed_claim Report states US President Trump ordered the U... include_with_annotation
37 2026-03-04 morning reported_observation Geolocated footage posted March 4 reportedly s... include_with_annotation
38 2026-03-04 evening forecast CTP-ISW assesses that Iran will continue attem... include_with_annotation
39 2026-03-04 evening attributed_claim Maersk reportedly suspended cargo booking acce... include_with_annotation
40 2026-03-04 evening attributed_claim Vessel traffic through the Strait of Hormuz is... include_with_annotation
41 2026-03-04 evening attributed_claim CTP-ISW reports that Qatar fully shut down gas... include_with_annotation
42 2026-03-04 evening attributed_claim CTP-ISW repeats a report that Iraq previously ... include_with_annotation
43 2026-03-04 evening forecast Companies reportedly warned other Gulf states,... include_with_annotation
44 2026-03-04 evening attributed_claim European natural gas prices have reportedly su... include_with_annotation
45 2026-03-04 evening attributed_claim CTP-ISW reports that some oil and fuel buyers,... include_with_annotation
46 2026-03-04 evening forecast The report assesses that degradation of Iran's... include_with_annotation
47 2026-03-04 evening attributed_claim CTP-ISW relays five UKMTO maritime-security in... include_with_annotation
48 2026-03-04 evening attributed_claim An unspecified senior Iranian naval commander ... include_with_annotation
49 2026-03-05 morning source_assessment CTP-ISW characterizes Iranian targeting since ... include_with_annotation
50 2026-03-05 evening attributed_claim A likely Iranian or Iranian-backed militia dro... include_with_annotation
51 2026-03-05 evening attributed_claim At least two Iranian ballistic missiles struck... include_with_annotation
52 2026-03-05 evening attributed_claim Saudi Arabia has shifted crude exports from St... include_with_annotation
53 2026-03-05 evening attributed_claim The report states that Qatar shut down gas liq... include_with_annotation
54 2026-03-05 evening attributed_claim UKMTO reported on March 4 an explosion on the ... include_with_annotation
55 2026-03-05 evening attributed_claim The IRGC Navy stated that it would target US, ... include_with_annotation
56 2026-03-05 evening attributed_claim Reuters, citing ship-tracking data, reported t... include_with_annotation
57 2026-03-06 evening attributed_claim UKMTO reported on March 6 that unknown project... include_with_annotation
58 2026-03-06 evening attributed_claim The report describes likely militia rocket and... include_with_annotation

Reproducibility¶

The code, raw snapshots, SHA-256 manifest, processed tables, release calendar, tested dependency lock, unit/integration tests and this executed notebook are included. Run python -m pytest from the project root. reports/validation.json records the actual final validation run. AI usage and verification describes the tools used, checks performed and responsibility for the analysis.

Primary references: BLS CPI, BLS API, BLS CPI schedule, Truflation methodology, Truflation 2026 weights, Truflation methodology updates, EIA gasoline, NBER dates. Additional claim-specific sources are in the reports and model audit.